web-dev-qa-db-ger.com

Verwenden Sie mehrere Konflikte in der Klausel ON CONFLICT

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

Ich muss beim Einfügen in diese Tabelle die ON CONFLICT-Syntax verwenden und andere Spalten aktualisieren, aber ich kann nicht beide Spalten in conflict_targetclause verwenden. 

Es klappt:

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

Aber wie man das für mehrere Kolonnen macht, etwa so:

...
ON CONFLICT ( col1, col2 )
DO UPDATE 
SET 
....
48
OTAR

Eine Probentabelle 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 wird reproduziert

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

conflict_target kann eine eindeutige Indexinferenz durchführen. Beim Auftreten Zusammenfassend besteht sie aus einer oder mehreren Spalten index_column_name und/oder Ausdrucksausdrücke für index_expression und ein optionales index_predicate. Alles Tabellenname eindeutige Indizes, die unabhängig von der Reihenfolge .__ enthalten. Es werden genau die von Konfliktziel angegebenen Spalten/Ausdrücke abgeleitet (gewählt) als Arbiter-Indizes. Wenn ein index_predicate angegeben ist, ist es Als weitere Voraussetzung für die Inferenz müssen Arbiter-Indizes erfüllt werden.

Dies vermittelt den Eindruck, dass die folgende Abfrage funktionieren sollte. Dies ist jedoch nicht der Fall, da tatsächlich ein zusammen 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 auf diese Weise, weil das, was passieren soll, wenn ein Konflikt in der zweiten Spalte auftritt, nicht genau definiert ist. Es gibt viele Möglichkeiten. Sollte in der obigen Q1-Abfrage beispielsweise postgresql col1 aktualisiert werden, wenn ein Konflikt mit col2 vorliegt? Was aber, wenn dies zu einem weiteren Konflikt bei col1 führt? Wie soll Postgresql damit umgehen?

Eine Lösung

Eine Lösung ist, 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üssen die Logik dieser gespeicherten Funktion so anpassen, dass die Spalten genau so aktualisiert werden, wie Sie es möchten. Rufe es gerne an

SELECT merge_db(3,2,'c');
SELECT merge_db(1,2,'d');
28
e4c5

ON CONFLICT erfordert 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

* Neben eindeutigen Indizes können Sie auch Ausschlussbedingungen verwenden. Dies sind etwas allgemeiner als eindeutige Einschränkungen. Angenommen, Ihre Tabelle hatte Spalten für id und valid_time (und valid_time ist eine tsrange), und Sie wollten doppelte ids zulassen, nicht jedoch für sich überschneidende Zeiträume. Eine eindeutige Einschränkung ist nicht hilfreich, aber mit einer Ausschlussbeschränkung können Sie sagen: "Neue Datensätze ausschließen, wenn ihre id einer alten id entspricht und deren valid_time ihren valid_time überlappt."

40

In heutiger Zeit ist (scheint) unmöglich. Weder die letzte Version von ON CONFLICTsyntax erlaubt eine Wiederholung der Klausel noch mit CTE ist möglich: Es ist nicht möglich, das INSERT von ON CONFLICT zu verletzen, um weitere Konfliktziele hinzuzufügen.

4
Peter Krauss
  1. Erstellen Sie eine Einschränkung (z. B. einen Fremdindex).

ODER UND

  1. Sehen Sie sich die vorhandenen Einschränkungen an (\ d in psq).
  2. Verwenden Sie ON CONSTRAINT (constraint_name) in der INSERT-Klausel.

Vlad hatte die richtige Idee.

Zuerst müssen Sie eine tabellenspezifische Einschränkung für die Spalten col1, col2 erstellen. Danach 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
1
Jubair

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

Beispiel aus Was ist neu 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;
1
Martin Gerhardy

Irgendwie hackig, aber ich habe dieses Problem gelöst, indem ich die beiden Werte von col1 und col2 in einer neuen Spalte, col3 (Art wie ein Index der beiden) verkettete, und es mit dem Vergleich verglich. Dies funktioniert nur, wenn Sie BEIDE col1 und col2 benötigen.

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

Dabei ist col3 die Verkettung der Werte aus col1 und col2.

0
Niko Dunk

Sie können normalerweise (ich würde denken) eine Anweisung mit nur einem on conflict generieren, der die einzige Einschränkung angibt, die für das von Ihnen eingefügte Objekt relevant ist.

Normalerweise ist immer nur eine Einschränkung die "relevante" Einschränkung. (Wenn viele, dann frage ich mich, ob etwas komisch/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 conflict-Klausel wird abhängig von dem, was ich versuche, dynamisch generiert. Wenn ich eine Benachrichtigungseinstellung für eine Seite einsetze, kann es einen eindeutigen Konflikt in der site_id, people_id, page_id-Einschränkung geben. Und wenn ich Benachrichtigungspräferenzen für eine Kategorie konfiguriere, weiß ich stattdessen, dass die Einschränkung, die verletzt werden kann, site_id, people_id, category_id ist.

Ich kann also, und wahrscheinlich auch Sie, in Ihrem Fall? Die korrekte on conflict (... columns ) generieren, weil ich weiß, was ich will tun soll, und dann weiß ich, welche der vielen einzigartigen Einschränkungen die einzige ist kann verletzt werden.

0
KajMagnus

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

0
user2625834