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 id
kolumny, jeśli nie ma konfliktów, lub zwrócić istniejące id
kolumny kolumn będących w konflikcie.
Czy można to zrobić? Jeśli tak, to jak?
ON CONFLICT UPDATE
aby nastąpiła zmiana w wierszu. WtedyRETURNING
go złapie.Odpowiedzi:
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:
To zapytanie zwróci wszystkie wiersze, niezależnie od tego, czy zostały właśnie wstawione, czy istniały wcześniej.
źródło
DO NOTHING
aspektowi pierwotnego pytania - wydaje mi się, że aktualizuje pole bez konfliktu (tutaj „nazwa”) dla wszystkich wierszy.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,VACUUM
kosztem. 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: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
source
Kolumna 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 chats
dział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ż
VALUES
wyrażenie jest wolnostojące (nie jest bezpośrednio dołączone doINSERT
), Postgres nie może wyprowadzać typów danych z kolumn docelowych i może być konieczne dodanie jawnych rzutów typów. Instrukcja: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 COMMITTED
izolację 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. AutomatycznieROLLBACK
), 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
),INSERT
wykryjesz konflikt (UNIQUE
indeks / ograniczenie jest bezwzględne), aDO NOTHING
zatem 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 ).SELECT
Widzi 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 .
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: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
VALUES
wyraż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 wVALUES
klauzuliINSERT
: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
chats
w przykładzie składa się tylko z 3 kolumn użytych w UPSERT: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 gousr
.źródło
ON CONFLICT SELECT...
gdzie jest coś :)Upsert, będąc rozszerzeniem
INSERT
zapytania, można zdefiniować za pomocą dwóch różnych zachowań w przypadku konfliktu ograniczeń:DO NOTHING
lubDO UPDATE
.Zauważ również, że
RETURNING
nic nie zwraca, ponieważ nie zostały wstawione krotki . Teraz zDO UPDATE
moż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.źródło
W przypadku wstawiania pojedynczego przedmiotu prawdopodobnie użyłbym koalesce podczas zwracania identyfikatora:
źródło
Głównym celem używania
ON CONFLICT DO NOTHING
jest uniknięcie zgłaszania błędu, ale nie spowoduje to powrotu wiersza. Potrzebujemy więc innego,SELECT
aby uzyskać istniejący identyfikator.W tym SQL, jeśli zakończy się niepowodzeniem w przypadku konfliktów, nic nie zwróci, a drugi
SELECT
otrzyma istniejący wiersz; jeśli wstawi się pomyślnie, będą dwa takie same rekordy, a następnie musimyUNION
scalić wynik.źródło
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:
Zakłada się, że tabela
chats
ma unikalne ograniczenie dotyczące kolumn(usr, contact)
.Aktualizacja: dodano sugerowane poprawki ze Spatara (poniżej). Dzięki!
źródło
CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_exists
prostu pisaćr.id IS NOT NULL as row_exists
. Zamiast poWHERE row_exists=FALSE
prostu pisaćWHERE NOT row_exists
.