PostgreSQL-Werte für mehrspaltige eindeutige Bedingungen und NULL

94

Ich habe eine Tabelle wie die folgende:

create table my_table (
    id   int8 not null,
    id_A int8 not null,
    id_B int8 not null,
    id_C int8 null,
    constraint pk_my_table primary key (id),
    constraint u_constrainte unique (id_A, id_B, id_C)
);

Und ich möchte (id_A, id_B, id_C)in jeder Situation anders sein. Die folgenden zwei Einfügungen müssen also zu einem Fehler führen:

INSERT INTO my_table VALUES (1, 1, 2, NULL);
INSERT INTO my_table VALUES (2, 1, 2, NULL);

Es verhält sich jedoch nicht wie erwartet, da laut Dokumentation zwei NULLWerte nicht miteinander verglichen werden, sodass beide Einfügungen fehlerfrei verlaufen.

Wie kann ich meine eindeutige Einschränkung garantieren, auch wenn id_Cdies NULLin diesem Fall der Fall sein kann? Eigentlich lautet die eigentliche Frage: Kann ich diese Art von Einzigartigkeit in "pure sql" garantieren oder muss ich sie auf einer höheren Ebene implementieren (in meinem Fall Java)?

Manuel Leduc
quelle
Angenommen, Sie haben Werte (1,2,1)und (1,2,2)in den (A,B,C)Spalten. Soll ein (1,2,NULL)hinzugefügt werden dürfen oder nicht?
ypercubeᵀᴹ
A und B können nicht null sein, aber C kann null oder ein beliebiger positiver ganzzahliger Wert sein. Also (1,2,3) und (2,4, null) sind gültig, aber (null, 2,3) oder (1, null, 4) sind ungültig. Und [(1,2, null), (1,2,3)] unterbricht keine eindeutige Einschränkung, sondern [(1,2, null), (1,2, null)] muss sie unterbrechen.
Manuel Leduc
2
Gibt es irgendwelche Werte, die niemals in diesen Spalten erscheinen werden (wie negative Werte?)
a_horse_with_no_name
Sie müssen Ihre Einschränkungen nicht in pg bezeichnen. Es wird automatisch einen Namen generieren. Nur zu deiner Information.
Evan Carroll

Antworten:

94

Sie können das in reinem SQL tun . Erstellen Sie einen partiellen eindeutigen Index zusätzlich zu dem, den Sie haben:

CREATE UNIQUE INDEX ab_c_null_idx ON my_table (id_A, id_B) WHERE id_C IS NULL;

So können Sie (a, b, c)in Ihre Tabelle eingeben :

(1, 2, 1)
(1, 2, 2)
(1, 2, NULL)

Aber keines davon ein zweites Mal.

Oder verwenden Sie zwei TeilindizesUNIQUE und keinen vollständigen Index (oder Einschränkung). Die beste Lösung hängt von den Details Ihrer Anforderungen ab. Vergleichen Sie:

Dies ist zwar elegant und effizient für eine einzelne nullfähige Spalte im UNIQUEIndex, gerät jedoch schnell außer Kontrolle . Diskutieren Sie dies - und wie Sie UPSERT mit Teilindizes verwenden:

Nebenbei

Keine Verwendung für Bezeichner mit gemischten Groß- und Kleinschreibung ohne Anführungszeichen in PostgreSQL.

Sie können eine serialSpalte als Primärschlüssel oder eine IDENTITYSpalte in Postgres 10 oder höher betrachten. Verbunden:

Damit:

CREATE TABLE my_table (
   my_table_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY  -- for pg 10+
-- my_table_id bigserial PRIMARY KEY  -- for pg 9.6 or older
 , id_a int8 NOT NULL
 , id_b int8 NOT NULL
 , id_c int8
 , CONSTRAINT u_constraint UNIQUE (id_a, id_b, id_c)
);

Wenn Sie nicht mehr als 2 Milliarden Zeilen (> 2147483647) über die Lebensdauer Ihrer Tabelle (einschließlich Abfall und gelöschter Zeilen) erwarten, sollten Sie integer(4 Bytes) anstelle von bigint(8 Bytes) in Betracht ziehen .

Erwin Brandstetter
quelle
1
In den Dokumenten wird diese Methode empfohlen. Durch Hinzufügen einer eindeutigen Einschränkung wird automatisch ein eindeutiger B-Tree-Index für die in der Einschränkung aufgelisteten Spalten oder Spaltengruppen erstellt. Eine Eindeutigkeitsbeschränkung, die nur einige Zeilen abdeckt, kann nicht als eindeutige Einschränkung geschrieben werden. Eine solche Einschränkung kann jedoch durch die Erstellung eines eindeutigen Teilindex erzwungen werden.
Evan Carroll
12

Ich hatte das gleiche Problem und fand einen anderen Weg, eindeutige NULL in die Tabelle aufzunehmen.

CREATE UNIQUE INDEX index_name ON table_name( COALESCE( foreign_key_field, -1) )

In meinem Fall ist das Feld foreign_key_fieldeine positive ganze Zahl und wird niemals -1 sein.

Also, um Manual Leduc zu beantworten, könnte eine andere Lösung sein

CREATE UNIQUE INDEX  u_constrainte (COALESCE(id_a, -1), COALESCE(id_b,-1),COALESCE(id_c, -1) )

Ich gehe davon aus, dass die IDs nicht -1 sein werden.

Was ist der Vorteil beim Erstellen eines Teilindex?
Für den Fall, Sie haben nicht die NOT NULL - Klausel id_a, id_bund id_ckönnen zusammen nur einmal NULL sein.
Bei einem Teilindex können die 3 Felder mehr als einmal NULL sein.

Luc M
quelle
3
> Was ist der Vorteil beim Erstellen eines Teilindex? Die Art und Weise, wie Sie es gemacht haben, COALESCEkann die Duplikate effektiv einschränken, aber der Index wäre beim Abfragen nicht sehr nützlich, da er ein Ausdrucksindex ist, der wahrscheinlich nicht mit Abfrageausdrücken übereinstimmt. Das heißt, es sei denn, SELECT COALESCE(col, -1) ...Sie würden den Index nicht erreichen.
Bo Jeanes
@BoJeanes Der Index wurde nicht für ein Leistungsproblem erstellt. Es wurde erstellt, um die geschäftlichen Anforderungen zu erfüllen.
Luc M
8

Eine Null kann bedeuten, dass der Wert für diese Zeile im Moment nicht bekannt ist, aber, falls bekannt, in Zukunft hinzugefügt wird (Beispiel FinishDatefür einen Lauf Project) oder dass für diese Zeile kein Wert angewendet werden kann (Beispiel EscapeVelocityfür ein Schwarzes Loch Star).

Meiner Meinung nach ist es normalerweise besser, die Tabellen zu normalisieren, indem alle Nullen entfernt werden.

In Ihrem Fall möchten Sie NULLsin Ihrer Spalte zulassen , möchten jedoch nur eine NULLzulassen. Warum? Welche Art von Beziehung besteht zwischen den beiden Tabellen?

Vielleicht können Sie einfach die Spalte ändern NOT NULLund stattdessen NULLeinen speziellen Wert (wie -1) speichern, von dem bekannt ist , dass er niemals erscheint. Dies löst das Problem der Eindeutigkeitseinschränkung (kann jedoch andere möglicherweise unerwünschte Nebenwirkungen haben. Wenn Sie beispielsweise -1"nicht bekannt / trifft nicht zu" verwenden, werden Summen- oder Durchschnittsberechnungen für die Spalte verzerrt. Andernfalls müssen alle derartigen Berechnungen ausgeführt werden berücksichtigen Sie den speziellen Wert und ignorieren Sie ihn.)

ypercubeᵀᴹ
quelle
2
In meinem Fall ist NULL wirklich NULL (id_C ist ein Fremdschlüssel für table_c, also kann er nicht den Wert -1 haben), das bedeutet, dass es keine Beziehung zwischen "my_table" und "table_c" gibt. Es hat also eine funktionale Bedeutung. Übrigens [(1, 1,1, null), (2, 1,2, null), (3,2,4, null)] ist eine gültige Liste der eingefügten Daten.
Manuel Leduc
1
Es ist nicht wirklich ein Nullwert, wie er in SQL verwendet wird, da Sie nur einen in allen Zeilen benötigen. Sie können Ihr Datenbankschema ändern, indem Sie entweder -1 zu table_c oder eine weitere Tabelle hinzufügen (dies wäre ein Supertyp zu subtype table_c).
ypercubeᵀᴹ
3
Ich möchte @Manuel nur darauf hinweisen, dass die Meinung zu Nullen in dieser Antwort nicht allgemein gültig ist und viel diskutiert wird. Viele, wie ich, denken, dass null für jeden gewünschten Zweck verwendet werden kann (sollte aber nur eine Bedeutung für jedes Feld haben und dokumentiert sein, möglicherweise im Feldnamen oder in einem Spaltenkommentar)
Jack Douglas
1
Sie können keinen Dummy-Wert verwenden, wenn Ihre Spalte ein AUSLÄNDISCHER SCHLÜSSEL ist.
Luc M
1
+1 Ich bin mit Ihnen: Wenn eine Spaltenkombination eindeutig sein soll, müssen Sie eine Entität in Betracht ziehen, in der diese Spaltenkombination eine PK ist. Das Datenbankschema der OPs sollte sich wahrscheinlich in eine übergeordnete und eine untergeordnete Tabelle ändern.
AK