これは、ギャップレス シーケンス問題の変形のように見えます。ここにも見られます。
ギャップレス シーケンスには、重大なパフォーマンスと同時実行の問題があります。
一度に複数の挿入が発生した場合に何が起こるかをよく考えてください。失敗した挿入を再試行する準備をするか、一度に1 つしか実行できないようLOCK TABLE myTable IN EXCLUSIVE MODE
にする必要があります。INSERT
INSERT
行ロックのあるシーケンス テーブルを使用する
この状況で私がすることは次のとおりです。
CREATE TABLE sequence_numbers(
level integer,
code integer,
next_value integer DEFAULT 0 NOT NULL,
PRIMARY KEY (level,code),
CONSTRAINT level_must_be_one_digit CHECK (level BETWEEN 0 AND 9),
CONSTRAINT code_must_be_three_digits CHECK (code BETWEEN 0 AND 999),
CONSTRAINT value_must_be_four_digits CHECK (next_value BETWEEN 0 AND 9999)
);
INSERT INTO sequence_numbers(level,code) VALUES (2,777);
CREATE OR REPLACE FUNCTION get_next_seqno(level integer, code integer)
RETURNS integer LANGUAGE 'SQL' AS $$
UPDATE sequence_numbers
SET next_value = next_value + 1
WHERE level = $1 AND code = $2
RETURNING (to_char(level,'FM9')||to_char(code,'FM000')||to_char(next_value,'FM0000'))::integer;
$$;
次に ID を取得します。
INSERT INTO myTable (sequence_number, blah)
VALUES (get_next_seqno(2,777), blah);
このアプローチは、一度に 1 つのトランザクションのみが特定の (レベル、モード) ペアで行を挿入できることを意味しますが、競合はないと思います。
デッドロックに注意
2 つの同時トランザクションが別の順序で行を挿入しようとすると、デッドロックする可能性があるという問題がまだ残っています。これを簡単に修正することはできません。常にローレベルとモードをハイの前に挿入するように挿入を注文するか、トランザクションごとに 1 つの挿入を行うか、デッドロックを抱えて再試行する必要があります。個人的には後者にします。
2 つの psql セッションでの問題の例。セットアップは次のとおりです。
CREATE TABLE myTable(seq_no integer primary key);
INSERT INTO sequence_numbers VALUES (1,666)
次に、2 つのセッションで:
SESSION 1 SESSION 2
BEGIN;
BEGIN;
INSERT INTO myTable(seq_no)
VALUES(get_next_seqno(2,777));
INSERT INTO myTable(seq_no)
VALUES(get_next_seqno(1,666));
INSERT INTO myTable(seq_no)
VALUES(get_next_seqno(2,777));
INSERT INTO myTable(seq_no)
VALUES(get_next_seqno(1,666));
セッション 2 の 2 番目の挿入は、セッション 1 が保持するロックを待機しているため、戻ることなくハングすることに気付くでしょう。セッション 1 が 2 番目の挿入でセッション 2 が保持するロックを取得しようとすると、それも下がる。処理を進めることができないため、1 ~ 2 秒後に PostgreSQL がデッドロックを検出し、トランザクションの 1 つを中止して、もう 1 つのトランザクションを続行できるようにします。
ERROR: deadlock detected
DETAIL: Process 16723 waits for ShareLock on transaction 40450; blocked by process 18632.
Process 18632 waits for ShareLock on transaction 40449; blocked by process 16723.
HINT: See server log for query details.
CONTEXT: SQL function "get_next_seqno" statement 1
これを処理してトランザクション全体を再試行できるようにコードを準備するか、単一挿入トランザクションまたは慎重な順序付けを使用してデッドロックを回避する必要があります。
存在しない (レベル、コード) ペアの自動作成
ところで、テーブルにまだ存在しない (レベル、コード) の組み合わせを最初の使用時に作成したい場合、これはアップサートの問題sequence_numbers
の変形であるため、驚くほど複雑です。私は個人的に次のように変更します。get_next_seqno
CREATE OR REPLACE FUNCTION get_next_seqno(level integer, code integer)
RETURNS integer LANGUAGE 'SQL' AS $$
-- add a (level,code) pair if it isn't present.
-- Racey, can fail, so you have to be prepared to retry
INSERT INTO sequence_numbers (level,code)
SELECT $1, $2
WHERE NOT EXISTS (SELECT 1 FROM sequence_numbers WHERE level = $1 AND code = $2);
UPDATE sequence_numbers
SET next_value = next_value + 1
WHERE level = $1 AND code = $2
RETURNING (to_char(level,'FM9')||to_char(code,'FM000')||to_char(next_value,'FM0000'))::integer;
$$;
このコードは失敗する可能性があるため、常にトランザクションを再試行できるようにしておく必要があります。その depesz の記事が説明しているように、より堅牢なアプローチが可能ですが、通常は価値がありません。上記のように、2 つのトランザクションが同じ新しい (レベル、コード) ペアを同時に追加しようとすると、1 つが次のエラーで失敗します。
ERROR: duplicate key value violates unique constraint "sequence_numbers_pkey"
DETAIL: Key (level, code)=(0, 555) already exists.
CONTEXT: SQL function "get_next_seqno" statement 1