これは、ギャップレスシーケンス問題の変形のように見えます。こちらもご覧ください。
ギャップのないシーケンスには、深刻なパフォーマンスと同時実行性の問題があります。
複数の挿入が同時に発生した場合に何が起こるかについて、非常によく考えてください。失敗した挿入を再試行する準備をする必要があります。または、LOCK TABLE myTable IN EXCLUSIVE MODE
INSERT
の前 したがって、INSERT
は1つだけです。 一度に飛行することができます。
行ロック付きのシーケンステーブルを使用する
この状況で私がすることは次のとおりです:
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はデッドロックを検出し、一方のトランザクションを中止して、もう一方のトランザクションを続行できるようにします。
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