PostgreSQLで行ごとに一意のカウンターを維持するにはどうすればよいですか?


10

document_revisionsテーブルに一意の(行ごとの)リビジョン番号を保持する必要があります。リビジョン番号はドキュメントにスコープが設定されているため、テーブル全体ではなく、関連するドキュメントに対してのみです。

私は最初に次のようなものを思いつきました:

current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);

しかし、競合状態があります!

私はそれをpg_advisory_lockで解決しようとしていますが、ドキュメントは少し不足していて、完全に理解していません。誤って何かをロックしたくありません。

以下は許容されますか、それとも間違っていますか、それともより良い解決策がありますか?

SELECT pg_advisory_lock(123);
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
SELECT pg_advisory_unlock(123);

代わりに、特定の操作(key2)のドキュメント行(key1)をロックするべきではありませんか?だからそれは適切な解決策でしょう:

SELECT pg_advisory_lock(id, 1) FROM documents WHERE id = 123;
current_rev = SELECT MAX(rev) FROM document_revisions WHERE document_id = 123;
INSERT INTO document_revisions(rev) VALUES(current_rev + 1);
SELECT pg_advisory_unlock(id, 1) FROM documents WHERE id = 123;

たぶん私はPostgreSQLに慣れておらず、SERIALのスコープを設定できますか、それともシーケンスを使用nextval()すれば、よりうまく機能しますか?


「指定された操作」の意味と「key2」の出所がわかりません。
TrygveLaugstøl2013

2
ペシミスティックなロックが必要な場合、ロック戦略は問題ありませんが、私はpg_advisory_xact_lockを使用して、すべてのロックがCOMMIT / ROLLBACKで自動的に解放されるようにします。
TrygveLaugstøl2013

回答:


2

あなたは、テーブル内の文書のすべてのリビジョンを保存すると仮定すると、アプローチがするだろうではないリビジョン番号を格納しますが、テーブルに保存されているリビジョンの数に基づいて、それを計算します。

これは本質的に派生値であり、保存する必要があるものではありません。

ウィンドウ関数を使用して、次のようなリビジョン番号を計算できます。

row_number() over (partition by document_id order by <change_date>)

change_dateリビジョンの順序を追跡するような列が必要になります。


一方、revisionドキュメントのプロパティとしてのみがあり、「ドキュメントが変更された回数」を示している場合は、次のような楽観的ロックアプローチを使用します。

update documents
set revision = revision + 1
where document_id = <id> and revision = <old_revision>;

これが0行を更新する場合、中間更新があり、ユーザーにこれを通知する必要があります。


一般に、ソリューションをできるだけシンプルに保つようにしてください。この場合、

  • 絶対に必要な場合を除き、明示的なロック関数の使用を避ける
  • データベースオブジェクトの数が少なく(ドキュメントシーケンスごとではなく)、属性の格納数が少ない(計算できる場合はリビジョンを保存しない)
  • orが後に続くのupdateではなく、単一のステートメントを使用するselectinsertupdate

実際、計算できるときに値を格納する必要はありません。思い出させてくれてありがとう!
Julien Portalier 2013

2
実は、私の状況では、古いリビジョンは:)いくつかの時点で削除されますので、私はそれを計算することができないか、リビジョン番号が減少するであろう
ジュリアンPortalier

3

SEQUENCEは一意であることが保証されており、ドキュメントの数が多すぎない場合(使用するシーケンスが多数ある場合)は、ユースケースが適切に見えるようになります。RETURNING句を使用して、シーケンスによって生成された値を取得します。たとえば、document_idとして「A36」を使用します。

  • ドキュメントごとに、増分を追跡するシーケンスを作成できます。
  • シーケンスの管理には注意が必要です。テーブルのdocument_id挿入/更新時に参照するために、ドキュメント名とそれに関連付けられたシーケンスを含む別のテーブルを保持することができdocument_revisionsます。

     CREATE SEQUENCE d_r_document_a36_seq;
    
     INSERT INTO document_revisions (document_id, rev)
     VALUES ('A36',nextval('d_r_document_a36_seq')) RETURNING rev;

フォーマットのdeszoをありがとう、コメントに貼り付けたときの見栄えが悪いことに気づきませんでした。
bma 2013

トランザクション内で実行されないため、次の値を前の+ 1にしたい場合、シーケンスは不適切なカウンターです。
TrygveLaugstøl2013

1
え?シーケンスはアトミックです。そのため、ドキュメントごとにシーケンスを提案しました。また、シーケンスがインクリメントされた後にロールバックによってシーケンスがデインクメントされないため、ギャップがないことが保証されていません。適切なロックが良い解決策ではなく、シーケンスが代替案を提示するというだけではありません。
bma 2013

1
ありがとう!リビジョン番号を保存する必要がある場合、シーケンスは間違いなく進むべき道です。
Julien Portalier 2013

2
シーケンスは基本的に1行のテーブルであるため、大量のシーケンスがあるとパフォーマンスに大きな影響を与えることに注意してください。詳細については、こちら
Magnuss

2

これはしばしば楽観的ロックで解決されます:

SELECT version, x FROM foo;

version | foo
    123 | ..

UPDATE foo SET x=?, version=124 WHERE version=123

更新によって0行の更新が返された場合、他の誰かがすでに行を更新しているため、更新を見逃しています。


ありがとう!これは、ドキュメントの更新のカウンターを維持する必要がある場合に役立ちます。しかし、私はdocument_revisionsテーブルの各行に一意のリビジョン番号が必要です。これは更新されず、前のリビジョンのフォロワーでなければなりません(つまり、前の行のリビジョン番号+ 1)。
Julien Portalier 2013

1
えっと、どうしてこのテクニックを使えないのですか?これは、ギャップのないシーケンスを提供する唯一の方法(悲観的ロック以外)です。
TrygveLaugstøl2013

2

(私はこのトピックに関する記事を再発見しようとしたときにこの質問に行きました。それを見つけたので、他の人が現在選択されている回答の代替オプションを追求している場合に備えて、ここに投稿します。row_number()

これと同じ使用例があります。SaaSの特定のプロジェクトに挿入された各レコードについて、同時INSERTsに直面して生成でき、理想的にはギャップのない、一意の増分番号が必要です。

この記事では、簡単な方法と後世のためにここで要約する、優れたソリューションについて説明します。

  1. 次の値を提供するためのカウンターとして機能する別のテーブルを用意します。2つの列がdocument_idありcounterます。counterなりますDEFAULT 0あなたが既に持っている場合は、代わりにdocumentグループのすべてのバージョンは、そのエンティティcounterが追加される可能性があります。
  2. カウンター()をアトミックに増分し、そのカウンター値に設定するBEFORE INSERTトリガーをdocument_versionsテーブルに追加します。UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counterNEW.version

あるいは、CTEを使用してアプリケーション層でこれを行うことができる場合があります(ただし、一貫性を保つためにトリガーとして使用することをお勧めします)。

WITH version AS (
  UPDATE document_revision_counters
    SET counter = counter + 1 
    WHERE document_id = 1
    RETURNING counter
)

INSERT 
  INTO document_revisions (document_id, rev, other_data)
  SELECT 1, version.counter, 'some other data'
  FROM "version";

これは、単一のステートメントでカウンター行を変更することにより、INSERTコミットされるまで古い値の読み取りをブロックすることを除いて、最初にそれを解決しようとしていた方法と基本的に同じです。

psqlこれが実際に動作していることを示すトランスクリプトです。

scratch=# CREATE TABLE document_revisions (document_id integer, rev integer, other_data text, PRIMARY KEY (document_id, rev));
CREATE TABLE

scratch=# CREATE TABLE document_revision_counters (document_id integer PRIMARY KEY, counter integer DEFAULT 0);
CREATE TABLE

scratch=# WITH version AS (
    INSERT INTO document_revision_counters (document_id) VALUES (2)
      ON CONFLICT (document_id)
      DO UPDATE SET counter = document_revision_counters.counter + 1
      RETURNING counter;
  )
  INSERT 
    INTO document_revisions (document_id, rev, other_data)
    SELECT 2, version.counter, 'doc 1 v1'
    FROM "version";
INSERT 0 1

scratch=# WITH version AS (
    INSERT INTO document_revision_counters (document_id) VALUES (2)
      ON CONFLICT (document_id)
      DO UPDATE SET counter = document_revision_counters.counter + 1
      RETURNING counter;
  )
  INSERT 
    INTO document_revisions (document_id, rev, other_data)
    SELECT 2, version.counter, 'doc 1 v2'
    FROM "version";
INSERT 0 1

scratch=# WITH version AS (
    INSERT INTO document_revision_counters (document_id) VALUES (2)
      ON CONFLICT (document_id)
      DO UPDATE SET counter = document_revision_counters.counter + 1
      RETURNING counter;
  )
  INSERT 
    INTO document_revisions (document_id, rev, other_data)
    SELECT 2, version.counter, 'doc 2 v1'
    FROM "version";
INSERT 0 1

scratch=# SELECT * FROM document_revisions;
 document_id | rev | other_data 
-------------+-----+------------
           2 |   1 | doc 1 v1
           2 |   2 | doc 1 v2
           2 |   1 | doc 2 v1
(3 rows)

ご覧のとおり、INSERTsがどのように発生するか、つまり次のようなトリガーバージョンに注意する必要があります。

CREATE OR REPLACE FUNCTION set_doc_revision()
RETURNS TRIGGER AS $$ BEGIN
  WITH version AS (
    INSERT INTO document_revision_counters (document_id, counter) VALUES (NEW.document_id, 1)
    ON CONFLICT (document_id)
    DO UPDATE SET counter = document_revision_counters.counter + 1
    RETURNING counter
  )

  SELECT INTO NEW.rev counter FROM version; RETURN NEW; END;
$$ LANGUAGE 'plpgsql';

CREATE TRIGGER set_doc_revision BEFORE INSERT ON document_revisions
FOR EACH ROW EXECUTE PROCEDURE set_doc_revision();

これにより、任意のソースから発信されたs INSERTに直面して、sがはるかに簡単になり、データの整合性がより堅牢になりINSERTます。

scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'baz');
INSERT 0 1

scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'foo');
INSERT 0 1

scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (1, 'bar');
INSERT 0 1

scratch=# INSERT INTO document_revisions (document_id, other_data) VALUES (42, 'meaning of life');
INSERT 0 1

scratch=# SELECT * FROM document_revisions;
 document_id | rev |   other_data    
-------------+-----+-----------------
           1 |   1 | baz
           1 |   2 | foo
           1 |   3 | bar
          42 |   1 | meaning of life
(4 rows)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.