Gunakan beberapa target_tonflik di klausa AKTIF

95

Saya memiliki dua kolom dalam tabel col1, col2keduanya diindeks unik (col1 unik dan begitu juga col2).

Saya perlu memasukkan ke dalam tabel ini, menggunakan ON CONFLICTsintaks dan memperbarui kolom lain, tetapi saya tidak dapat menggunakan kedua kolom dalam conflict_targetklausa.

Berhasil:

INSERT INTO table
...
ON CONFLICT ( col1 ) 
DO UPDATE 
SET 
-- update needed columns here

Tetapi bagaimana melakukan ini untuk beberapa kolom, seperti ini:

...
ON CONFLICT ( col1, col2 )
DO UPDATE 
SET 
....
Oto Shavadze
sumber
4
"col1, col2, keduanya diindeks unik." apakah itu berarti col1 unik dan col2 unik atau apakah kombinasi dari col1, col2 unik?
e4c5
1
apakah itu berarti col1 unik dan col2 unik, secara individual
Oto Shavadze

Jawaban:

48

Tabel dan data sampel

CREATE TABLE dupes(col1 int primary key, col2 int, col3 text,
   CONSTRAINT col2_unique UNIQUE (col2)
);

INSERT INTO dupes values(1,1,'a'),(2,2,'b');

Mereproduksi masalah

INSERT INTO dupes values(3,2,'c')
ON CONFLICT (col1) DO UPDATE SET col3 = 'c', col2 = 2

Sebut saja Q1 ini. Hasilnya adalah

ERROR:  duplicate key value violates unique constraint "col2_unique"
DETAIL:  Key (col2)=(2) already exists.

Apa yang dokumentasi mengatakan

conflict_target dapat melakukan inferensi indeks unik. Saat melakukan inferensi, ini terdiri dari satu atau lebih kolom index_column_name dan / atau ekspresi index_expression, dan index_predicate opsional. Semua indeks unik nama_tabel yang, tanpa memperhatikan urutan, berisi persis kolom / ekspresi yang ditentukan target_targetnya disimpulkan (dipilih) sebagai indeks penengah. Jika index_predicate ditentukan, itu harus, sebagai persyaratan lebih lanjut untuk inferensi, memenuhi indeks arbiter.

Ini memberi kesan bahwa kueri berikut harus berfungsi, tetapi tidak karena sebenarnya akan memerlukan indeks unik bersama pada col1 dan col2. Namun indeks seperti itu tidak akan menjamin bahwa col1 dan col2 akan menjadi unik secara individual yang merupakan salah satu persyaratan OP.

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT (col1,col2) DO UPDATE SET col3 = 'c', col2 = 2

Sebut saja kueri ini Q2 (ini gagal dengan kesalahan sintaks)

Mengapa?

Postgresql berperilaku seperti ini karena apa yang seharusnya terjadi ketika konflik terjadi pada kolom kedua tidak didefinisikan dengan baik. Ada beberapa kemungkinan. Misalnya dalam kueri Q1 di atas, haruskah postgresql memperbarui col1ketika ada konflik col2? Tapi bagaimana jika itu mengarah pada konflik lain col1? bagaimana postgresql diharapkan menangani itu?

Sebuah solusi

Solusinya adalah menggabungkan ON CONFLICT dengan UPSERT model lama .

CREATE OR REPLACE FUNCTION merge_db(key1 INT, key2 INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        UPDATE dupes SET col3 = data WHERE col1 = key1 and col2 = key2;
        IF found THEN
            RETURN;
        END IF;

        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently, or key2
        -- already exists in col2,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO dupes VALUES (key1, key2, data) ON CONFLICT (col1) DO UPDATE SET col3 = data;
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            BEGIN
                INSERT INTO dupes VALUES (key1, key2, data) ON CONFLICT (col2) DO UPDATE SET col3 = data;
                RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- Do nothing, and loop to try the UPDATE again.
            END;
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

Anda perlu mengubah logika dari fungsi yang disimpan ini sehingga ia memperbarui kolom persis seperti yang Anda inginkan. Panggil seperti itu

SELECT merge_db(3,2,'c');
SELECT merge_db(1,2,'d');
e4c5
sumber
3
Ini adalah cara bekerja tetapi sedikit lebih banyak pekerjaan / logika daripada yang diperlukan, yang harus Anda lakukan hanyalah membuat batasan unik pada dua kolom. Lihat jawaban saya di bawah.
Jubair
dapatkah saya menggunakan solusi merge_db juga jika saya memasukkan beberapa set NILAI sekaligus?
daniyel
@daniyel Anda harus menulis ulang fungsi yang disimpan
e4c5
3
Tidak jelas bagi saya bagaimana berguna untuk menyarankan penggunaan upsert kuno - pertanyaan ini direferensikan dengan baik untuk "postgres upsert 9.5" dan bisa lebih baik dengan menjelaskan bagaimana menggunakannya dengan semua opsi constraint_names.
Pak
3
@Pak Tidak jelas bagi Anda karena Anda belum membaca pertanyaan dengan jelas. Operasi tidak mencari kunci komposit di bidang tersebut. Jawaban lain berfungsi untuk kunci komposit
e4c5
65

ON CONFLICTmembutuhkan indeks * unik untuk melakukan deteksi konflik. Jadi, Anda hanya perlu membuat indeks unik di kedua kolom:

t=# create table t (id integer, a text, b text);
CREATE TABLE
t=# create unique index idx_t_id_a on t (id, a);
CREATE INDEX
t=# insert into t values (1, 'a', 'foo');
INSERT 0 1
t=# insert into t values (1, 'a', 'bar') on conflict (id, a) do update set b = 'bar';
INSERT 0 1
t=# select * from t;
 id | a |  b  
----+---+-----
  1 | a | bar

* Selain indeks unik, Anda juga dapat menggunakan batasan pengecualian . Ini sedikit lebih umum daripada batasan unik. Misalkan tabel Anda memiliki kolom untuk iddan valid_time(dan valid_timea tsrange), dan Anda ingin mengizinkan duplikat id, tetapi tidak untuk periode waktu yang tumpang tindih. Batasan unik tidak akan membantu Anda, tetapi dengan batasan pengecualian Anda dapat mengatakan "kecualikan record baru jika mereka idsama dengan yang lama iddan juga mereka valid_timetumpang tindih valid_time."

Paul A Jungwirth
sumber
4
Apa yang dibuat ini adalah indeks unik bersama-sama buat indeks unik idx_t_id_a di t (id, a); Tentu saja OP tidak menyatakan dengan jelas apakah kedua kolom tersebut unik secara individual atau bersama-sama.
e4c5
Mengapa postgres terkadang mengatakan tidak ada kolom yang dinamai menurut indeks dan gagal digunakan ON CONFLICT?
Pak
@Pak sepertinya Anda harus menulis pertanyaan Anda sendiri dengan perintah spesifik yang Anda gunakan dan pesan kesalahan yang Anda terima.
Paul A Jungwirth
@PaulAJungwirth Saya tidak tahu, jawaban Anda tepat - indeks unik sebagai batasan untuk on conflictperintah. Kesalahannya hanya "kolom my_index_name tidak ada".
Pak
Saya tetap mencoba ini dengan batasan unik terpisah pada setiap kolom seperti yang diminta OP, dan itu tidak berhasil. Bukannya aku mengharapkannya, tapi aku berharap.
sudo
6

Saat ini (sepertinya) tidak mungkin. Baik versi ON CONFLICT sintaks terakhir tidak mengizinkan untuk mengulangi klausa, maupun dengan CTE tidak dimungkinkan: tidak mungkin untuk membobol INSERT dari ON CONFLICT untuk menambahkan lebih banyak konflik-target.

Peter Krauss
sumber
3

Jika Anda menggunakan postgres 9.5, Anda dapat menggunakan ruang TERKECUALI.

Contoh diambil dari Apa yang baru di PostgreSQL 9.5 :

INSERT INTO user_logins (username, logins)
VALUES ('Naomi',1),('James',1)
ON CONFLICT (username)
DO UPDATE SET logins = user_logins.logins + EXCLUDED.logins;
Martin Gerhardy
sumber
2
  1. Buat batasan (indeks asing, misalnya).

ATAU DAN

  1. Lihat kendala yang ada (\ d di psq).
  2. Gunakan ON CONSTRAINT (constraint_name) di klausa INSERT.
Vladimir Voznesensky
sumber
1

Vlad mendapat ide yang tepat.

Pertama, Anda harus membuat batasan unik tabel pada kolom col1, col2 Kemudian setelah Anda melakukannya, Anda dapat melakukan hal berikut:

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT ON CONSTRAINT dupes_pkey 
DO UPDATE SET col3 = 'c', col2 = 2
Jubair
sumber
5
Maaf, tetapi Anda salah memahami pertanyaan. OP tidak menginginkan batasan unik bersama.
e4c5
1

Agak hacky tetapi saya menyelesaikan ini dengan menggabungkan dua nilai dari col1 dan col2 ke dalam kolom baru, col3 (semacam indeks dari keduanya) dan membandingkannya. Ini hanya berfungsi jika Anda membutuhkannya untuk mencocokkan KEDUA col1 dan col2.

INSERT INTO table
...
ON CONFLICT ( col3 ) 
DO UPDATE 
SET 
-- update needed columns here

Dimana col3 = rangkaian nilai dari col1 dan col2.

Niko Dunk
sumber
3
Anda dapat membuat indeks unik untuk dua kolom tersebut dan memberikan batasan tersebut on conflict.
Kishore Relangi
0

Anda biasanya (menurut saya) dapat menghasilkan pernyataan dengan hanya satu on conflictyang menentukan satu-satunya kendala yang relevan, untuk hal yang Anda masukkan.

Karena biasanya, hanya satu kendala yang "relevan", pada satu waktu. (Jika banyak, maka saya bertanya-tanya apakah ada yang aneh / dirancang dengan aneh, hmm.)

Contoh:
(Lisensi: Bukan CC0, hanya CC-By)

// there're these unique constraints:
//   unique (site_id, people_id, page_id)
//   unique (site_id, people_id, pages_in_whole_site)
//   unique (site_id, people_id, pages_in_category_id)
// and only *one* of page-id, category-id, whole-site-true/false
// can be specified. So only one constraint is "active", at a time.

val thingColumnName = thingColumnName(notfificationPreference)

val insertStatement = s"""
  insert into page_notf_prefs (
    site_id,
    people_id,
    notf_level,
    page_id,
    pages_in_whole_site,
    pages_in_category_id)
  values (?, ?, ?, ?, ?, ?)
  -- There can be only one on-conflict clause.
  on conflict (site_id, people_id, $thingColumnName)   <—— look
  do update set
    notf_level = excluded.notf_level
  """

val values = List(
  siteId.asAnyRef,
  notfPref.peopleId.asAnyRef,
  notfPref.notfLevel.toInt.asAnyRef,
  // Only one of these is non-null:
  notfPref.pageId.orNullVarchar,
  if (notfPref.wholeSite) true.asAnyRef else NullBoolean,
  notfPref.pagesInCategoryId.orNullInt)

runUpdateSingleRow(insertStatement, values)

Dan:

private def thingColumnName(notfPref: PageNotfPref): String =
  if (notfPref.pageId.isDefined)
    "page_id"
  else if (notfPref.pagesInCategoryId.isDefined)
    "pages_in_category_id"
  else if (notfPref.wholeSite)
    "pages_in_whole_site"
  else
    die("TyE2ABK057")

The on conflictklausa yang dihasilkan secara dinamis, tergantung pada apa yang saya coba lakukan. Jika saya memasukkan preferensi pemberitahuan, untuk halaman - maka mungkin ada konflik unik, pada site_id, people_id, page_idbatasan tersebut. Dan jika saya mengonfigurasi preferensi notifikasi, untuk kategori - maka saya tahu bahwa batasan yang bisa dilanggar adalah site_id, people_id, category_id.

Jadi saya bisa, dan kemungkinan besar Anda juga, dalam kasus Anda ?, menghasilkan yang benar on conflict (... columns ), karena saya tahu apa yang ingin saya lakukan, dan kemudian saya tahu satu dari sekian banyak kendala unik, yang dapat dilanggar.

KajMagnus
sumber
-4

ON CONFLICT adalah solusi yang sangat ceroboh, jalankan

UPDATE dupes SET key1=$1, key2=$2 where key3=$3    
if rowcount > 0    
  INSERT dupes (key1, key2, key3) values ($1,$2,$3);

bekerja di Oracle, Postgres dan semua database lainnya

pengguna2625834
sumber
Ini bukan atom, jadi bisa gagal dan menghasilkan hasil yang salah jika ada banyak koneksi pada saat yang bersamaan.
Bogdan Mart