ON CONFLICT DO UPDATE
Verhalten klären
Betrachten Sie das Handbuch hier :
Für jede einzelne Zeile, die zum Einfügen vorgeschlagen wird, wird entweder die Einfügung fortgesetzt, oder wenn eine durch angegebene Arbiter-Einschränkung oder ein durch angegebener Index
conflict_target
verletzt wird, wird die Alternative gewählt conflict_action
.
Meine kühne Betonung. Sie müssen also keine Prädikate für Spalten wiederholen, die im eindeutigen Index in der WHERE
Klausel zu UPDATE
(the conflict_action
) enthalten sind:
INSERT INTO test_upsert AS tu
(name , status, test_field , identifier, count)
VALUES ('shaun', 1 , 'test value', 'ident' , 1)
ON CONFLICT (name, status, test_field) DO UPDATE
SET count = tu.count + 1;
WHERE tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = 'test value'
Die eindeutige Verletzung legt bereits fest, was Ihre hinzugefügte WHERE
Klausel redundant durchsetzen würde.
Teilindex klären
Fügen Sie eine WHERE
Klausel es ein tatsächlicher zu machen Teilindex wie Sie selbst erwähnt (aber mit umgekehrter Logik):
CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL; -- not: "is not null"
Zur Verwendung dieses Teilindex in Ihrem UPSERT benötigen Sie eine passende wie @ypercube demonstriert :conflict_target
ON CONFLICT (name, status) WHERE test_field IS NULL
Nun wird auf den obigen Teilindex geschlossen. Jedoch , wie das Handbuch auch Notizen :
[...] Ein nicht partieller eindeutiger Index (ein eindeutiger Index ohne Prädikat) wird abgeleitet (und daher von diesem verwendet ON CONFLICT
), wenn ein solcher Index verfügbar ist, der alle anderen Kriterien erfüllt.
Wenn Sie einen zusätzlichen (oder nur einen) Index (name, status)
haben, wird dieser (auch) verwendet. Ein Index über (name, status, test_field)
würde ausdrücklich nicht abgeleitet. Dies erklärt Ihr Problem nicht, hat aber möglicherweise die Verwirrung beim Testen vergrößert.
Lösung
AIUI, noch löst keines der oben genannten Probleme Ihr Problem . Mit dem Teilindex würden nur Sonderfälle mit übereinstimmenden NULL-Werten erfasst. Andere doppelte Zeilen werden entweder eingefügt, wenn Sie keine anderen übereinstimmenden eindeutigen Indizes / Einschränkungen haben, oder wenn Sie dies tun, wird eine Ausnahme ausgelöst. Ich nehme an, das ist nicht was du willst. Du schreibst:
Der zusammengesetzte Schlüssel besteht aus 20 Spalten, von denen 10 nullwertfähig sein können.
Was genau halten Sie für ein Duplikat? Postgres (gemäß dem SQL-Standard) betrachtet zwei NULL-Werte nicht als gleich. Das Handbuch:
Im Allgemeinen wird eine eindeutige Einschränkung verletzt, wenn die Tabelle mehr als eine Zeile enthält, in der die Werte aller in der Einschränkung enthaltenen Spalten gleich sind. Zwei Nullwerte werden in diesem Vergleich jedoch niemals als gleich angesehen. Dies bedeutet, dass es auch bei Vorliegen einer eindeutigen Einschränkung möglich ist, doppelte Zeilen, die einen Nullwert enthalten, in mindestens einer der eingeschränkten Spalten zu speichern. Dieses Verhalten entspricht dem SQL-Standard, es wurde jedoch festgestellt, dass andere SQL-Datenbanken dieser Regel möglicherweise nicht folgen. Seien Sie also vorsichtig, wenn Sie Anwendungen entwickeln, die portabel sein sollen.
Verbunden:
IchNULL
gehe davon aus, dass Sie möchten, dassWerte in allen 10 nullbaren Spalten als gleich angesehen werden. Es ist elegant und praktisch, eine einzelne nullable-Spalte mit einem zusätzlichen Teilindex abzudecken, wie hier gezeigt:
Bei Spalten mit mehr Nullwerten ist dies jedoch schnell außer Kontrolle geraten. Sie benötigen einen Teilindex für jede eindeutige Kombination nullfähiger Spalten. Für nur 2 von denen , die für drei Teilindizes sind (a)
, (b)
und (a,b)
. Die Zahl wächst exponentiell mit 2^n - 1
. Um alle möglichen Kombinationen von NULL-Werten für Ihre 10 nullbaren Spalten abzudecken, benötigen Sie bereits 1023 Teilindizes. No Go.
Die einfache Lösung: Ersetzen Sie NULL-Werte und definieren Sie die beteiligten Spalten. NOT NULL
Mit einer einfachen UNIQUE
Einschränkung würde alles gut funktionieren .
Wenn dies keine Option ist, schlage ich einen Ausdrucksindex vor, mit COALESCE
dem NULL im Index ersetzt wird:
CREATE UNIQUE INDEX test_upsert_solution_idx
ON test_upsert (name, status, COALESCE(test_field, ''));
Die leere Zeichenfolge ( ''
) ist ein offensichtlicher Kandidat für Zeichentypen, aber Sie können jeden zulässigen Wert verwenden, der entweder nie erscheint oder mit NULL gemäß Ihrer Definition von "einzigartig" gefaltet werden kann .
Dann benutze diese Anweisung:
INSERT INTO test_upsert as tu(name,status,test_field,identifier, count)
VALUES ('shaun', 1, null , 'ident', 11) -- works with
, ('bob' , 2, 'test value', 'ident', 22) -- and without NULL
ON CONFLICT (name, status, COALESCE(test_field, '')) DO UPDATE -- match expr. index
SET count = COALESCE(tu.count + EXCLUDED.count, EXCLUDED.count, tu.count);
Wie bei @ypercube gehe ich davon aus, dass Sie count
die vorhandene Anzahl tatsächlich erweitern möchten . Da die Spalte NULL sein kann, würde das Hinzufügen von NULL die Spalte NULL setzen. Wenn Sie definieren count NOT NULL
, können Sie vereinfachen.
Eine andere Idee wäre, einfach das conflict_target aus der Anweisung zu entfernen, um alle eindeutigen Verstöße abzudecken . Dann könnten Sie verschiedene eindeutige Indizes definieren, um eine differenziertere Definition dessen zu erhalten, was "eindeutig" sein soll. Aber das fliegt nicht mit ON CONFLICT DO UPDATE
. Das Handbuch noch einmal:
Für ON CONFLICT DO NOTHING
ist es optional, ein conflict_target anzugeben. Wenn dies weggelassen wird, werden Konflikte mit allen verwendbaren Einschränkungen (und eindeutigen Indizes) behandelt. Für ON CONFLICT DO UPDATE
eine conflict_target müssen zur Verfügung gestellt werden.
count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) END
kann vereinfacht werdencount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)