Conflict-free Replicated Data Types¶
競合のない複製されたデータ型(CRDT)は、行の1つを破棄する代わりに、同時に変更された行からの値のマージをサポートします(これは従来の競合解決が行うことです)。
各CRDTタイプは、bdr.crdt_handlersカタログに追加のコールバックが追加された個別のPostgreSQLデータタイプとして実装されます。マージプロセスは、適用側のBDRライタ内で発生します。追加のユーザー操作は必要ありません。
CRDTでは、CLCDの章で説明されているように、テーブルで列レベルの競合解決を有効にする必要があります。
ユーザが行う唯一のアクションは、整数などの標準のビルトインではなく、CREATE / ALTER TABLEで特定のデータ型を使用することです。たとえば、1つの通常の整数カウンタと1つの行がある次のテーブルを考えてください。
CREATE TABLE non_crdt_example (
* id integer PRIMARY KEY,
* counter integer NOT NULL DEFAULT 0
);
INSERT INTO non_crdt_example (id) VALUES (1);
2つのノードで次のSQLを同時に発行した場合:
UPDATE non_crdt_example
* SET counter = counter + 1 -- "reflexive" update
WHERE id = 1;
…結果の値は、両方の更新が適用された後、このクエリーを使用して表示できます。
SELECT * FROM non_crdt_example WHERE id = 1;
* id | counter
-----+-----------
* 1 | 1
(1 row)
… update_if_newerconflictリゾルバにより、増分の1つが失われたことを示しています。代わりにCRDTカウンタデータタイプを使用する場合は、次のようなことを確認する必要があります。
CREATE TABLE crdt_example (
* id integer PRIMARY KEY,
* counter bdr.crdt_gcounter NOT NULL DEFAULT 0
);
ALTER TABLE crdt_example REPLICA IDENTITY FULL;
SELECT bdr.alter_table_conflict_detection('crdt_example',
* 'column_modify_timestamp', 'cts');
INSERT INTO crdt_example (id) VALUES (1);
ここでも、2つのノードで次のSQLを同時に発行し、変更が適用されるのを待ちます。
UPDATE crdt_example
* SET counter = counter + 1 -- "reflexive" update
WHERE id = 1;
SELECT id, counter FROM crdt_example WHERE id = 1;
* id | counter
-----+-----------
* 1 | 2
(1 row)
これは、CRDTがアキュムレーター列を正しく許可することを示しています。そうでなければ、競合する非同期の同時更新に直面してもです。
crdt_gcounter型は、上記のようにx = x + 1などの再帰的なUPDATE
SQLでのみ機能する状態ベースのCRDT型の例です。
bdr.crdt_raw_value構成オプションは、クエリがCRDTタイプの現在の値または完全な内部状態を返すかどうかを決定します。デフォルトでは、現在の数値のみが返されます。
trueに設定すると、クエリは完全な状態の表現形式を結果ます-特別なハッシュ演算子(#)を使用して、特別な演算演算子を使用せずに現在の数値のみを要求できます(これはデフォルトの動作です)。
bdr.crdt_raw_value = onを使用して完全な状態がダンプされる場合、値はbdr.crdt_raw_value = onでのみ再取得できます。
注:bdr.crdt_raw_valueは、クライアントに返されるデータのフォーマットのみを適用します。つまり、選択リスト内のシンプル列参照。クエリーの他の部分の列参照(たとえば、WHERE句または選択リストの偶数式)でも、#演算子の使用が必要になる場合があります。
別のクラスのCRDTデータ型が存在します。これは「デルタCRDT」型と呼ばれます(後で説明するように、操作ベースのCRDTの特別なサブクラスです)。
デルタCRDTを使用すると、値の更新は同じノードの以前の値と自動的に比較され、その後、他のすべてのノードで変更がデルタとして適用されます。
CREATE TABLE crdt_delta_example (
* id integer PRIMARY KEY,
* counter bdr.crdt_delta_counter NOT NULL DEFAULT 0
);
ALTER TABLE crdt_delta_example REPLICA IDENTITY FULL;
SELECT bdr.alter_table_conflict_detection('crdt_delta_example',
* 'column_modify_timestamp', 'cts');
INSERT INTO crdt_delta_example (id) VALUES (1);
2つのノードで次のSQLを同時に発行した場合:
UPDATE crdt_delta_example
* SET counter = 2 -- notice NOT counter = counter + 2
WHERE id = 1;
結果の値は、両方の更新が適用された後、このクエリーを使用して表示できます。
SELECT id, counter FROM crdt_delta_example WHERE id = 1;
* id | counter
-----+---------
* 1 | 4
(1 row)
通常のinteger列では、結果はもちろん2になります。しかし、デルタCRDTカウンタで行を更新する場合、OLDrowバージョンからスタートし、新しい行バージョンをmakeして、両方をリモートノードに送信し、そこで見つかったバージョンと比較します(LOCALバージョンと呼びましょう)。標準CRDTはNEWバージョンとLOCALバージョンをマージますが、デルタCRDTはOLDバージョンとNEWバージョンを比較して、デルタをLOCALバージョンに適用します。
CRDTタイプはbdrのパートとしてbdrスキーマにインストールされます。便宜上、基本的な演算子(+、#および!)および多くの一般的な集計関数(min、max、sum、
make 、edcreate_tran_8
tweaksearch_pathを使用する必要はありません。
重要な質問は、クエリーの計画と最適化がこれらの新しいデータ型でどのように機能するかです。
CRDTタイプは透過的に処理されます-ANALYZEとオプティマイザの両方が機能するため、他に何もしなくても推定とクエリープランニングが正常に機能します。
!!! Note * 現在、この機能はEDB Postgres ExtendedおよびEDB Postgres Advancedでのみ利用可能です。
状態ベースおよび操作ベースのCRDT¶
[1]の表記に従って、操作ベースと状態ベースの両方のCRDTを実装します。
操作ベースのCRDTタイプ(CmCRDT)¶
オペレーションは明示的に転送されるのではなく、リモートノードから受信した古い行と新しい行から計算されるため、操作ベースのタイプの実装は非常に簡単です。
現在、これらの操作ベースのCRDTを実装しています。
crdt_delta_counter-bigintカウンタ(増分/減分)crdt_delta_sum-numeric総和 (増分/減分)
これらのタイプは、デルタを計算するためのビットのコードを使用して、既存のデータタイプ(bigintのcrdt_delta_counter
isaドメインなど)を活用します。
このアプローチは、デルタの計算方法がわかっている型に対してのみ可能ですが、結果は非常にシンプルで安価であり(スペースとCPUの両方の点で)、いくつかの追加の利点があり構文(たとえば、横たわっているデータタイプ)。
主な欠点は、非同期環境と同時環境ではこの値を確実にリセットできないことです。
注:カスタムデータ型を作成し、状態と最後のオペレーションを保存することにより、より複雑な操作ベースの型を実装することもできます(個々の変更をすべてデコードして転送するため、複数の操作は必要ありません)。ただし、その時点では、スペース要件を除いて、状態ベースのタイプ(リセットする機能はまだありません)に比べて利点を得ることなく、主な利点(単純さ、既存のデータ型の再利用)を失いますノードごとの状態)。
状態ベースのCRDTタイプ(CvCRDT)¶
状態ベースの型はより複雑な内部状態を必要とするため、操作ベースの型のように通常のデータ型を直接使用することはできません。
現在、4つの状態ベースのCRDTを実装しています。
crdt_gcounter-bigintカウンタ(増分のみ)crdt_gsum-numeric総和/ カウンタ (増分のみ)crdt_pncounter-bigintカウンタ(増分/減分)crdt_pnsum-numeric総和/ カウンタ (increments / decrements)
通常、内部状態にはノードごとの情報が含まディスク上のサイズは増加しますが、追加の利点があります。カスタムデータ型を実装する必要性は、より多くのコード(入力/出力関数および演算子)を意味します。
利点は、値を確実にリセットする機能、失われた変更(適切に運用されたクラスターでは発生しないはず)が存在する場合の自己回復機能、およびソースノード以外からの変更を受信する機能です。
例、ノードAで値が変更され、変更ゲットがBに複製され、Cではなく(AとC間のネットワークの問題のため)、Bが値を変更し、この変更がCに複製される場合、 Aからのオリジナルの変更。操作ベースのCRDTでは、ACネットワーク接続が再び機能するまでノードCは変更を受信しませんでした。
CvCRDTの主な欠点は、ディスクスペースの点でコストが高いことです-クラスターから既に削除されたノードを含む各ノードの情報がビット必要です)。状態の複雑な性質( varlena型にシリアル化)は、CPU使用率の増加を意味します。
ディスクスペースの要件¶
重要な考慮事項は、CRDTタイプ、特にディスク上のサイズに関連するオーバーヘッドです。
操作ベースのタイプの場合、タイプは他のタイプの上にあるドメインにすぎず、同じディスクスペース要件を持っているため(ノードがいくつあっても)、これはかなり簡単です。
crdt_delta_counter-bigintと同じ(8バイト)crdt_delta_sum-numericと同じ(変数、精度に応じて およびスケール)
操作ベースのCRDTタイプはノードごとの情報を保存しないため、ノードの数に依存しません。
状態ベースのタイプの場合、シチュエーションはより複雑です。すべてのタイプは可変長であり(本質的にbytea列として格納されます)、ヘッダーと、値を「変更」した各ノードの一定量のノードごとの情報で構成されます。
bigintバリアントの場合、おおよそのサイズを計算する式は次のとおりです(Nは、この値を変更したノードの数を示します)。
crdt_gcounter-32B (header) + N * 12B (per-node)crdt_pncounter-48B (header) + N * 20B (per-node)
numericバリアントの場合、ヘッダー部分とノードごとの部分の両方にnumeric可変長値が含まれるため、正確な式はありません。このような値をいくつ保持する必要があるかをお考えください。
crdt_gsum修正:
20B (header) + N * 4B (per-node)変数:
(2 + N)numeric値crdt_pnsum修正:
20B (header) + N * 4B (per-node)変数:
(4 + 2 * N)numeric値注*:クラスター内にいくつのノードがあるかは関係ありません。 値がマルチプルのノードで更新されることはありません。また、それは重要ではありません 更新は同時(競合の原因)であったかどうか。
注*:また、それらのノードがすでにいくつあったかは関係ありません。 クラスターから削除されました。状態を圧縮する方法はまだありません。
CRDTタイプと競合処理¶
テーブルにはCRDT列と非CRDT列の両方が含まれる可能性があるため(実際、ほとんどの列は非CRDTであると予想されます)、通常の競合解決とCRDTマージの両方を行う必要があります。
競合解決が最初に発生し、保持するタプル(適用するタプル)と破棄するタプルを決定する責任があります。次にマージフェーズが発生し、CRDT列のデータが破棄されたタプルから適用タプルにマージされます。
注*:これにより、CRDTタイプはプレーンに比べてやや高価になります 競合の解決。マージは毎回発生ニーズがあるため、 競合解決が高速パスのいずれかを使用できる場合(変更 現在のトランザクションなどで)。
CRDTタイプと競合レポート¶
デフォルトでは、検出された競合はサーバーログに書き込まれます。 CRDTタイプがなければ、これは完全に理にかなっています。なぜなら、競合の解決は本質的に利用可能な情報の半分を破棄するからです(設定によってはローカルまたはリモート行)。これはデータロスを示します。
CRDTタイプを使用すると、何も捨てずに情報の両方の部分を組み合わせて、データロスの問題を解消できます。これにより、競合レポートが不要になります。
このため、CRDT マージによって競合を完全に解決できる場合、つまり各列が次の2つの条件の少なくとも1つを満たす場合、競合レポートをスキップします。
1)ローカルとリモートのタプルの値が同じ(NULLまたは等しい)
2)CRDTデータタイプを使用します(したがって、マージできます)
注*:これは、競合がない場合にも競合レポートをスキップすることを意味します CRDT列。ただし、ローカル/リモートタプルのすべての値は等しい。
CRDT値のリセット¶
CRDT値のリセットは可能ですが、特別なハンドリングが必要です。クラスターの非同期性は、異なるノードが変更ストリームの異なる場所でリセットオペレーションを見ることができることを意味します(実装方法は関係ありません)。異なるノードも同時にリセットを開始できます。すなわち、他のノードからのリセットを観察する前に。
言い換えれmake、リセットオペレーションが正しく動作するためには、通常の操作に対して可換であるニーズがあります。値をリセットするための多くの単純な方法(単一ノードで完全に機能する場合があります)は、まさにこの理由で失敗します。
例、値をリセットする最も簡単な方法は次のとおりです。
UPDATE crdt_table SET cnt = 0 WHERE id = 1;
状態ベースのCRDTでは、これは機能しません。他のノードの状態は破棄しますが、ローカルのみです。リモートノードのマージ機能により追加され、値が発散し、最終的に他のノードの変更によりそれを受け取ります。
操作ベースのCRDTでは、更新が-cntの減算として解釈されるため、これは機能しているように見える場合があります。ただし、同時リセットがない場合にのみ機能します。
2つのノードが同時にリセットを実行しようとすると、デルタを2回適用し、負の値を取得します(リセットから予想したものではありません)。
DELETE + INSERTをリセットとして使用することもできますが、これにはいくつかの弱点もあります。行が同じキーで再挿入された場合、すべてのノードが(他のノードからの変更に関して)操作のストリームの同じ位置に表示されることは保証されません。
BDRは、同じプライマリキー値を再利用することを特に推奨します。これは、同時ケースではデータの異常を引き起こす可能性があるためです。
状態ベースのCRDTタイプは、次のような特別な!演算子を使用して、確実にリセットをハンドルできます。
UPDATE tab SET counter = !counter WHERE ...;
「信頼できる」とは、値に上記の2つの問題(マルチプルの同時リセットと発散)がないことを意味します。
操作ベースのCRDTタイプは、Eager Replicationを使用してのみ確実にリセットできます。これにより、マルチプルの同時リセットが回避されるためです。
実装されたCRDTデータ型¶
現在、6つのCRDTデータ型が実装されています-成長のみのカウンタと総和、正負のカウンタと総和、およびデルタカウンタと総和。
、「総和」タイプは10進数ベース(numeric)です。
[1]で説明されている追加のCRDTタイプは、後で実装される可能性があります。
現在実装されているCRDTデータ型は、次のクエリーでリストできます。
SELECT n.nspname, t.typname
FROM bdr.crdt_handlers c
JOIN (pg_type t JOIN pg_namespace n ON t.typnamespace = n.oid)
* ON t.oid = c.crdt_type_id;
成長専用カウンタ(crdt_gcounter)¶
負でない値の増分のみをサポート(
value + intおよび
counter + bigint演算子)
カウンタの現在の値は、
#演算子を使用して取得できます。 またはbigintにキャストしてcounter = valueのようシンプル割り当てと互換性がない (これは、新しい値がどこかで計算されるときの一般的なパターンです アプリケーション)!演算子を使用して、カウンタをシンプルにリセットできます。 (counter = !counter)crdt_gcounter_to_textを使用して内部状態を検査できます
CREATE TABLE crdt_test (
* id INT PRIMARY KEY,
* cnt bdr.crdt_gcounter NOT NULL DEFAULT 0
);
INSERT INTO crdt_test VALUES (1, 0); -- initialized to 0
INSERT INTO crdt_test VALUES (2, 129824); -- initialized to 129824
INSERT INTO crdt_test VALUES (3, -4531); -- error: negative value
-- enable CLCD on the table
ALTER TABLE crdt_test REPLICA IDENTITY FULL;
SELECT bdr.alter_table_conflict_detection('crdt_test', 'column_modify_timestamp', 'cts');
-- increment counters
UPDATE crdt_test SET cnt = cnt + 1 WHERE id = 1;
UPDATE crdt_test SET cnt = cnt + 120 WHERE id = 2;
-- error: minus operator not defined
UPDATE crdt_test SET cnt = cnt - 1 WHERE id = 1;
-- error: increment has to be non-negative
UPDATE crdt_test SET cnt = cnt + (-1) WHERE id = 1;
-- reset counter
UPDATE crdt_test SET cnt = !cnt WHERE id = 1;
-- get current counter value
SELECT id, cnt::bigint, cnt FROM crdt_test;
-- show internal structure of counters
SELECT id, bdr.crdt_gcounter_to_text(cnt) FROM crdt_test;
成長のみの総和(crdt_gsum)¶
負でない値の増分のみをサポートします(
sum + numeric)総和の現在の値は、
#演算子を使用して取得できます。 またはnumericにキャストしてsum = valueのようシンプル割り当てと互換性がない (これは、新しい値がどこかで計算されるときの一般的なパターンです アプリケーション)!演算子(sum = !sum)を使用して、総和をシンプルにリセットできます。crdt_gsum_to_textを使用して内部状態を検査できます
CREATE TABLE crdt_test (
* id INT PRIMARY KEY,
* gsum bdr.crdt_gsum NOT NULL DEFAULT 0.0
);
INSERT INTO crdt_test VALUES (1, 0.0); -- initialized to 0
INSERT INTO crdt_test VALUES (2, 1298.24); -- initialized to 1298.24
INSERT INTO crdt_test VALUES (3, -45.31); -- error: negative value
-- enable CLCD on the table
ALTER TABLE crdt_test REPLICA IDENTITY FULL;
SELECT bdr.alter_table_conflict_detection('crdt_test', 'column_modify_timestamp', 'cts');
-- increment sum
UPDATE crdt_test SET gsum = gsum + 11.5 WHERE id = 1;
UPDATE crdt_test SET gsum = gsum + 120.33 WHERE id = 2;
-- error: minus operator not defined
UPDATE crdt_test SET gsum = gsum - 15.2 WHERE id = 1;
-- error: increment has to be non-negative
UPDATE crdt_test SET gsum = gsum + (-1.56) WHERE id = 1;
-- reset sum
UPDATE crdt_test SET gsum = !gsum WHERE id = 1;
-- get current sum value
SELECT id, gsum::numeric, gsum FROM crdt_test;
-- show internal structure of sums
SELECT id, bdr.crdt_gsum_to_text(gsum) FROM crdt_test;
正負カウンタ(crdt_pncounter)¶
正と負の両方の値を持つ増分をサポート(を通じて
counter + intおよびcounter + bigint演算子)
カウンタの現在の値は、
#演算子を使用して取得できます。 またはbigintにキャストすることによりcounter = valueのようシンプル割り当てと互換性がない (これは、新しい値がどこかで計算されるときの一般的なパターンです アプリケーション)!演算子を使用して、カウンタをシンプルにリセットできます。 (counter = !counter)crdt_pncounter_to_textを使用して内部状態を検査できます
CREATE TABLE crdt_test (
* id INT PRIMARY KEY,
* cnt bdr.crdt_pncounter NOT NULL DEFAULT 0
);
INSERT INTO crdt_test VALUES (1, 0); -- initialized to 0
INSERT INTO crdt_test VALUES (2, 129824); -- initialized to 129824
INSERT INTO crdt_test VALUES (3, -4531); -- initialized to -4531
-- enable CLCD on the table
ALTER TABLE crdt_test REPLICA IDENTITY FULL;
SELECT bdr.alter_table_conflict_detection('crdt_test', 'column_modify_timestamp', 'cts');
-- increment counters
UPDATE crdt_test SET cnt = cnt + 1 WHERE id = 1;
UPDATE crdt_test SET cnt = cnt + 120 WHERE id = 2;
UPDATE crdt_test SET cnt = cnt + (-244) WHERE id = 3;
-- decrement counters
UPDATE crdt_test SET cnt = cnt - 73 WHERE id = 1;
UPDATE crdt_test SET cnt = cnt - 19283 WHERE id = 2;
UPDATE crdt_test SET cnt = cnt - (-12) WHERE id = 3;
-- get current counter value
SELECT id, cnt::bigint, cnt FROM crdt_test;
-- show internal structure of counters
SELECT id, bdr.crdt_pncounter_to_text(cnt) FROM crdt_test;
-- reset counter
UPDATE crdt_test SET cnt = !cnt WHERE id = 1;
-- get current counter value after the reset
SELECT id, cnt::bigint, cnt FROM crdt_test;
正負の総和(crdt_pnsum)¶
正と負の両方の値を持つ増分をサポート(を通じて
sum + numeric)
総和の現在の値は、
#演算子を使用して取得できます。 またはnumericにキャストすることによりsum = valueのようシンプル割り当てと互換性がない (これは、新しい値がどこかで計算されるときの一般的なパターンです アプリケーション)!演算子(sum = !sum)を使用して、総和をシンプルにリセットできます。crdt_pnsum_to_textを使用して内部状態を検査できます
CREATE TABLE crdt_test (
* id INT PRIMARY KEY,
* pnsum bdr.crdt_pnsum NOT NULL DEFAULT 0
);
INSERT INTO crdt_test VALUES (1, 0); -- initialized to 0
INSERT INTO crdt_test VALUES (2, 1298.24); -- initialized to 1298.24
INSERT INTO crdt_test VALUES (3, -45.31); -- initialized to -45.31
-- enable CLCD on the table
ALTER TABLE crdt_test REPLICA IDENTITY FULL;
SELECT bdr.alter_table_conflict_detection('crdt_test', 'column_modify_timestamp', 'cts');
-- increment sums
UPDATE crdt_test SET pnsum = pnsum + 1.44 WHERE id = 1;
UPDATE crdt_test SET pnsum = pnsum + 12.20 WHERE id = 2;
UPDATE crdt_test SET pnsum = pnsum + (-24.34) WHERE id = 3;
-- decrement sums
UPDATE crdt_test SET pnsum = pnsum - 7.3 WHERE id = 1;
UPDATE crdt_test SET pnsum = pnsum - 192.83 WHERE id = 2;
UPDATE crdt_test SET pnsum = pnsum - (-12.22) WHERE id = 3;
-- get current sum value
SELECT id, pnsum::numeric, pnsum FROM crdt_test;
-- show internal structure of sum
SELECT id, bdr.crdt_pnsum_to_text(pnsum) FROM crdt_test;
-- reset sum
UPDATE crdt_test SET pnsum = !pnsum WHERE id = 1;
-- get current sum value after the reset
SELECT id, pnsum::numeric, pnsum FROM crdt_test;
デルタカウンタ(crdt_delta_counter)¶
bigintドメインとして定義されているため、bigint列とまったく同じように機能します正と負の両方の値を持つ増分をサポート
counter = valueのようシンプル割り当てと互換性があります (新しい値がアプリケーションのどこかで計算される場合に一般的)値をリセットするシンプル方法はありません(確実)
CREATE TABLE crdt_test (
* id INT PRIMARY KEY,
* cnt bdr.crdt_delta_counter NOT NULL DEFAULT 0
);
INSERT INTO crdt_test VALUES (1, 0); -- initialized to 0
INSERT INTO crdt_test VALUES (2, 129824); -- initialized to 129824
INSERT INTO crdt_test VALUES (3, -4531); -- initialized to -4531
-- enable CLCD on the table
ALTER TABLE crdt_test REPLICA IDENTITY FULL;
SELECT bdr.alter_table_conflict_detection('crdt_test', 'column_modify_timestamp', 'cts');
-- increment counters
UPDATE crdt_test SET cnt = cnt + 1 WHERE id = 1;
UPDATE crdt_test SET cnt = cnt + 120 WHERE id = 2;
UPDATE crdt_test SET cnt = cnt + (-244) WHERE id = 3;
-- decrement counters
UPDATE crdt_test SET cnt = cnt - 73 WHERE id = 1;
UPDATE crdt_test SET cnt = cnt - 19283 WHERE id = 2;
UPDATE crdt_test SET cnt = cnt - (-12) WHERE id = 3;
-- get current counter value
SELECT id, cnt FROM crdt_test;
総和(crdt_delta_sum)¶
numericドメインとして定義されているため、numeric列とまったく同じように機能します正と負の両方の値を持つ増分をサポート
sum = valueのようシンプル割り当てと互換性があります (新しい値がアプリケーションのどこかで計算される場合に一般的)値をリセットするシンプル方法はありません(確実)
CREATE TABLE crdt_test (
* id INT PRIMARY KEY,
* dsum bdr.crdt_delta_sum NOT NULL DEFAULT 0
);
INSERT INTO crdt_test VALUES (1, 0); -- initialized to 0
INSERT INTO crdt_test VALUES (2, 129.824); -- initialized to 129824
INSERT INTO crdt_test VALUES (3, -4.531); -- initialized to -4531
-- enable CLCD on the table
ALTER TABLE crdt_test REPLICA IDENTITY FULL;
SELECT bdr.alter_table_conflict_detection('crdt_test', 'column_modify_timestamp', 'cts');
-- increment counters
UPDATE crdt_test SET dsum = dsum + 1.32 WHERE id = 1;
UPDATE crdt_test SET dsum = dsum + 12.01 WHERE id = 2;
UPDATE crdt_test SET dsum = dsum + (-2.4) WHERE id = 3;
-- decrement counters
UPDATE crdt_test SET dsum = dsum - 7.33 WHERE id = 1;
UPDATE crdt_test SET dsum = dsum - 19.83 WHERE id = 2;
UPDATE crdt_test SET dsum = dsum - (-1.2) WHERE id = 3;
-- get current counter value
SELECT id, cnt FROM crdt_test;
[1] < https://en.wikipedia.org/ wiki/ Conflict-free_replicated_data_type >