PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

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

Obecnie przyjęta odpowiedź wydaje się poprawna dla pojedynczego celu konfliktu, kilku konfliktów, małych krotek i braku wyzwalaczy. Unika problemu ze współbieżnością 1 (patrz poniżej) z brutalną siłą. Proste rozwiązanie ma swój urok, skutki uboczne mogą być mniej ważne.

Jednak we wszystkich innych przypadkach nie aktualizować identyczne wiersze bez potrzeby. Nawet jeśli nie widzisz różnicy na powierzchni, istnieją różne skutki uboczne :

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

  • Blokuje „niewinne” wiersze przed zapisem, prawdopodobnie ponosząc koszty równoczesnych transakcji.

  • Może to sprawić, że wiersz będzie wyglądał na nowy, chociaż jest stary (sygnatura czasowa transakcji).

  • Co najważniejsze , z modelem MVCC PostgreSQL nowa wersja wiersza jest pisana dla każdej UPDATE , bez względu na to, czy zmieniły się dane wiersza. Pociąga to za sobą karę wydajności dla samego UPSERT, rozrost tabeli, rozrost indeksu, karę za wydajność dla kolejnych operacji na tabeli, VACUUM koszt. Niewielki efekt w przypadku kilku duplikatów, ale potężny głównie dla naiwniaków.

Plus , czasami nie jest praktyczne lub nawet możliwe użycie ON CONFLICT DO UPDATE . Instrukcja:

Dla ON CONFLICT DO UPDATE , conflict_target musi być dostarczony.

singiel „Cel konfliktu” nie jest możliwy, jeśli zaangażowanych jest wiele indeksów/ograniczeń. Ale tutaj jest powiązane rozwiązanie dla wielu indeksów częściowych:

  • UPSERT oparty na ograniczeniu UNIQUE z wartościami NULL

Wracając do tematu, 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”), aby złapać wszystkie możliwe konflikty, które mogą się pojawić - co może być pożądane lub nie.

Bez równoczesnego ł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

source kolumna jest opcjonalnym dodatkiem, aby pokazać, jak to działa. Możesz go faktycznie potrzebować, aby odróżnić oba przypadki (kolejna przewaga nad pustymi zapisami).

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

Ponieważ VALUES wyraż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 rzutowań typu. Instrukcja:

Kiedy VALUES jest używany w INSERT , wszystkie wartości są automatycznie przypisywane 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ą cytowanymi stałymi literałami, przekonwertowanie pierwszego jest wystarczające do określenia zakładanego typu dla wszystkich.

Samo zapytanie (nie licząc skutków ubocznych) może być nieco droższe dla niewielu duplikaty, ze względu na obciążenie CTE i dodatkowy SELECT (co powinno być tanie, ponieważ z definicji istnieje idealny indeks - unikalne ograniczenie jest implementowane z indeksem).

Może być (znacznie) szybszy dla wielu duplikaty. Efektywny koszt dodatkowych zapisów zależy od wielu czynników.

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

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

Informacje o CTE:

  • Czy zapytania typu SELECT są jedynym typem, który można zagnieżdżać?
  • Duplikuj instrukcje SELECT w podziale relacyjnym

Z równoczesnym ładowaniem zapisu

Zakładając domyślne READ COMMITTED izolacja transakcji. Powiązane:

  • Równoczesne transakcje powodują wyścig z unikalnym ograniczeniem wstawiania

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

Problem ze współbieżnością 1

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

Jeśli druga transakcja kończy się ROLLBACK (lub jakikolwiek błąd, np. automatyczne ROLLBACK ), transakcja może przebiegać normalnie. Drobny możliwy efekt uboczny:przerwy w numerach sekwencyjnych. Ale bez brakujących wierszy.

Jeśli druga transakcja kończy się normalnie (niejawna lub jawna COMMIT ), Twój INSERT wykryje konflikt (UNIQUE indeks / ograniczenie jest bezwzględne) i DO NOTHING , stąd też nie zwracają wiersza. (Również nie można zablokować wiersza, jak pokazano w problemie ze współbieżnością 2 poniżej, ponieważ jest niewidoczny .) SELECT widzi tę samą migawkę od początku zapytania i nie może zwrócić jeszcze niewidocznego wiersza.

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

To może być w porządku, tak 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 jest ona zgodna z liczbą wierszy danych wejściowych. Może wystarczyć w rzadkim przypadku. Chodzi o to, aby rozpocząć nowe zapytanie (może być w tej samej transakcji), które następnie zobaczy nowo zatwierdzone wiersze.

Lub sprawdź brakujące wiersze wyników w to samo zapytanie i zastąp ci z trikiem z brutalną siłą zademonstrowaną w odpowiedzi Alextoniego.

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 tak jak w powyższym zapytaniu, ale dodajemy jeszcze jeden krok z CTE ups , zanim zwrócimy kompletne zestaw wyników. Ten ostatni CTE przez większość czasu nic nie zrobi. Tylko jeśli w zwróconym wyniku znikną wiersze, używamy brutalnej siły.

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

Jeden efekt uboczny:drugi UPSERT zapisuje wiersze w nieprawidłowej kolejności, więc ponownie wprowadza możliwość zakleszczenia (patrz poniżej), jeśli trzy lub więcej transakcje pisane w tych samych wierszach nakładają się. Jeśli jest to problem, potrzebujesz innego rozwiązania - na przykład powtórzenie całego stwierdzenia, jak wspomniano powyżej.

Problem ze współbieżnością 2

Jeśli współbieżne transakcje mogą zapisywać dane w zaangażowanych kolumnach wierszy, których dotyczy problem, i musisz upewnić się, że znalezione wiersze nadal tam są na późniejszym etapie tej samej transakcji, możesz zablokować istniejące wiersze tanio w CTE ins (które w przeciwnym razie zostałyby odblokowane) za pomocą:

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

I dodaj klauzulę blokującą do SELECT jak również, jak FOR UPDATE .

To sprawia, że ​​konkurencyjne 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śnień:

  • Jak uwzględnić wykluczone wiersze w ZWROCIE z WSTAW ... W KONFLIKTACH
  • Czy SELECT lub INSERT w funkcji podatnej na wyścigi?

Zakleszczenia?

Broń się przed zakleszczeniem wstawiając wiersze w spójnej kolejności . Zobacz:

  • Zakleszczenie z wielorzędowymi WSTAWKAMI pomimo KONFLIKTU NIC NIE ROBI

Typy danych i rzuty

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

Jawne rzutowania typu dla pierwszego wiersza danych w samodzielnym VALUES wyrażenie może być niewygodne. Są sposoby na obejście tego. Możesz użyć dowolnej istniejącej relacji (tabela, widok, ...) jako szablon wiersza. Tabela docelowa jest oczywistym wyborem dla przypadku użycia. Dane wejściowe są automatycznie konwertowane do odpowiednich typów, tak jak w VALUES klauzula 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. Zobacz:

  • Rzutowanie typu NULL podczas aktualizowania wielu wierszy

... i nazwiska

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

Przy wstawianiu do wszystkich (wiodących) kolumn tabeli można pominąć nazwy kolumn. Zakładając, że tabela chats w 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
   )
   ...

Na marginesie:nie używaj zastrzeżonych słów, takich jak "user" jako identyfikator. To załadowany footgun. Używaj legalnych identyfikatorów pisanych małymi literami, nie cytowanych. Zamieniłem go na 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. niekompletne informacje z zapytania na pg_views

  2. Jak automatycznie zamykać bezczynne połączenia w PostgreSQL?

  3. java.lang.ClassNotFoundException:org.postgresql.Driver, Android

  4. użyj polecenia nazwa_bazy_danych w PostgreSQL

  5. 3 sposoby na wyświetlenie listy wszystkich funkcji w PostgreSQL