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

Apakah SELECT atau INSERT dalam suatu fungsi rentan terhadap kondisi balapan?

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 dan INSERT 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 jelas UNIQUE .

  • Hapus FOR SHARE dalam contoh saya jika Anda biasanya tidak memiliki DELETE concurrent bersamaan atau UPDATE di atas tabel tag . 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 atau name . 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 dengan INSERT berisi EXCEPTION klausa jarang dimasukkan. Kuerinya juga lebih sederhana.

  • FOR SHARE tidak mungkin di sini (tidak diizinkan di UNION kueri).

  • LIMIT 1 tidak akan diperlukan (diuji dalam hal 9.4). Postgres mendapatkan LIMIT 1 dari INTO _tag_id dan hanya dijalankan sampai baris pertama ditemukan.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Optimalkan PostgreSQL untuk pengujian cepat

  2. Bagaimana cara menulis DataFrame ke tabel postgres?

  3. Django memodelkan satu kunci asing ke banyak tabel

  4. Bisakah PostgreSQL mengindeks kolom array?

  5. Cara Membuat Bilangan Ordinal di PostgreSQL