Jak używać RETURNING z ON CONFLICT w PostgreSQL?


149

Mam następujący UPSERT w PostgreSQL 9.5:

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

Jeśli nie ma konfliktów, zwraca coś takiego:

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

Ale jeśli są konflikty, nie zwraca żadnych wierszy:

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

Chcę zwrócić nowe idkolumny, jeśli nie ma konfliktów, lub zwrócić istniejące idkolumny kolumn będących w konflikcie.
Czy można to zrobić? Jeśli tak, to jak?


1
Użyj, ON CONFLICT UPDATEaby nastąpiła zmiana w wierszu. Wtedy RETURNINGgo złapie.
Gordon Linoff,

1
@GordonLinoff A jeśli nie ma nic do aktualizacji?
Okku

1
Jeśli nie ma nic do aktualizacji, oznacza to, że nie było konfliktu, więc po prostu wstawia nowe wartości i zwraca ich id
zola

1
Znajdziesz tutaj inne sposoby . Chciałbym jednak poznać różnicę między nimi pod względem wydajności.
Stanislasdrg Przywróć Monikę

Odpowiedzi:


88

Miałem dokładnie ten sam problem i rozwiązałem go za pomocą polecenia „wykonaj aktualizację” zamiast „nic nie rób”, mimo że nie miałem nic do zaktualizowania. W twoim przypadku byłoby to coś takiego:

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;

To zapytanie zwróci wszystkie wiersze, niezależnie od tego, czy zostały właśnie wstawione, czy istniały wcześniej.


11
Jednym z problemów z tym podejściem jest to, że numer sekwencyjny klucza podstawowego jest zwiększany przy każdym konflikcie (fałszywa aktualizacja), co w zasadzie oznacza, że ​​możesz skończyć z ogromnymi lukami w sekwencji. Jakieś pomysły, jak tego uniknąć?
Mischa

9
@Mischa: co z tego? Sekwencje nigdy nie są gwarantowane bez przerw, a luki nie mają znaczenia (a jeśli tak, sekwencja jest niewłaściwa)
a_horse_with_no_name

24
Bym nie radzą, aby korzystać z tego w większości przypadków. Dodałem odpowiedź dlaczego.
Erwin Brandstetter

4
Wydaje się, że ta odpowiedź nie odpowiada DO NOTHINGaspektowi pierwotnego pytania - wydaje mi się, że aktualizuje pole bez konfliktu (tutaj „nazwa”) dla wszystkich wierszy.
PeterJCLaw

Jak omówiono w bardzo długiej odpowiedzi poniżej, użycie opcji „Do Update” dla pola, które nie uległo zmianie, nie jest „czystym” rozwiązaniem i może powodować inne problemy.
Bill Worthington

202

Obecnie akceptowane odpowiedź wydaje się ok, na jeden cel konfliktom, kilka małych konfliktów, krotki i nie wyzwala. Pozwala uniknąć problemu współbieżności 1 (patrz poniżej) przy użyciu brutalnej siły. Proste rozwiązanie ma swój urok, skutki uboczne mogą być mniej ważne.

Jednak we wszystkich innych przypadkach nie aktualizuj identycznych wierszy bez potrzeby. Nawet jeśli nie widać różnicy na powierzchni, istnieją różne skutki uboczne :

  • Może uruchomić wyzwalacze, których nie należy uruchamiać.

  • Zapisuje „niewinne” wiersze, prawdopodobnie powodując koszty równoległych transakcji.

  • Może to sprawić, że wiersz będzie wydawał się nowy, chociaż jest stary (znacznik czasu transakcji).

  • Co najważniejsze , w modelu MVCC PostgreSQL- a nowa wersja wiersza jest zapisywana dla każdego UPDATE, bez względu na to, czy zmieniły się dane wiersza. Wiąże się to z obniżeniem wydajności samego UPSERT, rozdęciem tabeli, wzrostem indeksu, spadkiem wydajności dla kolejnych operacji na stole, VACUUMkosztem. Efekt moll na kilka duplikatów, ale ogromny dla większości powtórzeń.

Ponadto czasami jest to niepraktyczne lub nawet możliwe w użyciu ON CONFLICT DO UPDATE. Instrukcja:

Dla ON CONFLICT DO UPDATE, A conflict_targetmusi być zapewnione.

Single „target konflikt” nie jest możliwe w przypadku wielu indeksów / ograniczenia są zaangażowani.

Możesz osiągnąć (prawie) to samo bez pustych aktualizacji i skutków ubocznych. Niektóre z poniższych rozwiązań działają również z ON CONFLICT DO NOTHING(bez „celu konfliktu”), wychwytując wszystkie możliwe konflikty, które mogą się pojawić - co może być pożądane lub nie.

Bez jednoczesnego ładowania zapisu

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

sourceKolumna jest opcjonalnym dodatkiem do wykazania, w jaki sposób to działa. W rzeczywistości możesz go potrzebować, aby odróżnić oba przypadki (kolejna przewaga nad pustymi zapisami).

Wersja ostateczna JOIN chatsdziała, ponieważ nowo wstawione wiersze z dołączonego CTE modyfikującego dane nie są jeszcze widoczne w podstawowej tabeli. (Wszystkie części tej samej instrukcji SQL wyświetlają te same migawki bazowych tabel).

Ponieważ VALUESwyrażenie jest wolnostojące (nie jest bezpośrednio dołączone do INSERT), Postgres nie może wyprowadzać typów danych z kolumn docelowych i może być konieczne dodanie jawnych rzutów typów. Instrukcja:

Gdy VALUESjest używany w INSERT, wszystkie wartości są automatycznie przekształcane do typu danych odpowiedniej kolumny docelowej. Gdy jest używany w innych kontekstach, może być konieczne określenie prawidłowego typu danych. Jeśli wszystkie wpisy są stałymi literałowymi w cudzysłowach, wymuszenie pierwszej jest wystarczające do określenia zakładanego typu dla wszystkich.

Samo zapytanie (nie licząc efektów ubocznych) może być nieco droższe dla kilku powtórzeń, ze względu na narzut CTE i dodatkowy SELECT(który powinien być tani, ponieważ z definicji jest tam doskonały indeks - unikalne ograniczenie jest realizowane z indeks).

Może być (dużo) szybsze dla wielu duplikatów. Efektywny koszt dodatkowych zapisów zależy od wielu czynników.

Ale w każdym przypadku jest mniej skutków ubocznych i ukrytych kosztów . Najprawdopodobniej jest ogólnie tańszy.

Dołączone sekwencje są nadal zaawansowane, ponieważ wartości domyślne są wypełniane przed testowaniem pod kątem konfliktów.

O CTE:

Przy jednoczesnym obciążeniu zapisu

Zakładając domyślną READ COMMITTEDizolację transakcji . Związane z:

Najlepsza strategia obrony przed warunkami wyścigu zależy od dokładnych wymagań, liczby i rozmiaru wierszy w tabeli i na tablicach UPSERT, liczby równoległych transakcji, prawdopodobieństwa wystąpienia konfliktów, dostępnych zasobów i innych czynników ...

Kwestia współbieżności 1

Jeśli równoległa transakcja została zapisana w wierszu, który Twoja transakcja próbuje teraz wykonać do UPSERT, transakcja musi poczekać na zakończenie drugiej.

Jeśli druga transakcja zakończy się ROLLBACK(lub jakimkolwiek błędem, np. Automatycznie ROLLBACK), transakcja może przebiegać normalnie. Niewielki możliwy efekt uboczny: luki w kolejnych liczbach. Ale żadnych brakujących rzędów.

Jeśli druga transakcja zakończy się normalnie (niejawnie lub jawnie COMMIT), INSERTwykryjesz konflikt ( UNIQUEindeks / ograniczenie jest bezwzględne), a DO NOTHINGzatem również nie zwróci wiersza. (Nie można również zablokować wiersza, jak pokazano w problemie współbieżności 2 poniżej, ponieważ nie jest on widoczny ). SELECTWidzi tę samą migawkę od początku zapytania, a także nie może zwrócić jeszcze niewidocznego wiersza.

Brakuje takich wierszy w zestawie wyników (nawet jeśli istnieją w tabeli bazowej)!

To może być w porządku, jak jest . Zwłaszcza jeśli nie zwracasz wierszy, jak w przykładzie i jesteś zadowolony, wiedząc, że wiersz tam jest. Jeśli to nie wystarczy, można to obejść na różne sposoby.

Możesz sprawdzić liczbę wierszy danych wyjściowych i powtórzyć instrukcję, jeśli nie zgadza się z liczbą wierszy danych wejściowych. Może wystarczyć w rzadkich przypadkach. Chodzi o to, aby rozpocząć nowe zapytanie (może być w tej samej transakcji), co spowoduje wyświetlenie nowo zatwierdzonych wierszy.

Lub sprawdź, czy w tym samym zapytaniu nie ma brakujących wierszy wyników i nadpisz je za pomocą sztuczki brutalnej siły przedstawionej w odpowiedzi 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;

To jest podobne do zapytania powyżej, ale dodajemy jeszcze jeden krok z CTE ups, zanim zwrócimy pełny zestaw wyników. Ten ostatni CTE przez większość czasu nic nie da. Tylko jeśli brakuje wierszy w zwracanym wyniku, używamy brutalnej siły.

Jeszcze więcej. Im więcej konfliktów z istniejącymi wcześniej wierszami, tym większe prawdopodobieństwo, że będzie to skuteczniejsze niż proste podejście.

Jeden efekt uboczny: Drugi UPSERT zapisuje wiersze poza kolejnością, więc ponownie wprowadza możliwość zakleszczenia (patrz poniżej), jeśli trzy lub więcej transakcji zapisujących w tych samych wierszach zachodzi na siebie. Jeśli to problem, potrzebujesz innego rozwiązania - na przykład powtórzenia całej instrukcji, jak wspomniano powyżej.

Kwestia współbieżności 2

Jeśli współbieżne transakcje mogą zapisywać w odpowiednich kolumnach wierszy, których dotyczy problem, i musisz upewnić się, że znalezione wiersze nadal znajdują się na późniejszym etapie tej samej transakcji, możesz tanio zablokować istniejące wiersze w CTE ins(który w przeciwnym razie zostałby odblokowany) z:

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

I dodaj również klauzulę blokującą SELECT, na przykładFOR UPDATE .

To sprawia, że ​​konkurujące operacje zapisu czekają do końca transakcji, kiedy wszystkie blokady zostaną zwolnione. Więc bądź zwięzły.

Więcej szczegółów i wyjaśnienia:

Impas?

Chroń się przed zakleszczeniami , wstawiając wiersze w stałej kolejności . Widzieć:

Typy danych i rzuty

Istniejąca tabela jako szablon dla typów danych ...

Jawne rzutowania typu dla pierwszego wiersza danych w wolnostojącym VALUESwyrażeniu mogą być niewygodne. Są sposoby na obejście tego. Możesz użyć dowolnej istniejącej relacji (tabela, widok, ...) jako szablonu wiersza. Tabela docelowa jest oczywistym wyborem dla przypadku użycia. Dane wejściowe są automatycznie wymuszane na odpowiednie typy, jak w VALUESklauzuli 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')
   )
   ...

To nie działa w przypadku niektórych typów danych. Widzieć:

... i nazwiska

Działa to również dla wszystkich typów danych.

Podczas wstawiania do wszystkich (wiodących) kolumn tabeli można pominąć nazwy kolumn. Zakładając, że tabela chatsw przykładzie składa się tylko z 3 kolumn użytych w 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
   )
   ...

Poza tym: nie używaj zastrzeżonych słów, takich "user"jak identyfikator. To naładowany pistolet. Użyj prawidłowych, małych i niecytowanych identyfikatorów. Zastąpiłem go usr.


2
Sugerujesz, że ta metoda nie spowoduje powstania luk w serialach, ale są one następujące: WSTAWIĆ ... W KONFLIKCIE NIC nie zwiększa za każdym razem numeru seryjnego w porównaniu z tym, co widzę
harmic

1
nie żeby to miało tak duże znaczenie, ale dlaczego jest tak, że seriale są zwiększane? i czy nie ma sposobu, aby tego uniknąć?
wyraźny

1
@salient: Tak jak dodałem powyżej: domyślne wartości kolumn są wypełniane przed testowaniem pod kątem konfliktów, a sekwencje nigdy nie są wycofywane, aby uniknąć konfliktów z współbieżnymi zapisami.
Erwin Brandstetter

7
Niesamowite. Działa jak urok i jest łatwy do zrozumienia, gdy się mu dokładnie przyjrzy. Nadal chciałbym, ON CONFLICT SELECT...gdzie jest coś :)
Roshambo

3
Niesamowite. Wydaje się, że twórcy Postgresa torturują użytkowników. Dlaczego po prostu nie sprawić, by klauzula zwracająca zawsze zwracała wartości, niezależnie od tego, czy były wstawki, czy nie?
Anatoly Alekseev

16

Upsert, będąc rozszerzeniem INSERTzapytania, można zdefiniować za pomocą dwóch różnych zachowań w przypadku konfliktu ograniczeń: DO NOTHINGlub DO UPDATE.

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

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

Zauważ również, że RETURNINGnic nie zwraca, ponieważ nie zostały wstawione krotki . Teraz z DO UPDATEmożna wykonywać operacje na krotce, z którą występuje konflikt. Po pierwsze należy zauważyć, że ważne jest, aby zdefiniować ograniczenie, które będzie używane do zdefiniowania konfliktu.

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
Niezły sposób, aby zawsze uzyskać identyfikator wiersza, którego dotyczy problem, i wiedzieć, czy był to insert, czy upsert. Dokładnie to, czego potrzebowałem.
Moby Duck

Nadal korzysta z funkcji „Do Update”, której wady zostały już omówione.
Bill Worthington

4

W przypadku wstawiania pojedynczego przedmiotu prawdopodobnie użyłbym koalesce podczas zwracania identyfikatora:

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

2
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;

Głównym celem używania ON CONFLICT DO NOTHINGjest uniknięcie zgłaszania błędu, ale nie spowoduje to powrotu wiersza. Potrzebujemy więc innego, SELECTaby uzyskać istniejący identyfikator.

W tym SQL, jeśli zakończy się niepowodzeniem w przypadku konfliktów, nic nie zwróci, a drugi SELECTotrzyma istniejący wiersz; jeśli wstawi się pomyślnie, będą dwa takie same rekordy, a następnie musimy UNIONscalić wynik.


To rozwiązanie działa dobrze i pozwala uniknąć niepotrzebnego zapisu (aktualizacji) do bazy danych !! Miły!
Simon C

0

Zmodyfikowałem niesamowitą odpowiedź Erwina Brandstettera, która nie zwiększy sekwencji, a także nie będzie zapisywać i blokować żadnych wierszy. Jestem stosunkowo nowy w PostgreSQL, więc daj mi znać, jeśli zauważysz jakieś wady tej metody:

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

Zakłada się, że tabela chatsma unikalne ograniczenie dotyczące kolumn(usr, contact) .

Aktualizacja: dodano sugerowane poprawki ze Spatara (poniżej). Dzięki!


1
Zamiast po CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_existsprostu pisać r.id IS NOT NULL as row_exists. Zamiast po WHERE row_exists=FALSEprostu pisać WHERE NOT row_exists.
spatar
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.