Conflict-free Replicated Data Types

競合のないレプリケートデータ型(CRDT)は、行の1つを破棄する代わりに、同時に変更される行の値のマージをサポートしています。

各 CRDT 型は個別のPostgreSQLデータタイプとして実装され、 bdr.crdt_handlers カタログに追加のコールバックがあります。マージプロセスは、適用側のBDRライタ内で発生します。追加のユーザ操作は必要ありません。

CLCD の章に記載されているように、CRDTでは、テーブルで列レベルの競合解決を有効にする必要があります。

ユーザが行う唯一のアクションは、 整数などの標準のビルトインデータ型ではなく、 CREATE/ALTER TABLEで特定のデータタイプを使用することです。たとえば、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_newer conflict リゾルバにより、インクリメントの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 タイプは、状態ベースのCRDTタイプの例であり、上記のように x = x + 1 などの再帰 UPDATE SQLでのみ機能します。

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カウンタで行を UPDATE するときは、古い行バージョンからスタートて新しい行バージョンをmakeし、両方をリモートノードに送信します。そこで見つけたバージョンと比較します(ローカルバージョンと呼びましょう) )。標準CRDTはNEWバージョンとLOCALバージョンをマージますが、デルタCRDTはOLDバージョンとNEWバージョンを比較し、デルタをローカルバージョンに適用します。

CRDTタイプは、 bdr のパートとしてbdr スキーマにインストールされます。便宜上、基本的な演算子( + 、 # 、 ! )と多くの一般的な集計関数( min 、 max 、 sum 、 avg )が pg_catalog でmakeされ、 bdr を調整せずに使用できます。

重要な問題は、クエリーの計画と最適化がこれらの新しいデータ型でどのように機能するかです。 CRDTタイプは透過的に処理されます- ANALYZE とオプティマイザの両方が機能するため、他に何もすることなく、推定とクエリー計画が正常に機能します。

状態ベースおよび操作ベースのCRDT

[1]の表記に従って、操作ベースと状態ベースの両方のCRDTを実装します。

オペレーションベースのCRDTタイプ(CmCRDT)

操作は明示的に転送されるのではなく、リモートノードから受け取った古い行と新しい行から計算されるため、オペレーションベースの型の実装は非常に簡単です。

現在、次のオペレーションベースのCRDTを実装しています。

  • crdt_delta_counter - bigint カウンタ(インクリメント/デクリメント)

  • crdt_delta_sum - numeric 総和 (インクリメント/デクリメント)

これらの型は、既存のデータ型を活用し(たとえば、 crdt_delta_counter はbigint のドメインです)、デルタを計算するためのビットのコード。

このアプローチは、デルタの計算方法を知っている型でのみ可能ですが、結果は非常にシンプルで安価で(スペースとCPUの両方の観点から)、いくつかの追加の利点があり構文(例:基になるデータタイプ)。

主な欠点は、非同期および並行環境でこの値を確実にリセットできないことです。

注:カスタムデータ型を作成し、状態と最後のオペレーションを保存することにより、より複雑なオペレーションベースの型を実装することもできます(個々の変更をすべてデコードして転送するため、マルチプルのオペレーションは必要ありません)。しかし、その時点で、スペース要件(ノードごとの状態が必要です)。

状態ベースのCRDTタイプ(CvCRDT)

状態ベースの型はより複雑な内部状態を必要とするため、操作ベースの型のように通常のデータ型を直接使用することはできません。

現在、4つの状態ベースのCRDTを実装しています。

  • crdt_gcounter - bigint カウンタ(インクリメントのみ)

  • crdt_gsum - numeric 総和/カウンタ(インクリメントのみ)

  • crdt_pncounter - bigint カウンタ(インクリメント/デクリメント)

  • crdt_pnsum - numeric 総和/カウンタ(インクリメント/デクリメント)

通常、内部状態にはノードごとの情報が含まれるため、ディスク上のサイズは大きくなりますが、追加の利点があります。カスタムデータ型を実装する必要性は、より多くのコード(イン/アウト関数と演算子)を意味します。

利点は、値を確実にリセットできること、変更が失われた場合の自己修復性(適切に操作されたクラスターでは発生しない)、およびソースノード以外から変更を受信できることです。

例、ノードAで値が変更され、変更はBにレプリケートされますが、Cにはレプリケートされません(AとC間のネットワーク問題のため)。 Bが値を変更し、この変更がCに複製されると、Aからのオリジナルの変更も含まれます。 オペレーションベースのCRDTでは、ノードCはACネットワーク接続が再び機能し始めるまで変更を受け取りません。

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 マージの両方を実行する必要があります。

競合の解決が最初に行われ、どのタプル(applytuple)を保持し、どのタプルを破棄するかを決定します。次にマージフェーズが発生し、破棄されたタプルのCRDT列のデータがapplyタプルにマージされます。

注:これにより、競合解決で高速パス(現在のトランザクションで変更されたもの)を使用できる場合でも、マージを毎回行うニーズがあるため、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 を使用してのみ確実にリセットできます。これにより、マルチプルの同時リセットが回避されます。 Eager Replicationを使用して、いずれかの種類のCRDTを特定の値に設定することもできます。

実装されたCRDTデータ型

現在、6つのCRDTデータ型が実装されています- grow-only カウンタ and 総和、正負のカウンタ and 総和、および delta カウンタ and 総和。カウンターとサムはほとんど同じように動作しますが、「カウンタ」型は整数ベース(bigint )であり、「総和」型は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- wiki >