Użyj wielu argumentów_konfliktowych w klauzuli ON CONFLICT

101

Mam dwie kolumny w tabeli col1, col2obie są niepowtarzalnie indeksowane (col1 jest unikalny, podobnie jak col2).

Muszę wstawić do tej tabeli, użyć ON CONFLICTskładni i zaktualizować inne kolumny, ale nie mogę użyć obu kolumn w conflict_targetklauzuli.

To działa:

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

Ale jak to zrobić dla kilku kolumn, coś takiego:

...
ON CONFLICT ( col1, col2 )
DO UPDATE 
SET 
....
Oto Shavadze
źródło
4
„col1, col2, oba są niepowtarzalnie indeksowane”. czy to oznacza, że ​​col1 jest unikalny, a col2 jest unikalny, czy też kombinacje col1 i col2 są unikalne?
e4c5
1
czy to oznacza, że ​​col1 jest wyjątkowy, a col2 jest wyjątkowy, indywidualnie
Oto Shavadze

Odpowiedzi:

49

Przykładowa tabela i dane

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

Odtwarzanie problemu

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

Nazwijmy to Q1. Wynik to

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

Co mówi dokumentacja

konflikt_target może wykonywać unikalne wnioskowanie o indeksie. Podczas wykonywania wnioskowania składa się z co najmniej jednej kolumny nazwa_kolumny_indeksu i / lub wyrażenia wyrażenia_indeksu oraz opcjonalnego przypisania_indeksu. Wszystkie unikalne indeksy nazwa_tabeli, które, bez względu na kolejność, zawierają dokładnie kolumny / wyrażenia określone w celu_konfliktu, są wywnioskowane (wybrane) jako indeksy arbitrów. Jeśli określono index_predicate, to jako kolejny wymóg wnioskowania musi spełniać indeksy arbitrów.

Daje to wrażenie, że poniższe zapytanie powinno zadziałać, ale tak nie jest, ponieważ w rzeczywistości wymagałoby to wspólnego, unikalnego indeksu dla kol1 i kol2. Jednak taki indeks nie gwarantowałby, że col1 i col2 będą niepowtarzalne indywidualnie, co jest jednym z wymagań PO.

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

Nazwijmy to zapytanie Q2 (kończy się to błędem składniowym)

Czemu?

Postgresql zachowuje się w ten sposób, ponieważ to, co powinno się stać, gdy wystąpi konflikt w drugiej kolumnie, nie jest dobrze zdefiniowane. Możliwości jest wiele. Na przykład w powyższym zapytaniu Q1, czy należy aktualizować postgresql, col1gdy występuje konflikt col2? Ale co, jeśli to prowadzi do kolejnego konfliktu col1? jak postgresql ma sobie z tym poradzić?

Rozwiązanie

Rozwiązaniem jest połączenie ON CONFLICT ze staromodnym UPSERTem .

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;

Musisz zmodyfikować logikę tej zapisanej funkcji, aby aktualizowała kolumny dokładnie tak, jak chcesz. Wywołaj to jak

SELECT merge_db(3,2,'c');
SELECT merge_db(1,2,'d');
e4c5
źródło
3
W ten sposób działa, ale trochę więcej pracy / logiki niż jest to konieczne, wszystko, co naprawdę musisz zrobić, to stworzyć unikalne ograniczenie na dwóch kolumnach. Zobacz moją odpowiedź poniżej.
Jubair
czy mogę użyć rozwiązania merge_db również wtedy, gdy wstawiam wiele zestawów WARTOŚCI na raz?
daniyel,
@daniyel będziesz musiał przepisać zapisaną funkcję
e4c5
3
Nie jest dla mnie jasne, w jaki sposób warto zasugerować użycie staromodnego upsert - to pytanie jest dobrze przywoływane w przypadku „postgres upsert 9.5” i mogłoby być lepiej, wyjaśniając, jak go używać ze wszystkimi opcjami constraint_names.
Pak
3
@Pak Nie jest to dla Ciebie jasne, ponieważ nie przeczytałeś dokładnie pytania. Operator nie szuka klucza złożonego w tych polach. Druga odpowiedź działa w przypadku kluczy złożonych
e4c5,
66

ON CONFLICTwymaga unikalnego indeksu * do wykrywania konfliktów. Musisz więc tylko utworzyć unikalny indeks dla obu kolumn:

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

* Oprócz unikalnych indeksów można również użyć ograniczeń wykluczających . Są to nieco bardziej ogólne niż unikalne ograniczenia. Załóżmy, że w Twojej tabeli są kolumny idi valid_time(i valid_timejest to a tsrange) i chcesz zezwolić na zduplikowane wartości id, ale nie na nakładające się okresy. Unikalne ograniczenie ci nie pomoże, ale z ograniczeniem wykluczającym możesz powiedzieć „wyklucz nowe rekordy, jeśli są idrówne stare, ida także valid_timepokrywają się z nimi valid_time”.

Paul A Jungwirth
źródło
4
To, co to tworzy, to wspólny unikalny indeks stwórz unikalny indeks idx_t_id_a on t (id, a); Oczywiście OP nie określa jasno, czy te dwie kolumny są niepowtarzalne indywidualnie, czy razem.
e4c5
Dlaczego postgres czasami mówi, że nie ma kolumny nazwanej na podstawie indeksu i nie można jej użyć ON CONFLICT?
Pak
@Pak Wygląda na to, że powinieneś napisać własne pytanie z konkretnym poleceniem, którego używasz, i otrzymanym komunikatem o błędzie.
Paul A Jungwirth
@PaulAJungwirth Nie wiem, twoja odpowiedź jest trafna - unikalny indeks jako ograniczenie dla on conflictpolecenia. Błąd to po prostu „kolumna my_index_name nie istnieje”.
Pak
Mimo to próbowałem tego z oddzielnym unikalnym ograniczeniem dla każdej kolumny, o co prosił OP, i to nie zadziałało. Nie żeby się tego spodziewałem, ale miałem nadzieję.
sudo
6

W dzisiejszych czasach jest (wydaje się) niemożliwe. Ani ostatnia wersja ON CONFLICT składni nie pozwala na powtórzenie klauzuli, ani z CTE nie jest możliwe: nie jest możliwe złamanie INSERT z ON CONFLICT, aby dodać więcej celów konfliktu.

Peter Krauss
źródło
4

Jeśli używasz postgres 9.5, możesz użyć WYŁĄCZONEJ przestrzeni.

Przykład zaczerpnięty z Co nowego w 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
źródło
2
  1. Utwórz ograniczenie (na przykład indeks obcy).

ALBO I

  1. Spójrz na istniejące ograniczenia (\ d w psq).
  2. Użyj ON CONSTRAINT (constraint_name) w klauzuli INSERT.
Vladimir Voznesensky
źródło
2

Vlad wpadł na dobry pomysł.

Najpierw musisz utworzyć unikalne ograniczenie tabeli na kolumnach.Po col1, col2 wykonaniu tej czynności możesz wykonać następujące czynności:

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT ON CONSTRAINT dupes_pkey 
DO UPDATE SET col3 = 'c', col2 = 2
Jubair
źródło
5
Przepraszamy, ale źle zrozumiałeś pytanie. OP nie chce wspólnego, unikalnego ograniczenia.
e4c5
1

Trochę hacky, ale rozwiązałem to, łącząc dwie wartości z col1 i col2 w nową kolumnę, col3 (coś w rodzaju indeksu dwóch) i porównując z tym. Działa to tylko wtedy, gdy potrzebujesz, aby pasowało do obu kol1 i kol2.

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

Gdzie col3 = konkatenacja wartości z col1 i col2.

Niko Dunk
źródło
4
możesz utworzyć unikalny indeks dla tych dwóch kolumn i wprowadzić to ograniczenie on conflict.
Kishore Relangi
0

Zwykle (tak mi się wydaje) można wygenerować oświadczenie tylko z jednym on conflictokreślającym jedyne ograniczenie, które ma znaczenie dla wstawianej rzeczy.

Ponieważ zazwyczaj tylko jedno ograniczenie jest „istotne” w danym momencie. (Jeśli jest ich wiele, to zastanawiam się, czy coś jest dziwne / dziwnie zaprojektowane, hmm.)

Przykład:
(Licencja: nie CC0, tylko 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)

I:

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

on conflictKlauzula jest generowana dynamicznie, w zależności od tego, co próbuję zrobić. Jeśli wstawiam preferencje dotyczące powiadomień dla strony - może wystąpić wyjątkowy konflikt dotyczący site_id, people_id, page_idograniczenia. A jeśli konfiguruję ustawienia powiadomień dla kategorii - zamiast tego wiem, że ograniczenie, które może zostać naruszone, tosite_id, people_id, category_id .

Więc mogę, i całkiem prawdopodobne, że Ty też, w twoim przypadku ?, wygenerować poprawną on conflict (... columns ), ponieważ wiem, co chcę zrobić, a potem wiem, które z wielu unikalnych ograniczeń jest tym, które może zostać naruszone.

KajMagnus
źródło
-4

ON CONFLICT to bardzo niezdarne rozwiązanie, uciekaj

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

działa na Oracle, Postgres i wszystkich innych bazach danych

user2625834
źródło
Nie jest atomowy, więc może zawieść i dać błędne wyniki w przypadku wielu połączeń w tym samym czasie.
Bogdan Mart