web-dev-qa-db-ja.com

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

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()のほうが適していますか?

10

ドキュメントのすべてのリビジョンをテーブルに保存すると仮定すると、アプローチはnotリビジョン番号を保存しますが、テーブルに保存されているリビジョンの数に基づいて計算します。

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

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

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行を更新する場合、中間更新があり、ユーザーにこれを通知する必要があります。


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

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

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;
    
3
bma

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

SELECT version, x FROM foo;

version | foo
    123 | ..

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

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

2

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

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

この記事では素敵なソリューションについて説明します 、簡単に、そして後世のためにここで要約します。

  1. 次の値を提供するためのカウンターとして機能する別のテーブルを用意します。 document_idcounterの2つの列があります。 counterDEFAULT 0になります。または、すべてのバージョンをグループ化するdocumentエンティティがすでにある場合は、counterをそこに追加できます。
  2. BEFORE INSERTトリガーをdocument_versionsテーブルに追加します。これにより、カウンター(UPDATE document_revision_counters SET counter = counter + 1 WHERE document_id = ? RETURNING counter)が自動的にインクリメントされ、NEW.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();

これにより、任意のソースからのINSERTsに直面しても、INSERTsがはるかに単純になり、データの整合性がより堅牢になります。

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)
2
Bo Jeanes