Ini terlihat seperti varian dari masalah urutan gapless; juga terlihat di sini.
Urutan tanpa celah memiliki masalah kinerja dan konkurensi yang serius.
Pikirkan baik-baik tentang apa yang akan terjadi jika banyak sisipan terjadi sekaligus. Anda harus siap untuk mencoba lagi penyisipan yang gagal, atau LOCK TABLE myTable IN EXCLUSIVE MODE
sebelum INSERT
jadi hanya satu INSERT
dapat terbang pada suatu waktu.
Gunakan tabel urutan dengan penguncian baris
Apa yang akan saya lakukan dalam situasi ini adalah:
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;
$$;
lalu untuk mendapatkan ID:
INSERT INTO myTable (sequence_number, blah)
VALUES (get_next_seqno(2,777), blah);
Pendekatan ini berarti bahwa hanya satu transaksi yang dapat menyisipkan baris dengan pasangan (level, mode) apa pun pada satu waktu, tetapi saya pikir ini bebas balapan.
Hati-hati dengan kebuntuan
Masih ada masalah di mana dua transaksi bersamaan dapat menemui jalan buntu jika mereka mencoba memasukkan baris dalam urutan yang berbeda. Tidak ada perbaikan yang mudah untuk ini; Anda harus memesan sisipan Anda sehingga Anda selalu memasukkan level rendah dan mode sebelum tinggi, melakukan satu sisipan per transaksi, atau hidup dengan kebuntuan dan coba lagi. Secara pribadi saya akan melakukan yang terakhir.
Contoh masalah, dengan dua sesi psql. Penyiapannya adalah:
CREATE TABLE myTable(seq_no integer primary key);
INSERT INTO sequence_numbers VALUES (1,666)
lalu dalam dua sesi:
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));
Anda akan melihat bahwa penyisipan kedua di sesi 2 akan hang tanpa kembali, karena menunggu pada kunci yang dipegang oleh sesi 1. Ketika sesi 1 melanjutkan untuk mencoba mendapatkan kunci yang dipegang oleh sesi 2 di sisipan kedua, itu juga akan menggantung. Tidak ada kemajuan yang dapat dibuat, jadi setelah satu atau dua detik PostgreSQL akan mendeteksi kebuntuan dan membatalkan salah satu transaksi, memungkinkan yang lain untuk melanjutkan:
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
Kode Anda harus siap untuk menangani ini dan mencoba lagi seluruh transaksi , atau harus menghindari kebuntuan menggunakan transaksi single-insert atau pemesanan hati-hati.
Secara otomatis membuat pasangan (level,kode) yang tidak ada
BTW, jika Anda ingin (level, kode) kombinasi yang belum ada di sequence_numbers
tabel yang akan dibuat pada penggunaan pertama, itu sangat rumit untuk diperbaiki karena ini adalah varian dari masalah upsert. Saya pribadi akan memodifikasi get_next_seqno
menjadi seperti ini:
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;
$$;
Kode ini bisa gagal, jadi Anda harus selalu siap untuk mencoba lagi transaksi. Seperti yang dijelaskan artikel depesz itu, pendekatan yang lebih kuat mungkin dilakukan tetapi biasanya tidak sepadan. Seperti yang tertulis di atas, jika dua transaksi secara bersamaan mencoba menambahkan pasangan baru (level,kode) yang sama, salah satunya akan gagal dengan:
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