PostgreSQL'de RETURNING ON CONFLICT ile nasıl kullanılır?


162

PostgreSQL 9.5'te aşağıdaki UPSERT'ye sahibim:

INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;

Çakışma yoksa şöyle bir şey döndürür:

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

Ancak çakışmalar varsa, herhangi bir satır döndürmez:

----------
    | id |
----------

idÇakışma yoksa yeni sütunları döndürmek veya idçakışan sütunların mevcut sütunlarını döndürmek istiyorum .
Bu yapılabilir mi? Öyleyse nasıl?


2
Satırda ON CONFLICT UPDATEbir değişiklik olması için kullanın . Sonra RETURNINGonu yakalayacak.
Gordon Linoff

1
@GordonLinoff Ya güncellenecek bir şey yoksa?
Okku

1
Güncellenecek bir şey yoksa, bu, herhangi bir çakışma olmadığı anlamına gelir, bu nedenle sadece yeni değerleri ekler ve kimliklerini döndürür
zola

1
Burada başka yollar bulacaksınız . Yine de performans açısından ikisi arasındaki farkı bilmek isterim.
Standaa Reinstate Monica

Yanıtlar:


94

Tam olarak aynı sorunu yaşadım ve güncellemem gereken hiçbir şey olmamasına rağmen "hiçbir şey yapma" yerine "güncelleme yap" seçeneğini kullanarak çözdüm. Senin durumunda böyle bir şey olurdu:

INSERT INTO chats ("user", "contact", "name") 
       VALUES ($1, $2, $3), 
              ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO UPDATE SET name=EXCLUDED.name RETURNING id;

Bu sorgu, yeni eklenmiş veya daha önce varolmuş olsalar da, tüm satırları döndürecektir.


12
Bu yaklaşımla ilgili bir sorun, birincil anahtarın sıra numarasının her çakışma (sahte güncelleme) üzerine artmasıdır, bu da temelde dizide büyük boşluklarla karşılaşabileceğiniz anlamına gelir. Bundan nasıl kaçınılacağına dair bir fikrin var mı?
Mischa

9
@Mischa: ne olmuş yani? Dizilerin ilk etapta boşluksuz olacağı asla garanti edilmez ve boşlukların önemi yoktur (ve eğer öyleyse, bir dizi yapılacak yanlış şeydir)
a_horse_with_no_name

24
Ben ediyorum değil çoğu durumda bu kullanmanızı tavsiye ederiz. Neden diye bir cevap ekledim.
Erwin Brandstetter

4
Bu yanıt DO NOTHING, orijinal sorunun yönüne ulaşmış gibi görünmüyor - bana göre, tüm satırlar için çakışmayan alanı (burada, "ad") güncelliyor gibi görünüyor.
PeterJCLaw

Aşağıdaki çok uzun yanıtta tartışıldığı gibi, değişmemiş bir alan için "Güncelleme Yap" seçeneğinin kullanılması "temiz" bir çözüm değildir ve başka sorunlara neden olabilir.
Bill Worthington

226

Şu anda kabul edilen yanıt , tek bir çatışma hedefi, az sayıda çatışma, küçük demetler ve tetikleyici yok için uygun görünüyor. Kaba kuvvet ile eşzamanlılık sorunu 1'i (aşağıya bakın) önler . Basit çözümün çekiciliği vardır, yan etkiler daha az önemli olabilir.

Diğer tüm durumlar için olsa da, do not gerek kalmadan aynı satırları güncelleştirmek. Yüzeyde bir fark görmeseniz bile çeşitli yan etkileri vardır :

  • Ateşlenmemesi gereken tetikleyicileri ateşleyebilir.

  • Muhtemelen eşzamanlı işlemler için maliyete neden olan "masum" satırları yazıp kilitler.

  • Eski olmasına rağmen satırın yeni görünmesine neden olabilir (işlem zaman damgası).

  • En önemlisi , PostgreSQL'in MVCC modelindeUPDATE , satır verilerinin değişip değişmediğine bakılmaksızın her biri için yeni bir satır sürümü yazılır . Bu, UPSERT'nin kendisi için bir performans cezasına, masa şişmesine, indeks şişmesine, masadaki sonraki işlemler için performans kaybına, VACUUMmaliyete neden olur. Birkaç kopya için küçük bir etki, ancak çoğunlukla kopyalar için büyük bir etki .

Artı , bazen pratik değildir ve hatta kullanılması mümkün değildir ON CONFLICT DO UPDATE. Kullanım kılavuzu:

İçin ON CONFLICT DO UPDATEbir conflict_targetsağlanmalıdır.

Bir tek çoklu endeksler / kısıtlamalar dahil değilseniz "çatışma hedef" mümkün değildir.

Boş güncellemeler ve yan etkiler olmadan (neredeyse) aynı şeyi elde edebilirsiniz. Aşağıdaki çözümlerden bazıları, ortaya çıkabilecek olası tüm çatışmaları ON CONFLICT DO NOTHINGyakalamak için ("çatışma hedefi" yok) ile de çalışır - ki bu arzu edilebilir veya olmayabilir.

Eşzamanlı yazma yükü olmadan

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

sourceSütun Bunun nasıl çalıştığını göstermek için isteğe bağlı ektir. Aslında her iki durum arasındaki farkı söylemek için buna ihtiyacınız olabilir (boş yazmalara göre başka bir avantaj).

Son JOIN chatsçalışma, eklenmiş bir veri değiştirici CTE'den yeni eklenen satırlar temel tabloda henüz görünmediği için çalışır. (Aynı SQL ifadesinin tüm bölümleri, temel tabloların aynı anlık görüntülerini görür.)

Yana VALUESifadesi (doğrudan bağlı olmayan serbest duran olduğu INSERT) Postgres hedef sütunları ve varsa türet veri tipleri açık tür yayınları eklemek mümkün değil. Kullanım kılavuzu:

İçinde VALUESkullanıldığında INSERT, değerlerin tümü otomatik olarak karşılık gelen hedef sütununun veri türüne zorlanır. Diğer bağlamlarda kullanıldığında, doğru veri türünü belirtmek gerekebilir. Girişlerin tümü alıntılanmış değişmez sabitlerse, ilkinin zorlanması, tümü için varsayılan türü belirlemek için yeterlidir.

Sorgunun kendisi (yan etkileri saymaz), CTE'nin ek yükü ve ek (mükemmel indeks tanım gereği olduğu için ucuz olmalıdır - benzersiz bir kısıtlama uygulanır) nedeniyle birkaç kopya için biraz daha pahalı olabilir. SELECTbir dizin).

Birçok kopya için (çok) daha hızlı olabilir . Ek yazma işlemlerinin etkin maliyeti birçok faktöre bağlıdır.

Ancak her durumda daha az yan etki ve gizli maliyet vardır. Muhtemelen genel olarak daha ucuzdur.

Çakışmaları test etmeden önce varsayılan değerler doldurulduğundan, ekli diziler hala ileri düzeydedir .

CTE'ler hakkında:

Eşzamanlı yazma yükü ile

Varsayılan READ COMMITTEDişlem izolasyonu varsayılır . İlişkili:

Yarış koşullarına karşı en iyi savunma stratejisi, kesin gerekliliklere, tablodaki ve UPSERT'lerdeki satırların sayısına ve boyutuna, eşzamanlı işlemlerin sayısına, çatışma olasılığına, mevcut kaynaklara ve diğer faktörlere bağlıdır ...

Eşzamanlılık sorunu 1

Eşzamanlı bir işlem, işleminizin şimdi UPSERT'ye çalıştığı bir satıra yazıldıysa, işleminizin diğerinin bitmesini beklemesi gerekir.

Diğer işlem ROLLBACK(veya herhangi bir hata, yani otomatik ROLLBACK) ile biterse , işleminiz normal şekilde devam edebilir. Küçük olası yan etkiler: ardışık sayılardaki boşluklar. Ancak eksik satır yok.

Diğer işlem normal şekilde biterse (örtük veya açık COMMIT), INSERTbir çakışma algılarsınız ( UNIQUEdizin / kısıtlama mutlaktır) ve DO NOTHINGdolayısıyla satırı da döndürmezsiniz. (Ayrıca, görünmediği için aşağıdaki eşzamanlılık sorunu 2'de gösterildiği gibi satırı kilitleyemez .) Sorgunun başından itibaren aynı anlık görüntüyü görür ve henüz görünmeyen satırı döndüremez.SELECT

Sonuç kümesinde bu tür satırlar eksiktir (alttaki tabloda bulunsalar bile)!

Bu gibi Tamam olabilir . Özellikle örnekteki gibi satırları döndürmüyorsanız ve satırın orada olduğunu bilmekten memnunsanız. Bu yeterince iyi değilse, etrafında çeşitli yollar vardır.

Çıktının satır sayısını kontrol edebilir ve girdinin satır sayısıyla eşleşmiyorsa ifadeyi tekrarlayabilirsiniz. Nadir durum için yeterince iyi olabilir. Önemli olan yeni bir sorgu başlatmaktır (aynı işlemde olabilir), bu daha sonra yeni taahhüt edilen satırları görür.

Veya aynı sorgu içinde eksik sonuç satırlarını kontrol edin ve Alextoni'nin cevabında gösterilen kaba kuvvet numarasıyla bunların üzerine yazın .

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;

Yukarıdaki sorgu gibi, ancak sonuç kümesinin tamamınıups döndürmeden önce CTE ile bir adım daha ekliyoruz . Bu son CTE çoğu zaman hiçbir şey yapmaz. Sadece dönen sonuçtan satırlar eksik kalırsa, kaba kuvvet kullanırız.

Henüz daha fazla ek yük. Önceden var olan satırlarla ne kadar fazla çakışma olursa, bu basit yaklaşımdan o kadar iyi performans gösterecektir.

Bir yan etki: 2. UPSERT, satırları sırasız yazar, bu nedenle aynı satırlara yazılan üç veya daha fazla işlemin üst üste gelmesi durumunda kilitlenme olasılığını yeniden sunar (aşağıya bakın) . Bu bir sorunsa, yukarıda belirtildiği gibi tüm ifadeyi tekrarlamak gibi farklı bir çözüme ihtiyacınız var.

Eşzamanlılık sorunu 2

Eşzamanlı işlemler, etkilenen satırların ilgili sütunlarına yazabiliyorsa ve bulduğunuz satırların aynı işlemde daha sonraki bir aşamada hala orada olduğundan emin olmanız gerekiyorsa, CTE'deki mevcut satırları ucuza kilitleyebilirsinizins (aksi takdirde kilidi açılacaktır) ile:

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

Ve , gibi bir kilitleme cümlesiSELECTFOR UPDATE ekleyin .

Bu, rakip yazma işlemlerinin, tüm kilitler serbest bırakıldığında işlemin sonuna kadar beklemesini sağlar. Bu yüzden kısa olun.

Daha fazla ayrıntı ve açıklama:

Kilitlenme mi?

Satırları tutarlı bir sırayla ekleyerek kilitlenmelere karşı savunun . Görmek:

Veri türleri ve yayınlar

Veri türleri için şablon olarak mevcut tablo ...

Serbest duran VALUESifadedeki ilk veri satırı için açık tip yayınlar uygun olmayabilir. Etrafında yollar var. Mevcut herhangi bir ilişkiyi (tablo, görünüm, ...) satır şablonu olarak kullanabilirsiniz. Hedef tablo, kullanım durumu için bariz bir seçimdir. Girdi verileri gibi, otomatik olarak uygun türlerine zorlama bir VALUESbir bölgesinin maddesi 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')
   )
   ...

Bu, bazı veri türleri için çalışmaz. Görmek:

... ve isimler

Bu aynı zamanda tüm veri türleri için de geçerlidir .

Tablonun tüm (önde gelen) sütunlarına eklerken, sütun adlarını atlayabilirsiniz. chatsÖrnekteki tablonun yalnızca UPSERT'de kullanılan 3 sütundan oluştuğunu varsayarsak :

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
   )
   ...

Bir kenara: tanımlayıcı gibi ayrılmış kelimeler kullanmayın "user". Bu dolu bir tabanca. Yasal, küçük harfli, alıntılanmamış tanımlayıcılar kullanın. İle değiştirdim usr.


2
Bu yöntemin dizilerde boşluklar yaratmayacağını ima ediyorsunuz, ancak bunlar: INSERT ...
ÇATIŞMADA

1
o kadar önemli değil, ama neden diziler çoğalıyor? ve bundan kaçınmanın bir yolu yok mu?
salient

8
İnanılmaz. Bir cazibe gibi çalışır ve dikkatlice baktığınızda anlaşılması kolaydır. Yine de ON CONFLICT SELECT...bir şeyin nerede olmasını diliyorum :)
Roshambo

2
@Roshambo: Evet, bu çok daha şık olurdu. (
Buradayken

6
İnanılmaz. Postgres'in yaratıcıları kullanıcılara işkence ediyor gibi görünüyor. Ekleme olup olmadığına bakılmaksızın neden sadece dönen cümleciğin her zaman değer döndürmesini sağlamıyorsunuz?
Anatoly Alekseev

17

Upsert, INSERTsorgunun bir uzantısı olarak, bir kısıtlama çakışması durumunda iki farklı davranışla tanımlanabilir: DO NOTHINGveya DO UPDATE.

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
   ON CONFLICT DO NOTHING RETURNING *;

 id | sub_id | status
----+--------+--------
 (0 rows)

Bunun RETURNINGhiçbir şey döndürmediğini de unutmayın , çünkü hiçbir tuple eklenmemiştir . Şimdi ile DO UPDATE, bir çatışma olan tuple üzerinde işlem yapmak mümkündür. İlk olarak, bir çatışma olduğunu tanımlamak için kullanılacak bir kısıtlama tanımlamanın önemli olduğuna dikkat edin.

INSERT INTO upsert_table VALUES (2, 2, 'inserted')
   ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key
   DO UPDATE SET status = 'upserted' RETURNING *;

 id | sub_id |  status
----+--------+----------
  2 |      2 | upserted
(1 row)

2
Etkilenen satır kimliğini her zaman almanın ve bunun bir ekleme veya yükseltme olup olmadığını bilmenin güzel bir yolu. Tam ihtiyacım olan şey.
Moby Duck

2
Bu, hala dezavantajları daha önce tartışılmış olan "Do Update" kullanmaktadır.
Bill Worthington

7
WITH e AS(
    INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
)
SELECT * FROM e
UNION
    SELECT id FROM chats WHERE user=$1, contact=$2;

Kullanmanın temel amacı ON CONFLICT DO NOTHINGhata atmayı önlemektir, ancak hiçbir satır dönüşüne neden olmaz. Yani SELECTmevcut kimliği almak için başka birine ihtiyacımız var.

Bu SQL'de, çakışmalarda başarısız olursa, hiçbir şey döndürmez, sonra ikincisi SELECTvar olan satırı alır; başarılı bir şekilde eklenirse, o zaman iki aynı kayıt olacaktır, o zaman UNIONsonucu birleştirmemiz gerekir .


1
Bu çözüm iyi çalışıyor ve DB'ye gereksiz yazma (güncelleme) yapılmasını önlüyor !! Güzel!
Simon C

5

Tek bir öğenin eklenmesi için, kimliği döndürürken muhtemelen bir birleştirme kullanırdım:

WITH new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    VALUES ($1, $2, $3)
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT COALESCE(
    (SELECT id FROM new_chats),
    (SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

0

Erwin Brandstetter'ın harika cevabını değiştirdim, bu sırayı artırmayacak ve ayrıca herhangi bir satırı yazma-kilitlemeyecek. PostgreSQL konusunda nispeten yeniyim, bu nedenle bu yöntemin herhangi bir sakıncası görürseniz lütfen bana bildirin:

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, new_rows AS (
   SELECT 
     c.usr
     , c.contact
     , c.name
     , r.id IS NOT NULL as row_exists
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   )
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

Bu, tablonun chatssütunlarda benzersiz bir kısıtlamaya sahip olduğunu varsayar (usr, contact).

Güncelleme: spatardan önerilen revizyonları ekledi (aşağıda). Teşekkürler!


1
CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_existsSadece yazmak yerine r.id IS NOT NULL as row_exists. WHERE row_exists=FALSESadece yazmak yerine WHERE NOT row_exists.
spatar
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.