Jawaban yang saat ini diterima tampaknya ok untuk target konflik tunggal, beberapa konflik, tupel kecil dan tidak ada pemicu. Ini menghindari masalah konkurensi 1 (lihat di bawah) dengan kekerasan. Solusi sederhana memiliki daya tariknya, efek sampingnya mungkin kurang penting.
Untuk semua kasus lain, meskipun, tidak memperbarui baris identik tanpa perlu. Bahkan jika Anda tidak melihat perbedaan di permukaan, ada berbagai efek samping :
Mungkin memicu pemicu yang seharusnya tidak dipecat.
Itu menulis-mengunci baris "tidak bersalah", mungkin menimbulkan biaya untuk transaksi bersamaan.
Mungkin membuat baris tampak baru, meskipun sudah lama (stempel waktu transaksi).
Yang paling penting , dengan model MVCC PostgreSQL versi baris baru ditulis untuk setiap UPDATE
, tidak peduli apakah data baris diubah. Ini menimbulkan penalti kinerja untuk UPSERT itu sendiri, tabel mengasapi, indeks mengasapi, penalti kinerja untuk operasi selanjutnya di atas meja, VACUUM
biaya. Efek kecil untuk beberapa duplikat, tetapi besar untuk sebagian besar dupes.
Plus , kadang tidak praktis atau bahkan mungkin digunakan ON CONFLICT DO UPDATE
. Manual:
Untuk ON CONFLICT DO UPDATE
, conflict_target
harus disediakan.
Sebuah tunggal "Target konflik" tidak mungkin jika beberapa indeks / kendala yang terlibat.
Anda dapat mencapai (hampir) sama tanpa pembaruan dan efek samping yang kosong. Beberapa solusi berikut juga bekerja dengan ON CONFLICT DO NOTHING
(tidak ada "target konflik"), untuk menangkap semua kemungkinan konflik yang mungkin timbul - yang mungkin atau mungkin tidak diinginkan.
Tanpa memuat tulis bersamaan
WITH input_rows(usr, contact, name) AS (
VALUES
(text 'foo1', text 'bar1', text 'bob1') -- type casts in first row
, ('foo2', 'bar2', 'bob2')
-- more?
)
, ins AS (
INSERT INTO chats (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id --, usr, contact -- return more columns?
)
SELECT 'i' AS source -- 'i' for 'inserted'
, id --, usr, contact -- return more columns?
FROM ins
UNION ALL
SELECT 's' AS source -- 's' for 'selected'
, c.id --, usr, contact -- return more columns?
FROM input_rows
JOIN chats c USING (usr, contact); -- columns of unique index
The source
kolom tambahan opsional untuk menunjukkan bagaimana ini bekerja. Anda mungkin benar-benar membutuhkannya untuk memberi tahu perbedaan antara kedua kasus (keuntungan lain dari penulisan kosong).
Final JOIN chats
berfungsi karena baris yang baru dimasukkan dari CTE pengubah data terlampir belum terlihat dalam tabel di bawahnya. (Semua bagian dari pernyataan SQL yang sama melihat snapshot yang sama dari tabel yang mendasarinya.)
Karena VALUES
ekspresi itu berdiri sendiri (tidak melekat langsung ke suatu INSERT
) Postgres tidak dapat memperoleh tipe data dari kolom target dan Anda mungkin harus menambahkan gips tipe eksplisit. Manual:
Ketika VALUES
digunakan INSERT
, nilai-nilai semua secara otomatis dipaksa ke tipe data dari kolom tujuan yang sesuai. Saat digunakan dalam konteks lain, mungkin perlu menentukan tipe data yang benar. Jika entri semua dikutip konstanta literal, paksaan yang pertama sudah cukup untuk menentukan jenis asumsi untuk semua.
Permintaan itu sendiri (tidak menghitung efek samping) mungkin sedikit lebih mahal untuk beberapa dupe, karena overhead CTE dan tambahan SELECT
(yang seharusnya murah karena indeks sempurna ada menurut definisi - batasan unik diterapkan dengan sebuah indeks).
Mungkin (jauh) lebih cepat untuk banyak duplikat. Biaya efektif untuk menulis tambahan tergantung pada banyak faktor.
Tetapi ada sedikit efek samping dan biaya tersembunyi dalam kasus apa pun. Secara keseluruhan mungkin lebih murah.
Urutan terlampir masih lanjut, karena nilai default diisi sebelum pengujian untuk konflik.
Tentang CTE:
Dengan beban tulis bersamaan
Dengan asumsi READ COMMITTED
isolasi transaksi standar . Terkait:
Strategi terbaik untuk bertahan terhadap kondisi balapan tergantung pada persyaratan yang tepat, jumlah dan ukuran baris dalam tabel dan dalam UPSERT, jumlah transaksi bersamaan, kemungkinan konflik, sumber daya yang tersedia, dan faktor lainnya ...
Masalah konkurensi 1
Jika transaksi konkuren telah menulis ke baris yang sekarang coba Anda transaksikan oleh transaksi, transaksi Anda harus menunggu yang lain selesai.
Jika transaksi lain berakhir dengan ROLLBACK
(atau kesalahan, yaitu otomatis ROLLBACK
), transaksi Anda dapat berjalan secara normal. Kemungkinan kecil efek samping: kesenjangan dalam angka berurutan. Tapi tidak ada baris yang hilang.
Jika transaksi lain berakhir secara normal (implisit atau eksplisit COMMIT
), Anda INSERT
akan mendeteksi konflik ( UNIQUE
indeks / kendala absolut) dan DO NOTHING
, karenanya juga tidak mengembalikan baris. (Juga tidak dapat mengunci baris seperti yang ditunjukkan dalam masalah konkurensi 2 di bawah, karena tidak terlihat .) The SELECT
melihat snapshot yang sama dari awal permintaan dan juga tidak dapat mengembalikan baris yang belum terlihat.
Setiap baris seperti itu tidak ada pada set hasil (meskipun mereka ada di tabel yang mendasarinya)!
Ini mungkin baik-baik saja . Terutama jika Anda tidak mengembalikan baris seperti pada contoh dan puas mengetahui baris ada di sana. Jika itu tidak cukup baik, ada berbagai cara di sekitarnya.
Anda dapat memeriksa jumlah baris output dan mengulangi pernyataan jika tidak cocok dengan jumlah baris input. Mungkin cukup baik untuk kasus yang jarang terjadi. Intinya adalah memulai kueri baru (bisa dalam transaksi yang sama), yang kemudian akan melihat baris yang baru dikomit.
Atau periksa baris hasil yang hilang dalam kueri yang sama dan timpa yang dengan trik brute force yang ditunjukkan dalam jawaban Alextoni .
WITH input_rows(usr, contact, name) AS ( ... ) -- see above
, ins AS (
INSERT INTO chats AS c (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id, usr, contact -- we need unique columns for later join
)
, sel AS (
SELECT 'i'::"char" AS source -- 'i' for 'inserted'
, id, usr, contact
FROM ins
UNION ALL
SELECT 's'::"char" AS source -- 's' for 'selected'
, c.id, usr, contact
FROM input_rows
JOIN chats c USING (usr, contact)
)
, ups AS ( -- RARE corner case
INSERT INTO chats AS c (usr, contact, name) -- another UPSERT, not just UPDATE
SELECT i.*
FROM input_rows i
LEFT JOIN sel s USING (usr, contact) -- columns of unique index
WHERE s.usr IS NULL -- missing!
ON CONFLICT (usr, contact) DO UPDATE -- we've asked nicely the 1st time ...
SET name = c.name -- ... this time we overwrite with old value
-- SET name = EXCLUDED.name -- alternatively overwrite with *new* value
RETURNING 'u'::"char" AS source -- 'u' for updated
, id --, usr, contact -- return more columns?
)
SELECT source, id FROM sel
UNION ALL
TABLE ups;
Ini seperti kueri di atas, tetapi kami menambahkan satu langkah lagi dengan CTE ups
, sebelum kami mengembalikan set hasil yang lengkap . CTE terakhir tidak akan melakukan apa-apa hampir sepanjang waktu. Hanya jika baris hilang dari hasil yang dikembalikan, kami menggunakan brute force.
Lebih banyak overhead. Semakin banyak konflik dengan baris yang sudah ada, semakin besar kemungkinan ini akan mengungguli pendekatan sederhana.
Satu efek samping: UPSERT ke-2 menulis baris yang tidak sesuai, sehingga memperkenalkan kembali kemungkinan deadlock (lihat di bawah) jika tiga atau lebih transaksi menulis ke baris yang sama tumpang tindih. Jika itu masalah, Anda memerlukan solusi yang berbeda - seperti mengulangi seluruh pernyataan seperti disebutkan di atas.
Masalah konkurensi 2
Jika transaksi bersamaan dapat menulis ke kolom yang terlibat dari baris yang terpengaruh, dan Anda harus memastikan bahwa baris yang Anda temukan masih ada pada tahap selanjutnya dalam transaksi yang sama, Anda dapat mengunci baris yang ada dengan murah di CTE ins
(yang kalau tidak akan terkunci) dengan:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
Dan menambahkan klausa penguncian ke SELECT
juga, sepertiFOR UPDATE
.
Hal ini membuat operasi penulisan yang bersaing menunggu hingga akhir transaksi, ketika semua kunci dilepaskan. Jadi singkat saja.
Lebih detail dan penjelasan:
Kebuntuan?
Bertahan dari kebuntuan dengan memasukkan baris dalam urutan yang konsisten . Lihat:
Tipe data dan gips
Tabel yang ada sebagai templat untuk tipe data ...
Tipe gips yang eksplisit untuk baris pertama data dalam VALUES
ekspresi yang berdiri sendiri mungkin tidak nyaman. Ada beberapa cara untuk mengatasinya. Anda dapat menggunakan hubungan apa pun yang ada (tabel, tampilan, ...) sebagai templat baris. Tabel target adalah pilihan yang jelas untuk use case. Data input dipaksa untuk jenis yang sesuai secara otomatis, seperti dalam VALUES
klausa dari INSERT
:
WITH input_rows AS (
(SELECT usr, contact, name FROM chats LIMIT 0) -- only copies column names and types
UNION ALL
VALUES
('foo1', 'bar1', 'bob1') -- no type casts here
, ('foo2', 'bar2', 'bob2')
)
...
Ini tidak berfungsi untuk beberapa tipe data. Lihat:
... dan nama
Ini juga berfungsi untuk semua tipe data.
Saat menyisipkan ke semua (memimpin) kolom tabel, Anda bisa menghilangkan nama kolom. Dengan asumsi tabel chats
dalam contoh hanya terdiri dari 3 kolom yang digunakan dalam UPSERT:
WITH input_rows AS (
SELECT * FROM (
VALUES
((NULL::chats).*) -- copies whole row definition
('foo1', 'bar1', 'bob1') -- no type casts needed
, ('foo2', 'bar2', 'bob2')
) sub
OFFSET 1
)
...
Selain itu: jangan gunakan kata - kata khusus seperti "user"
pengidentifikasi. Itu footgun yang dimuat. Gunakan pengidentifikasi legal, huruf kecil, tanda kutip. Saya menggantinya dengan usr
.
ON CONFLICT UPDATE
sehingga ada perubahan pada baris. MakaRETURNING
akan menangkapnya.