PostgreSQL
 sql >> Teknologi Basis Data >  >> RDS >> PostgreSQL

Bagaimana cara menggunakan RETURNING dengan ON CONFLICT di PostgreSQL?

Jawaban yang saat ini diterima tampaknya ok untuk satu target konflik, sedikit 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 tidak terlalu penting.

Namun, untuk semua kasus lain, jangan jangan perbarui baris yang identik tanpa perlu. Bahkan jika Anda tidak melihat perbedaan di permukaan, ada berbagai efek samping :

  • Ini mungkin memicu pemicu yang tidak boleh dipicu.

  • Ini mengunci baris "tidak bersalah", mungkin menimbulkan biaya untuk transaksi bersamaan.

  • Ini mungkin membuat baris tampak baru, meskipun sudah lama (stempel waktu transaksi).

  • Yang terpenting , dengan model MVCC PostgreSQL, versi baris baru ditulis untuk setiap UPDATE , tidak peduli apakah data baris berubah. Ini menimbulkan penalti kinerja untuk UPSERT itu sendiri, tabel mengasapi, indeks mengasapi, hukuman kinerja untuk operasi selanjutnya di atas meja, VACUUM biaya. Efek kecil untuk beberapa duplikat, tetapi besar untuk sebagian besar penipuan.

Plus , terkadang tidak praktis atau bahkan tidak mungkin menggunakan ON CONFLICT DO UPDATE . Panduan:

Untuk ON CONFLICT DO UPDATE , sebuah conflict_target harus disediakan.

Sebuah tunggal "target konflik" tidak dimungkinkan jika banyak indeks/kendala yang terlibat. Tapi di sini ada solusi terkait untuk beberapa indeks parsial:

  • UPSERT berdasarkan batasan UNIK dengan nilai NULL

Kembali ke topik, Anda dapat mencapai (hampir) hal yang sama tanpa pembaruan kosong dan efek samping. Beberapa solusi berikut juga bekerja dengan ON CONFLICT DO NOTHING (tidak ada "target konflik"), untuk menangkap semua kemungkinan konflik yang mungkin timbul - yang mungkin diinginkan atau tidak diinginkan.

Tanpa beban 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

source kolom adalah tambahan opsional untuk menunjukkan cara kerjanya. Anda mungkin benar-benar membutuhkannya untuk membedakan antara kedua kasus (keuntungan lain dibandingkan penulisan kosong).

JOIN chats terakhir berfungsi karena baris yang baru dimasukkan dari CTE pengubah data yang dilampirkan belum terlihat di tabel yang mendasarinya. (Semua bagian dari pernyataan SQL yang sama melihat snapshot yang sama dari tabel yang mendasarinya.)

Sejak VALUES ekspresi berdiri sendiri (tidak langsung dilampirkan ke INSERT ) Postgres tidak dapat memperoleh tipe data dari kolom target dan Anda mungkin harus menambahkan gips tipe eksplisit. Panduan:

Saat VALUES digunakan dalam INSERT , semua nilai secara otomatis dipaksakan ke tipe data dari kolom tujuan yang sesuai. Ketika digunakan dalam konteks lain, mungkin perlu untuk menentukan tipe data yang benar. Jika semua entri adalah konstanta literal yang dikutip, memaksa yang pertama sudah cukup untuk menentukan tipe yang diasumsikan untuk semua.

Permintaan itu sendiri (tidak termasuk efek samping) mungkin sedikit lebih mahal untuk beberapa penipuan, karena overhead CTE dan SELECT . tambahan (yang seharusnya murah karena indeks yang sempurna ada menurut definisi - batasan unik diimplementasikan dengan indeks).

Mungkin (jauh) lebih cepat untuk banyak duplikat. Biaya efektif penulisan tambahan bergantung pada banyak faktor.

Tapi ada lebih sedikit efek samping dan biaya tersembunyi dalam hal apapun. Ini kemungkinan besar secara keseluruhan lebih murah.

Urutan terlampir masih lanjutan, karena nilai default diisi sebelum menguji konflik.

Tentang CTE:

  • Apakah kueri jenis SELECT satu-satunya jenis yang dapat disarangkan?
  • Deduplikat pernyataan SELECT dalam divisi relasional

Dengan beban tulis bersamaan

Dengan asumsi READ COMMITTED isolasi transaksi. Terkait:

  • Transaksi bersamaan menghasilkan kondisi balapan dengan batasan unik pada sisipan

Strategi terbaik untuk bertahan melawan kondisi balapan tergantung pada persyaratan yang tepat, jumlah dan ukuran baris dalam tabel dan di UPSERT, jumlah transaksi bersamaan, kemungkinan konflik, sumber daya yang tersedia, dan faktor lainnya ...

Masalah konkurensi 1

Jika transaksi bersamaan telah ditulis ke baris yang sekarang coba diUPSERT oleh transaksi Anda, transaksi Anda harus menunggu hingga transaksi lainnya selesai.

Jika transaksi lainnya diakhiri dengan ROLLBACK (atau kesalahan apa pun, yaitu ROLLBACK otomatis ), transaksi Anda dapat berjalan normal. Kemungkinan efek samping kecil:celah dalam nomor urut. Tapi tidak ada baris yang hilang.

Jika transaksi lain berakhir normal (implisit atau eksplisit COMMIT ), INSERT . Anda akan mendeteksi konflik (UNIQUE indeks / batasan adalah mutlak) 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 .) SELECT melihat snapshot yang sama dari awal kueri dan juga tidak dapat mengembalikan baris yang belum terlihat.

Setiap baris seperti itu hilang dari kumpulan hasil (meskipun 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 untuk mengatasinya.

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 query baru (bisa dalam transaksi yang sama), yang kemudian akan melihat baris-baris yang baru di-commit.

Atau periksa baris hasil yang hilang di dalam kueri yang sama dan timpa mereka 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 ups CTE , sebelum kami mengembalikan lengkap hasil yang ditetapkan. CTE terakhir itu tidak akan melakukan apa-apa hampir sepanjang waktu. Hanya jika baris hilang dari hasil yang dikembalikan, kami menggunakan kekerasan.

Lebih banyak overhead, belum. Semakin banyak konflik dengan baris yang sudah ada sebelumnya, semakin besar kemungkinan ini akan mengungguli pendekatan sederhana.

Satu efek samping:UPSERT ke-2 menulis baris yang tidak berurutan, sehingga memunculkan kembali kemungkinan kebuntuan (lihat di bawah) jika tiga atau lebih transaksi menulis ke baris yang sama tumpang tindih. Jika itu masalah, Anda memerlukan solusi lain - seperti mengulang seluruh pernyataan seperti yang disebutkan di atas.

Masalah konkurensi 2

Jika transaksi bersamaan dapat menulis ke kolom yang terlibat dari baris yang terpengaruh, dan Anda harus memastikan baris yang Anda temukan masih ada di tahap selanjutnya dalam transaksi yang sama, Anda dapat mengunci baris yang ada murah di ins CTE (yang seharusnya tidak terkunci) dengan:

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

Dan tambahkan klausa penguncian ke SELECT juga, seperti FOR UPDATE .

Ini membuat operasi penulisan yang bersaing menunggu hingga akhir transaksi, ketika semua kunci dilepaskan. Jadi singkat saja.

Lebih detail dan penjelasan:

  • Cara menyertakan baris yang dikecualikan di RETURNING from INSERT ... ON CONFLICT
  • Apakah SELECT atau INSERT dalam fungsi rentan terhadap kondisi balapan?

Deadlock?

Bertahan melawan kebuntuan dengan menyisipkan baris dalam urutan yang konsisten . Lihat:

  • Deadlock dengan INSERT multi-baris meskipun DALAM KONFLIK TIDAK MELAKUKAN APA-APA

Tipe data dan cast

Tabel yang ada sebagai template untuk tipe data ...

Jenis gips eksplisit untuk baris pertama data dalam VALUES . yang berdiri sendiri ekspresi mungkin tidak nyaman. Ada cara di sekitarnya. Anda dapat menggunakan relasi yang ada (tabel, tampilan, ...) sebagai templat baris. Tabel target adalah pilihan yang jelas untuk kasus penggunaan. Data input dipaksa ke jenis yang sesuai secara otomatis, seperti di 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 bekerja untuk beberapa tipe data. Lihat:

  • Mentransmisikan jenis NULL saat memperbarui beberapa baris

... dan nama

Ini juga berfungsi untuk semua tipe data.

Saat menyisipkan ke semua kolom (utama) tabel, Anda dapat menghilangkan nama kolom. Dengan asumsi tabel chats pada 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:jangan gunakan kata-kata khusus seperti "user" sebagai pengenal. Itu pistol yang dimuat. Gunakan pengidentifikasi legal, huruf kecil, tanpa tanda kutip. Saya menggantinya dengan usr .



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Apakah ada sesuatu seperti fungsi Zip() di PostgreSQL yang menggabungkan dua array?

  2. Postgres/JSON - perbarui semua elemen array

  3. PostgreSQL, seret dan tukar

  4. SQL INSERT tanpa menentukan kolom. Apa yang terjadi?

  5. Bagaimana cara mencocokkan satu hari penuh dengan bidang datetime?