Saya memiliki UPSERT berikut di PostgreSQL 9.5:
INSERT INTO chats ("user", "contact", "name")
VALUES ($1, $2, $3),
($2, $1, NULL)
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;
Jika tidak ada konflik, ia mengembalikan sesuatu seperti ini:
----------
| id |
----------
1 | 50 |
----------
2 | 51 |
----------
Tetapi jika ada konflik itu tidak mengembalikan baris:
----------
| id |
----------
Saya ingin mengembalikan id
kolom baru jika tidak ada konflik atau mengembalikan id
kolom yang ada dari kolom yang bertentangan.
Bisakah ini dilakukan? Jika ya, bagaimana caranya?
ON CONFLICT UPDATE
sehingga ada perubahan pada baris. MakaRETURNING
akan menangkapnya.Jawaban:
Saya memiliki masalah yang persis sama, dan saya menyelesaikannya menggunakan 'do update' bukannya 'do nothing', meskipun saya tidak ada pembaruan. Dalam kasus Anda itu akan menjadi seperti ini:
Kueri ini akan mengembalikan semua baris, terlepas dari apakah mereka baru saja dimasukkan atau sudah ada sebelumnya.
sumber
DO NOTHING
aspek pertanyaan awal - bagi saya tampaknya memperbarui bidang non-konflik (di sini, "nama") untuk semua baris.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: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
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 suatuINSERT
) Postgres tidak dapat memperoleh tipe data dari kolom target dan Anda mungkin harus menambahkan gips tipe eksplisit. Manual: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 otomatisROLLBACK
), 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
), AndaINSERT
akan mendeteksi konflik (UNIQUE
indeks / kendala absolut) danDO NOTHING
, karenanya juga tidak mengembalikan baris. (Juga tidak dapat mengunci baris seperti yang ditunjukkan dalam masalah konkurensi 2 di bawah, karena tidak terlihat .) TheSELECT
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 .
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: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 dalamVALUES
klausa dariINSERT
: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:Selain itu: jangan gunakan kata - kata khusus seperti
"user"
pengidentifikasi. Itu footgun yang dimuat. Gunakan pengidentifikasi legal, huruf kecil, tanda kutip. Saya menggantinya denganusr
.sumber
ON CONFLICT SELECT...
ada apa :)Upsert, menjadi ekstensi dari
INSERT
kueri dapat didefinisikan dengan dua perilaku berbeda dalam kasus konflik kendala:DO NOTHING
atauDO UPDATE
.Perhatikan juga bahwa
RETURNING
tidak mengembalikan apa-apa, karena tidak ada tupel yang dimasukkan . Sekarang denganDO UPDATE
, adalah mungkin untuk melakukan operasi pada tuple ada konflik dengan. Catatan pertama bahwa penting untuk mendefinisikan batasan yang akan digunakan untuk mendefinisikan bahwa ada konflik.sumber
Untuk penyisipan satu item, saya mungkin akan menggunakan gabungan ketika mengembalikan id:
sumber
Tujuan utama penggunaan
ON CONFLICT DO NOTHING
adalah untuk menghindari kesalahan melempar, tetapi tidak akan menyebabkan baris kembali. Jadi kita perlu yang lainSELECT
untuk mendapatkan id yang ada.Dalam SQL ini, jika gagal pada konflik, tidak akan mengembalikan apa pun, maka yang kedua
SELECT
akan mendapatkan baris yang ada; jika berhasil dimasukkan, maka akan ada dua catatan yang sama, maka kita perluUNION
menggabungkan hasilnya.sumber
Saya mengubah jawaban yang luar biasa dari Erwin Brandstetter, yang tidak akan menambah urutan, dan juga tidak akan menulis-mengunci setiap baris. Saya relatif baru di PostgreSQL, jadi silakan beri tahu saya jika Anda melihat ada kekurangan pada metode ini:
Ini mengasumsikan bahwa tabel
chats
memiliki batasan unik pada kolom(usr, contact)
.Pembaruan: menambahkan revisi yang disarankan dari spatar (di bawah). Terima kasih!
sumber
CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_exists
hanya menulisr.id IS NOT NULL as row_exists
. Alih-alihWHERE row_exists=FALSE
hanya menulisWHERE NOT row_exists
.