Ini adalah masalah berulang dari SELECT
atau INSERT
di bawah kemungkinan beban tulis bersamaan, terkait dengan (tetapi berbeda dari) UPSERT
(yaitu INSERT
atau UPDATE
).
Fungsi PL/pgSQL ini menggunakan UPSERT (INSERT ... ON CONFLICT .. DO UPDATE
) ke INSERT
atau SELECT
baris tunggal :
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql AS
$func$
BEGIN
SELECT tag_id -- only if row existed before
FROM tag
WHERE tag = _tag
INTO _tag_id;
IF NOT FOUND THEN
INSERT INTO tag AS t (tag)
VALUES (_tag)
ON CONFLICT (tag) DO NOTHING
RETURNING t.tag_id
INTO _tag_id;
END IF;
END
$func$;
Masih ada jendela kecil untuk kondisi balapan. Untuk membuat benar-benar yakin kami mendapatkan ID:
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql AS
$func$
BEGIN
LOOP
SELECT tag_id
FROM tag
WHERE tag = _tag
INTO _tag_id;
EXIT WHEN FOUND;
INSERT INTO tag AS t (tag)
VALUES (_tag)
ON CONFLICT (tag) DO NOTHING
RETURNING t.tag_id
INTO _tag_id;
EXIT WHEN FOUND;
END LOOP;
END
$func$;
db<>main biola di sini
Ini terus berulang hingga INSERT
atau SELECT
berhasil.Panggil:
SELECT f_tag_id('possibly_new_tag');
Jika perintah berikutnya dalam transaksi yang sama mengandalkan keberadaan baris dan sebenarnya mungkin transaksi lain memperbarui atau menghapusnya secara bersamaan, Anda dapat mengunci baris yang ada di SELECT
pernyataan dengan FOR SHARE
.
Jika baris disisipkan, baris akan dikunci (atau tidak terlihat untuk transaksi lain) hingga akhir transaksi.
Mulailah dengan kasus umum (INSERT
vs SELECT
) untuk membuatnya lebih cepat.
Terkait:
- Dapatkan ID dari INSERT bersyarat
- Cara menyertakan baris yang dikecualikan di RETURNING from INSERT ... ON CONFLICT
Solusi terkait (SQL murni) untuk INSERT
atau SELECT
beberapa baris (satu set) sekaligus:
- Bagaimana cara menggunakan RETURNING dengan ON CONFLICT di PostgreSQL?
Ada apa dengan ini solusi SQL murni?
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE sql AS
$func$
WITH ins AS (
INSERT INTO tag AS t (tag)
VALUES (_tag)
ON CONFLICT (tag) DO NOTHING
RETURNING t.tag_id
)
SELECT tag_id FROM ins
UNION ALL
SELECT tag_id FROM tag WHERE tag = _tag
LIMIT 1;
$func$;
Tidak sepenuhnya salah, tetapi gagal untuk menutup celah, seperti @FunctorSalad berhasil. Fungsi tersebut dapat muncul dengan hasil kosong jika transaksi bersamaan mencoba melakukan hal yang sama pada waktu yang sama. Panduan:
Semua pernyataan dieksekusi dengan snapshot yang sama
Jika transaksi bersamaan menyisipkan tag baru yang sama beberapa saat sebelumnya, tetapi belum dilakukan:
-
Bagian UPSERT muncul kosong, setelah menunggu transaksi bersamaan selesai. (Jika transaksi bersamaan harus mundur, itu masih memasukkan tag baru dan mengembalikan ID baru.)
-
Bagian SELECT juga muncul kosong, karena didasarkan pada snapshot yang sama, di mana tag baru dari transaksi bersamaan (namun tidak terikat) tidak terlihat.
Kami mendapatkan tidak ada . Tidak seperti yang dimaksudkan. Itu kontra-intuitif dengan logika naif (dan saya terjebak di sana), tapi begitulah cara kerja model MVCC dari Postgres - harus bekerja.
Jadi jangan gunakan ini jika beberapa transaksi dapat mencoba memasukkan tag yang sama secara bersamaan. Atau loop sampai Anda benar-benar mendapatkan baris. Loop hampir tidak akan pernah dipicu dalam beban kerja umum.
Postgres 9.4 atau lebih lama
Mengingat tabel ini (sedikit disederhanakan):
CREATE table tag (
tag_id serial PRIMARY KEY
, tag text UNIQUE
);
Sebuah hampir 100% aman fungsinya untuk menyisipkan tag baru / pilih yang sudah ada, bisa jadi seperti ini.
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT tag_id int)
LANGUAGE plpgsql AS
$func$
BEGIN
LOOP
BEGIN
WITH sel AS (SELECT t.tag_id FROM tag t WHERE t.tag = _tag FOR SHARE)
, ins AS (INSERT INTO tag(tag)
SELECT _tag
WHERE NOT EXISTS (SELECT 1 FROM sel) -- only if not found
RETURNING tag.tag_id) -- qualified so no conflict with param
SELECT sel.tag_id FROM sel
UNION ALL
SELECT ins.tag_id FROM ins
INTO tag_id;
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- insert in concurrent session?
RAISE NOTICE 'It actually happened!'; -- hardly ever happens
END;
EXIT WHEN tag_id IS NOT NULL; -- else keep looping
END LOOP;
END
$func$;
db<>main biola di sini
sqlfiddle lama
Mengapa tidak 100%? Pertimbangkan catatan dalam manual untuk UPSERT
terkait contoh:
- https://www.postgresql.org/docs/current/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE
Penjelasan
-
Coba
SELECT
pertama . Dengan cara ini Anda menghindari jauh lebih mahal penanganan pengecualian 99,99% dari waktu. -
Gunakan CTE untuk meminimalkan slot waktu (yang sudah kecil) untuk kondisi balapan.
-
Jendela waktu antara
SELECT
danINSERT
dalam satu kueri sangat kecil. Jika Anda tidak memiliki beban bersamaan yang berat, atau jika Anda dapat hidup dengan pengecualian setahun sekali, Anda dapat mengabaikan kasus tersebut dan menggunakan pernyataan SQL, yang lebih cepat. -
Tidak perlu
FETCH FIRST ROW ONLY
(=LIMIT 1
). Nama tagnya jelasUNIQUE
. -
Hapus
FOR SHARE
dalam contoh saya jika Anda biasanya tidak memilikiDELETE
concurrent bersamaan atauUPDATE
di atas tabeltag
. Membutuhkan sedikit performa. -
Jangan pernah mengutip nama bahasa:
'plpgsql'.plpgsql
adalah pengidentifikasi . Mengutip dapat menyebabkan masalah dan hanya ditoleransi untuk kompatibilitas mundur. -
Jangan gunakan nama kolom yang tidak deskriptif seperti
id
atauname
. Saat menggabungkan beberapa tabel (yang Anda lakukan dalam DB relasional) Anda berakhir dengan beberapa nama yang identik dan harus menggunakan alias.
Dibangun dalam fungsi Anda
Dengan menggunakan fungsi ini, Anda sebagian besar dapat menyederhanakan FOREACH LOOP
ke:
...
FOREACH TagName IN ARRAY $3
LOOP
INSERT INTO taggings (PostId, TagId)
VALUES (InsertedPostId, f_tag_id(TagName));
END LOOP;
...
Namun, lebih cepat, sebagai pernyataan SQL tunggal dengan unnest()
:
INSERT INTO taggings (PostId, TagId)
SELECT InsertedPostId, f_tag_id(tag)
FROM unnest($3) tag;
Menggantikan seluruh loop.
Solusi alternatif
Varian ini dibangun berdasarkan perilaku UNION ALL
dengan LIMIT
klausa:segera setelah cukup banyak baris ditemukan, sisanya tidak pernah dieksekusi:
- Bagaimana cara mencoba beberapa SELECT hingga hasilnya tersedia?
Berdasarkan ini, kami dapat mengalihdayakan INSERT
menjadi fungsi tersendiri. Hanya di sana kita membutuhkan penanganan eksepsi. Sama amannya dengan solusi pertama.
CREATE OR REPLACE FUNCTION f_insert_tag(_tag text, OUT tag_id int)
RETURNS int
LANGUAGE plpgsql AS
$func$
BEGIN
INSERT INTO tag(tag) VALUES (_tag) RETURNING tag.tag_id INTO tag_id;
EXCEPTION WHEN UNIQUE_VIOLATION THEN -- catch exception, NULL is returned
END
$func$;
Yang digunakan dalam fungsi utama:
CREATE OR REPLACE FUNCTION f_tag_id(_tag text, OUT _tag_id int)
LANGUAGE plpgsql AS
$func$
BEGIN
LOOP
SELECT tag_id FROM tag WHERE tag = _tag
UNION ALL
SELECT f_insert_tag(_tag) -- only executed if tag not found
LIMIT 1 -- not strictly necessary, just to be clear
INTO _tag_id;
EXIT WHEN _tag_id IS NOT NULL; -- else keep looping
END LOOP;
END
$func$;
-
Ini sedikit lebih murah jika sebagian besar panggilan hanya membutuhkan
SELECT
, karena blok yang lebih mahal denganINSERT
berisiEXCEPTION
klausa jarang dimasukkan. Kuerinya juga lebih sederhana. -
FOR SHARE
tidak mungkin di sini (tidak diizinkan diUNION
kueri). -
LIMIT 1
tidak akan diperlukan (diuji dalam hal 9.4). Postgres mendapatkanLIMIT 1
dariINTO _tag_id
dan hanya dijalankan sampai baris pertama ditemukan.