Verwenden Sie mehrere Konfliktziele in der ON CONFLICT-Klausel

91

Ich habe zwei Spalten in der Tabelle col1, col2beide sind eindeutig indiziert (col1 ist eindeutig und col2 auch).

Ich muss beim Einfügen in diese Tabelle die ON CONFLICTSyntax verwenden und andere Spalten aktualisieren, aber ich kann nicht beide Spalten in conflict_targetKlausel verwenden.

Es klappt:

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

Aber wie geht das für mehrere Spalten?

...
ON CONFLICT ( col1, col2 )
DO UPDATE 
SET 
....
Oto Shavadze
quelle
4
"col1, col2, beide sind eindeutig indiziert." Bedeutet das, dass col1 einzigartig und col2 einzigartig ist oder sind Kombinationen von col1, col2 einzigartig?
e4c5
1
Bedeutet das, dass col1 einzigartig und col2 einzigartig ist
Oto Shavadze

Antworten:

43

Eine Beispieltabelle und Daten

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

Das Problem reproduzieren

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

Nennen wir das Q1. Das Ergebnis ist

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

Was die Dokumentation sagt

konfliktziel kann eine eindeutige Indexinferenz durchführen. Wenn eine Inferenz durchgeführt wird, besteht sie aus einer oder mehreren Spalten mit dem Indexspaltennamen und / oder den Ausdrücken mit dem Indexausdruck sowie einem optionalen Indexpredikat. Alle eindeutigen Indizes für Tabellennamen, die unabhängig von der Reihenfolge genau die von Konfliktzielen angegebenen Spalten / Ausdrücke enthalten, werden als Arbiter-Indizes abgeleitet (ausgewählt). Wenn ein index_predicate angegeben wird, muss es als weitere Voraussetzung für die Inferenz die Arbiter-Indizes erfüllen.

Dies erweckt den Eindruck, dass die folgende Abfrage funktionieren sollte, jedoch nicht, da tatsächlich ein eindeutiger Index für col1 und col2 erforderlich wäre. Ein solcher Index würde jedoch nicht garantieren, dass col1 und col2 einzeln eindeutig sind, was eine der Anforderungen des OP ist.

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

Nennen wir diese Abfrage Q2 (dies schlägt mit einem Syntaxfehler fehl).

Warum?

Postgresql verhält sich so, weil nicht genau definiert ist, was passieren soll, wenn ein Konflikt in der zweiten Spalte auftritt. Es gibt verschiedene Möglichkeiten. Sollte postgresql beispielsweise in der obigen Q1-Abfrage aktualisiert werden, col1wenn ein Konflikt besteht col2? Aber was ist, wenn das zu einem weiteren Konflikt führt col1? Wie soll postgresql damit umgehen?

Eine Lösung

Eine Lösung besteht darin, ON CONFLICT mit altmodischem UPSERT zu kombinieren .

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;

Sie müssten die Logik dieser gespeicherten Funktion so ändern, dass die Spalten genau so aktualisiert werden, wie Sie es möchten. Rufe es wie auf

SELECT merge_db(3,2,'c');
SELECT merge_db(1,2,'d');
e4c5
quelle
3
Dies funktioniert so, aber ein bisschen mehr Arbeit / Logik als nötig. Alles, was Sie wirklich tun müssen, ist, eine eindeutige Einschränkung für die beiden Spalten zu erstellen. Siehe meine Antwort unten.
Jubair
Kann ich die Lösung merge_db auch verwenden, wenn ich mehrere Sätze von WERTEN gleichzeitig einfüge?
Daniyel
@daniyel müssen Sie die gespeicherte Funktion neu schreiben
e4c5
3
Es ist mir unklar, wie nützlich es ist, die Verwendung des altmodischen Upsert vorzuschlagen - diese Frage ist für "postgres upsert 9.5" gut referenziert, und es könnte besser sein, zu erklären, wie man sie mit allen Optionen für Constraint_Names verwendet.
Pak
2
@Pak Es ist dir unklar, weil du die Frage nicht klar gelesen hast. Die Operation sucht in diesen Feldern nicht nach einem zusammengesetzten Schlüssel. Die andere Antwort funktioniert für zusammengesetzte Schlüssel
e4c5
63

ON CONFLICTerfordert einen eindeutigen Index * für die Konflikterkennung. Sie müssen also nur einen eindeutigen Index für beide Spalten erstellen:

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

* Zusätzlich zu eindeutigen Indizes können Sie auch Ausschlussbeschränkungen verwenden . Diese sind etwas allgemeiner als eindeutige Einschränkungen. Angenommen, Ihre Tabelle enthält Spalten für idund valid_time(und valid_timeist a tsrange), und Sie möchten doppelte ids zulassen , jedoch nicht für überlappende Zeiträume. Eine eindeutige Einschränkung hilft Ihnen nicht weiter, aber mit einer Ausschlussbeschränkung können Sie sagen: "Neue Datensätze ausschließen, wenn sie ideinem alten entsprechen idund sich auch valid_timeüberlappen valid_time."

Paul A Jungwirth
quelle
4
Dadurch wird ein zusammen eindeutiger Index erstellt. Erstellen Sie einen eindeutigen Index idx_t_id_a für t (id, a). Natürlich gibt das OP nicht klar an, ob die beiden Spalten einzeln oder zusammen eindeutig sind.
e4c5
Warum sagt postgres manchmal, dass keine Spalte nach dem Index benannt ist und nicht verwendet wird ON CONFLICT?
Pak
@Pak Es klingt so, als ob Sie Ihre eigene Frage mit dem von Ihnen verwendeten Befehl und der Fehlermeldung, die Sie erhalten, schreiben sollten.
Paul A Jungwirth
@PaulAJungwirth Ich weiß nicht, Ihre Antwort ist genau richtig - ein eindeutiger Index als Einschränkung für den on conflictBefehl. Der Fehler ist nur "Spalte my_index_name existiert nicht".
Pak
Ich habe es trotzdem mit einer separaten eindeutigen Einschränkung für jede Spalte versucht, als das OP gefragt hat, und es hat nicht funktioniert. Nicht, dass ich es erwartet hätte, aber ich hatte gehofft.
Sudo
5

In der heutigen Zeit ist (scheint) unmöglich. Weder die letzte Version der ON CONFLICT Syntax erlaubt es, die Klausel zu wiederholen, noch ist mit CTE möglich: Es ist nicht möglich, das INSERT von ON CONFLICT zu durchbrechen, um weitere Konfliktziele hinzuzufügen.

Peter Krauss
quelle
3

Wenn Sie Postgres 9.5 verwenden, können Sie den EXCLUDED-Bereich verwenden.

Beispiel aus den Neuerungen in 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
quelle
2
  1. Erstellen Sie eine Einschränkung (z. B. Fremdindex).

ODER UND

  1. Sehen Sie sich vorhandene Einschränkungen an (\ d in psq).
  2. Verwenden Sie ON CONSTRAINT (Einschränkungsname) in der INSERT-Klausel.
Vladimir Voznesensky
quelle
1

Ein bisschen hacky, aber ich habe das gelöst, indem ich die beiden Werte von col1 und col2 zu einer neuen Spalte, col3 (ähnlich einem Index der beiden), verkettet und damit verglichen habe. Dies funktioniert nur, wenn Sie es benötigen, um sowohl mit col1 als auch mit col2 übereinzustimmen.

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

Wobei col3 = die Verkettung der Werte von col1 und col2 ist.

Niko Dunk
quelle
3
Sie können einen eindeutigen Index für diese beiden Spalten erstellen und diese Einschränkung angeben on conflict.
Kishore Relangi
0

Vlad hatte die richtige Idee.

Zuerst müssen Sie eine eindeutige Tabelleneinschränkung für die Spalten erstellen. col1, col2 Anschließend können Sie Folgendes tun:

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT ON CONSTRAINT dupes_pkey 
DO UPDATE SET col3 = 'c', col2 = 2
Jubair
quelle
3
Entschuldigung, aber Sie haben die Frage falsch verstanden. Das OP möchte keine zusammen eindeutige Einschränkung.
e4c5
0

Sie können normalerweise (ich würde denken) eine Anweisung mit nur einer generieren on conflict, die die einzige Einschränkung angibt, die für das Objekt, das Sie einfügen, relevant ist.

Denn normalerweise ist jeweils nur eine Einschränkung die "relevante". (Wenn viele, dann frage ich mich, ob etwas seltsam / seltsam gestaltet ist, hmm.)

Beispiel:
(Lizenz: Nicht CC0, nur 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)

Und:

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

Die on conflictKlausel wird dynamisch generiert, je nachdem, was ich versuche. Wenn ich eine Benachrichtigungseinstellung für eine Seite einfüge, kann es zu einem eindeutigen Konflikt mit der site_id, people_id, page_idEinschränkung kommen. Und wenn ich Benachrichtigungseinstellungen für eine Kategorie konfiguriere, weiß ich stattdessen, dass die Einschränkung, die verletzt werden kann, ist site_id, people_id, category_id.

Also ich kann, und ziemlich wahrscheinlich auch Sie, in Ihrem Fall ?, erzeugen die richtige on conflict (... columns ), weil ich weiß , was ich will tun, und dann weiß ich einzelne der vielen einzigartigen Zwänge , die, ist derjenige, der verletzt bekommen.

KajMagnus
quelle
-1

ON CONFLICT ist eine sehr ungeschickte Lösung

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

funktioniert mit Oracle, Postgres und allen anderen Datenbanken

user2625834
quelle
Es ist nicht atomar, daher kann es bei mehreren Verbindungen gleichzeitig fehlschlagen und zu falschen Ergebnissen führen.
Bogdan Mart