Database
 sql >> Datenbank >  >> RDS >> Database

NULL-Komplexitäten – Teil 4, Fehlende eindeutige Standardbeschränkung

Dieser Artikel ist Teil 4 einer Serie über NULL-Komplexitäten. In den vorherigen Artikeln (Teil 1, Teil 2 und Teil 3) habe ich die Bedeutung von NULL als Markierung für einen fehlenden Wert behandelt, wie sich NULLen in Vergleichen und in anderen Abfrageelementen verhalten, und Standardfunktionen zur NULL-Behandlung, die dies nicht sind noch in T-SQL verfügbar. Diesen Monat behandle ich den Unterschied zwischen der Definition einer eindeutigen Einschränkung im ISO/IEC-SQL-Standard und der Funktionsweise in T-SQL. Ich biete auch kundenspezifische Lösungen an, die Sie implementieren können, wenn Sie die Standardfunktionalität benötigen.

Standard-UNIQUE-Einschränkung

SQL Server behandelt NULLen genauso wie Nicht-NULL-Werte, um eine eindeutige Einschränkung zu erzwingen. Das heißt, eine Eindeutigkeitsbeschränkung für T ist genau dann erfüllt, wenn es keine zwei Zeilen R1 und R2 von T gibt, so dass R1 und R2 dieselbe Kombination von NULL-Werten und Nicht-NULL-Werten in den eindeutigen Spalten haben. Angenommen, Sie definieren eine Unique-Einschränkung für col1, bei der es sich um eine NULL-fähige Spalte eines INT-Datentyps handelt. Ein Versuch, die Tabelle so zu ändern, dass mehr als eine Zeile mit einem NULL-Wert in Spalte 1 entsteht, wird ebenso abgelehnt wie eine Änderung, die zu mehr als einer Zeile mit dem Wert 1 in Spalte 1 führen würde.

Angenommen, Sie definieren eine zusammengesetzte Unique-Einschränkung für die Kombination der NULL-fähigen INT-Spalten col1 und col2. Ein Versuch, die Tabelle so zu ändern, dass eine der folgenden Kombinationen von (col1, col2)-Werten mehr als einmal vorkommt, wird zurückgewiesen:(NULL, NULL), (3, NULL), (NULL, 300 ), (1, 100).

Wie Sie sehen können, behandelt die T-SQL-Implementierung der Unique-Einschränkung NULLen genauso wie Nicht-NULL-Werte, um die Eindeutigkeit zu erzwingen.

Wenn Sie einen Fremdschlüssel für eine Tabelle X definieren möchten, die auf eine Tabelle Y verweist, müssen Sie die Eindeutigkeit für die referenzierte(n) Spalte(n) mit einer der folgenden Optionen erzwingen:

  • Primärschlüssel
  • Eindeutigkeitsbeschränkung
  • Ungefilterter eindeutiger Index

Ein Primärschlüssel ist für NULL-fähige Spalten nicht zulässig. Sowohl eine Unique-Einschränkung (die einen Index unter den Deckblättern erstellt) als auch ein explizit erstellter eindeutiger Index sind für NULL-fähige Spalten zulässig und erzwingen ihre Eindeutigkeit in T-SQL mithilfe der oben genannten Logik. Die referenzierende Tabelle darf Zeilen mit NULL in der referenzierenden Spalte haben, unabhängig davon, ob die referenzierte Tabelle eine Zeile mit NULL in der referenzierten Spalte hat. Die Idee ist, eine optionale Beziehung zu unterstützen. Einige Zeilen in der referenzierenden Tabelle könnten Zeilen sein, die sich nicht auf Zeilen in der referenzierten Tabelle beziehen. Sie implementieren dies, indem Sie eine NULL in der Referenzierungsspalte verwenden.

Um die T-SQL-Implementierung einer Unique-Einschränkung zu demonstrieren, führen Sie den folgenden Code aus, der eine Tabelle namens T3 mit einer Unique-Einschränkung erstellt, die für die NULLable INT-Spalte col1 definiert ist, und sie mit einigen Beispielzeilen füllt:

USE tempdb;
GO
 
DROP TABLE IF EXISTS dbo.T3;
GO
 
CREATE TABLE dbo.T3(col1 INT NULL, col2 INT NULL, CONSTRAINT UNQ_T3 UNIQUE(col1));
 
INSERT INTO dbo.T3(col1, col2) VALUES(1, 100),(2, -1),(NULL, -1),(3, 300);

Verwenden Sie den folgenden Code, um die Tabelle abzufragen:

SELECT * FROM dbo.T3;

Diese Abfrage generiert die folgende Ausgabe:

col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Versuchen Sie, eine zweite Zeile mit NULL in Spalte1 einzufügen:

INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);

Dieser Versuch wird abgelehnt und Sie erhalten die folgende Fehlermeldung:

Nachricht 2627, Level 14, Status 1
Verletzung der UNIQUE KEY-Einschränkung 'UNQ_T3'. Doppelter Schlüssel kann nicht in Objekt „dbo.T3“ eingefügt werden. Der doppelte Schlüsselwert ist ().

Die standardmäßige Unique Constraint-Definition unterscheidet sich ein wenig von der T-SQL-Version. Der Hauptunterschied hat mit der NULL-Behandlung zu tun. Hier ist die eindeutige Beschränkungsdefinition aus dem Standard:

„Eine Eindeutigkeitsbedingung für T ist genau dann erfüllt, wenn es keine zwei Zeilen R1 und R2 von T gibt, sodass R1 und R2 dieselben Nicht-NULL-Werte in den eindeutigen Spalten haben.“

Eine Tabelle T mit einer eindeutigen Einschränkung für Spalte1 lässt also mehrere Zeilen mit einem NULL-Wert in Spalte1 zu, verbietet jedoch mehrere Zeilen mit demselben Nicht-NULL-Wert in Spalte1.

Was etwas schwieriger zu erklären ist, ist, was gemäß dem Standard mit einer zusammengesetzten eindeutigen Beschränkung passiert. Angenommen, Sie haben eine eindeutige Einschränkung für (Spalte1, Spalte2) definiert. Sie können mehrere Zeilen mit (NULL, NULL) haben, aber Sie können nicht mehrere Zeilen mit (3, NULL) haben, genau wie Sie nicht mehrere Zeilen mit (1, 100) haben können. Ebenso können Sie mit (NULL, 300) nicht mehrere Zeilen haben. Der Punkt ist, dass Sie nicht mehrere Zeilen mit denselben Nicht-NULL-Werten in den eindeutigen Spalten haben dürfen. Wie bei einem Fremdschlüssel können Sie eine beliebige Anzahl von Zeilen in der referenzierenden Tabelle mit NULLen in allen referenzierenden Spalten haben, unabhängig davon, was in der referenzierten Tabelle vorhanden ist. Solche Zeilen stehen in keiner Beziehung zu Zeilen in der referenzierten Tabelle (optionale Beziehung). Wenn Sie jedoch einen Nicht-NULL-Wert in einer der referenzierenden Spalten haben, muss es eine Zeile in der referenzierten Tabelle mit denselben Nicht-NULL-Werten in den referenzierten Spalten geben.

Angenommen, Sie haben eine Datenbank auf einer Plattform, die die standardmäßige Unique-Einschränkung unterstützt, und Sie müssen diese Datenbank zu SQL Server migrieren. Es können Probleme mit der Erzwingung von Unique-Einschränkungen in SQL Server auftreten, wenn die Unique-Spalten NULL-Werte unterstützen. Daten, die im Quellsystem als gültig galten, können in SQL Server als ungültig angesehen werden. In den folgenden Abschnitten werde ich eine Reihe möglicher Problemumgehungen in SQL Server untersuchen.

Lösung 1, mit gefiltertem Index oder indizierter Ansicht

Eine gängige Problemumgehung in T-SQL zum Erzwingen der standardmäßigen Unique-Constraint-Funktionalität, wenn nur eine Zielspalte betroffen ist, besteht darin, einen eindeutigen gefilterten Index zu verwenden, der nur die Zeilen filtert, in denen die Zielspalte nicht NULL ist. Der folgende Code löscht die vorhandene eindeutige Einschränkung von T3 und implementiert einen solchen Index:

ALTER TABLE dbo.T3 DROP CONSTRAINT UNQ_T3;
 
CREATE UNIQUE NONCLUSTERED INDEX idx_col1_notnull ON dbo.T3(col1) WHERE col1 IS NOT NULL;

Da der Index nur Zeilen filtert, in denen col1 nicht NULL ist, wird seine UNIQUE-Eigenschaft nur für die col1-Werte erzwungen, die nicht NULL sind.

Denken Sie daran, dass T3 bereits eine Zeile mit NULL in Spalte1 hat. Verwenden Sie zum Testen dieser Lösung den folgenden Code, um eine zweite Zeile mit NULL in Spalte1 hinzuzufügen:

INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);

Dieser Code wird erfolgreich ausgeführt.

Denken Sie daran, dass T3 bereits eine Zeile mit dem Wert 1 in col1 hat. Führen Sie den folgenden Code aus, um zu versuchen, eine zweite Zeile mit 1 in col1 hinzuzufügen:

INSERT INTO dbo.T3(col1, col2) VALUES(1, 500);

Wie erwartet schlägt dieser Versuch mit folgendem Fehler fehl:

Msg 2601, Level 14, State 1
Kann keine doppelte Schlüsselzeile in Objekt „dbo.T3“ mit eindeutigem Index „idx_col1_notnull“ einfügen. Der doppelte Schlüsselwert ist (1).

Verwenden Sie den folgenden Code, um T3 abzufragen:

SELECT * FROM dbo.T3;

Dieser Code generiert die folgende Ausgabe, die zwei Zeilen mit NULL in col1 zeigt:

col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300
NULL        400

Diese Lösung funktioniert gut, wenn Sie die Eindeutigkeit nur für eine Spalte erzwingen müssen und wenn Sie die referenzielle Integrität nicht mit einem Fremdschlüssel erzwingen müssen, der auf diese Spalte zeigt.

Das Problem mit dem Fremdschlüssel besteht darin, dass SQL Server einen Primärschlüssel oder eine eindeutige Einschränkung oder einen eindeutigen nicht gefilterten Index erfordert, der für die referenzierte Spalte definiert ist. Es funktioniert nicht, wenn nur ein eindeutiger gefilterter Index für die referenzierte Spalte definiert ist. Versuchen wir, eine Tabelle mit einem Fremdschlüssel zu erstellen, der auf T3.col1 verweist. Verwenden Sie zunächst den folgenden Code, um die Tabelle T3 zu erstellen:

DROP TABLE IF EXISTS dbo.T3FK;
GO
 
CREATE TABLE dbo.T3FK
(
  id INT NOT NULL IDENTITY CONSTRAINT PK_T3FK PRIMARY KEY,
  col1 INT NULL, 
  col2 INT NULL, 
  othercol VARCHAR(10) NOT NULL
);

Versuchen Sie dann, den folgenden Code auszuführen, um zu versuchen, einen Fremdschlüssel hinzuzufügen, der von T3FK.col1 auf T3.col1 zeigt:

ALTER TABLE dbo.T3FK ADD CONSTRAINT FK_T3_T3FK
  FOREIGN KEY(col1) REFERENCES dbo.T3(col1);

Dieser Versuch schlägt mit folgendem Fehler fehl:

Msg 1776, Level 16, State 0
Es gibt keine Primär- oder Kandidatenschlüssel in der referenzierten Tabelle 'dbo.T3', die mit der referenzierenden Spaltenliste im Fremdschlüssel 'FK_T3_T3FK' übereinstimmen.

Msg 1750, Level 16, State 1
Constraint oder Index konnte nicht erstellt werden. Siehe vorherige Fehler.

Löschen Sie an dieser Stelle den vorhandenen gefilterten Index zur Bereinigung:

DROP INDEX idx_col1_notnull ON dbo.T3;

Lassen Sie die Tabelle T3FK nicht fallen, da Sie sie in späteren Beispielen verwenden werden.

Das andere Problem mit der gefilterten Indexlösung, vorausgesetzt, Sie benötigen keinen Fremdschlüssel, besteht darin, dass sie nicht funktioniert, wenn Sie die standardmäßige Unique-Constraint-Funktionalität für mehrere Spalten erzwingen müssen, beispielsweise für die Kombination (Spalte1, Spalte2). . Denken Sie daran, dass die standardmäßige Eindeutigkeitsbeschränkung doppelte Nicht-NULL-Kombinationen von Werten in den eindeutigen Spalten verbietet. Um diese Logik mit einem gefilterten Index zu implementieren, müssen Sie nur Zeilen filtern, in denen eine der eindeutigen Spalten nicht NULL ist. Anders ausgedrückt, Sie müssen nur Zeilen filtern, die nicht in allen eindeutigen Spalten NULL-Werte enthalten. Leider erlauben gefilterte Indizes nur sehr einfache Ausdrücke. Sie unterstützen weder OR, NOT noch Manipulationen an den Spalten. Daher wird derzeit keine der folgenden Indexdefinitionen unterstützt:

CREATE UNIQUE NONCLUSTERED INDEX idx_customunique ON dbo.T3(col1, col2)
  WHERE col1 IS NOT NULL OR col2 IS NOT NULL;
 
CREATE UNIQUE NONCLUSTERED INDEX idx_customunique ON dbo.T3(col1, col2)
  WHERE NOT (col1 IS NULL AND col2 IS NULL);
 
CREATE UNIQUE NONCLUSTERED INDEX idx_customunique ON dbo.T3(col1, col2)
  WHERE COALESCE(col1, col2) IS NOT NULL;

Die Problemumgehung in einem solchen Fall besteht darin, eine indizierte Ansicht basierend auf einer Abfrage zu erstellen, die col1 und col2 von T3 mit einer der obigen WHERE-Klauseln zurückgibt, mit einem eindeutigen gruppierten Index auf (col1, col2), wie folgt:

CREATE VIEW dbo.T3CustomUnique WITH SCHEMABINDING
AS
  SELECT col1, col2 FROM dbo.T3 WHERE col1 IS NOT NULL OR col2 IS NOT NULL;
GO
 
CREATE UNIQUE CLUSTERED INDEX idx_col1_col2 ON dbo.T3CustomUnique(col1, col2);
GO

Sie dürfen mehrere Zeilen mit (NULL, NULL) in (Spalte1, Spalte2) hinzufügen, aber Sie dürfen nicht mehrere Vorkommen von Nicht-NULL-Kombinationen von Werten in (Spalte1, Spalte2) hinzufügen, z. B. (3 , NULL) oder (NULL, 300) oder (1, 100). Dennoch unterstützt diese Lösung keinen Fremdschlüssel.

Führen Sie an dieser Stelle den folgenden Code zur Bereinigung aus:

DROP VIEW IF EXISTS dbo.T3CustomUnique;

Lösung 2, mit Ersatzschlüssel und berechneter Spalte

Die Lösungen mit dem gefilterten Index und der indizierten Ansicht sind gut, solange Sie keinen Fremdschlüssel unterstützen müssen. Aber was ist, wenn Sie die referenzielle Integrität erzwingen müssen? Eine Option besteht darin, weiterhin den gefilterten Index oder die indizierte Ansichtslösung zu verwenden, um die Eindeutigkeit zu erzwingen, und Trigger zu verwenden, um die referenzielle Integrität zu erzwingen. Diese Option ist jedoch recht teuer.

Eine andere Möglichkeit besteht darin, eine völlig andere Lösung für den Eindeutigkeitsteil zu verwenden, die einen Fremdschlüssel unterstützt. Die Lösung besteht darin, der referenzierten Tabelle (in unserem Fall T3) zwei Spalten hinzuzufügen. Eine Spalte namens id ist ein Ersatzschlüssel mit einer Identitätseigenschaft. Eine andere Spalte namens flag ist eine dauerhaft berechnete Spalte, die id zurückgibt, wenn col1 NULL ist, und 0, wenn sie nicht NULL ist. Anschließend erzwingen Sie eine Unique-Einschränkung für die Kombination von col1 und flag. Hier ist der Code zum Hinzufügen der beiden Spalten und der eindeutigen Einschränkung:

ALTER TABLE dbo.T3
  ADD id INT NOT NULL IDENTITY,
      flag AS CASE WHEN col1 IS NULL THEN id ELSE 0 END PERSISTED,
      CONSTRAINT UNQ_T3_col1_flag UNIQUE(col1, flag);

Verwenden Sie den folgenden Code, um T3 abzufragen:

SELECT * FROM dbo.T3;

Dieser Code generiert die folgende Ausgabe:

col1        col2        id          flag
----------- ----------- ----------- -----------
1           100         1           0
2           -1          2           0
NULL        -1          3           3
3           300         4           0
NULL        400         5           5

Für die Referenztabelle (in unserem Fall T3FK) fügen Sie eine berechnete Spalte namens flag hinzu, die immer auf 0 gesetzt ist, und einen Fremdschlüssel, der auf (col1, flag) definiert ist und auf die eindeutigen Spalten von T3 (col1, flag) zeigt :

ALTER TABLE dbo.T3FK
  ADD flag AS 0 PERSISTED,
      CONSTRAINT FK_T3_T3FK
        FOREIGN KEY(col1, flag) REFERENCES dbo.T3(col1, flag);

Lassen Sie uns diese Lösung testen.

Versuchen Sie, die folgenden Zeilen hinzuzufügen:

INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES
  (1, 100, 'A'),
  (2, -1, 'B'),
  (3, 300, 'C');

Diese Zeilen wurden ordnungsgemäß hinzugefügt, da alle über entsprechende referenzierte Zeilen verfügen.

Fragen Sie die Tabelle T3FK:

ab
SELECT * FROM dbo.T3FK;

Sie erhalten die folgende Ausgabe:

id          col1        col2        othercol   flag
----------- ----------- ----------- ---------- -----------
1           1           100         A          0
2           2           -1          B          0
3           3           300         C          0

Versuchen Sie, eine Zeile hinzuzufügen, die keine entsprechende Zeile in der referenzierten Tabelle hat:

INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES
  (4, 400, 'D');

Der Versuch wird, wie es sein sollte, mit folgendem Fehler abgelehnt:

Nachricht 547, Ebene 16, Status 0
Die INSERT-Anweisung stand in Konflikt mit der FOREIGN KEY-Einschränkung „FK_T3_T3FK“. Der Konflikt ist in Datenbank „TSQLV5“, Tabelle „dbo.T3“ aufgetreten.

Versuchen Sie, T3FK eine Zeile mit einer NULL in Spalte1 hinzuzufügen:

INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES
  (NULL, NULL, 'E');

Diese Zeile gilt als mit keiner Zeile in T3FK verwandt (optionale Beziehung) und sollte laut Standard unabhängig davon erlaubt sein, ob in der referenzierten Tabelle in col1 eine NULL existiert. T-SQL unterstützt dieses Szenario und die Zeile wurde erfolgreich hinzugefügt.

Fragen Sie die Tabelle T3FK:

ab
SELECT * FROM dbo.T3FK;

Dieser Code generiert die folgende Ausgabe:

id          col1        col2        othercol   flag
----------- ----------- ----------- ---------- -----------
1           1           100         A          0
2           2           -1          B          0
3           3           300         C          0
5           NULL        NULL        E          0

Die Lösung funktioniert gut, wenn Sie die standardmäßige Eindeutigkeitsfunktion für eine einzelne Spalte erzwingen müssen. Es gibt jedoch ein Problem, wenn Sie die Eindeutigkeit für mehrere Spalten erzwingen müssen. Um das Problem zu demonstrieren, löschen Sie zunächst die Tabellen T3 und T3FK:

DROP TABLE IF EXISTS dbo.T3FK, dbo.T3;

Verwenden Sie den folgenden Code, um T3 mit einer zusammengesetzten eindeutigen Einschränkung für (col1, col2, flag) neu zu erstellen:

CREATE TABLE dbo.T3
(
  col1 INT NULL,
  col2 INT NULL,
  id INT NOT NULL IDENTITY,
  flag AS CASE WHEN col1 IS NULL AND col2 IS NULL THEN id ELSE 0 END PERSISTED,
  CONSTRAINT UNQ_T3 UNIQUE(col1, col2, flag)
);

Beachten Sie, dass das Flag auf id gesetzt ist, wenn sowohl col1 als auch col2 NULL sind und andernfalls 0.

Die eindeutige Einschränkung selbst funktioniert gut.

Führen Sie den folgenden Code aus, um einige Zeilen zu T3 hinzuzufügen, einschließlich mehrerer Vorkommen von (NULL, NULL) in (col1, col2):

INSERT INTO dbo.T3(col1, col2) VALUES(1, 100),(1, 200),(NULL, NULL),(NULL, NULL);

Diese Zeilen wurden ordnungsgemäß hinzugefügt.

Versuchen Sie, zwei Vorkommen von (1, NULL) in (Spalte1, Spalte2) hinzuzufügen:

INSERT INTO dbo.T3(col1, col2) VALUES(1, NULL),(1, NULL);

Dieser Versuch schlägt mit dem folgenden Fehler fehl, wie er sollte:

Nachricht 2627, Level 14, Status 1
Verletzung der UNIQUE KEY-Einschränkung 'UNQ_T3'. Doppelter Schlüssel kann nicht in Objekt „dbo.T3“ eingefügt werden. Der doppelte Schlüsselwert ist (1, , 0).

Versuchen Sie, zwei Vorkommen von (NULL, 100) in (Spalte1, Spalte2) hinzuzufügen:

INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 100),(NULL, 100);

Auch dieser Versuch schlägt mit folgendem Fehler fehl:

Nachricht 2627, Level 14, Status 1
Verletzung der UNIQUE KEY-Einschränkung 'UNQ_T3'. Doppelter Schlüssel kann nicht in Objekt „dbo.T3“ eingefügt werden. Der doppelte Schlüsselwert ist (, 100, 0).

Versuchen Sie, die folgenden zwei Zeilen hinzuzufügen, in denen keine Verletzung auftreten sollte:

INSERT INTO dbo.T3(col1, col2) VALUES(3, NULL),(NULL, 300);

Diese Zeilen wurden erfolgreich hinzugefügt.

Fragen Sie an dieser Stelle die Tabelle T3 ab:

SELECT * FROM dbo.T3;

Sie erhalten die folgende Ausgabe:

col1        col2        id          flag
----------- ----------- ----------- -----------
1           100         1           0
1           200         2           0
NULL        NULL        3           3
NULL        NULL        4           4
3           NULL        9           0
NULL        300         10          0

So weit, so gut.

Führen Sie als Nächstes den folgenden Code aus, um die Tabelle T3FK mit einem zusammengesetzten Fremdschlüssel zu erstellen, der auf die eindeutigen Spalten von T3 verweist:

CREATE TABLE dbo.T3FK
(
  id INT NOT NULL IDENTITY CONSTRAINT PK_T3FK PRIMARY KEY,
  col1 INT NULL, 
  col2 INT NULL, 
  othercol VARCHAR(10) NOT NULL,
  flag AS 0 PERSISTED,
  CONSTRAINT FK_T3_T3FK
    FOREIGN KEY(col1, col2, flag) REFERENCES dbo.T3(col1, col2, flag)
);

Diese Lösung ermöglicht natürlich das Hinzufügen von Zeilen zu T3FK mit (NULL, NULL) in (col1, col2). Das Problem ist, dass es auch erlaubt, Zeilen mit NULL entweder in Spalte 1 oder Spalte 2 hinzuzufügen, selbst wenn die andere Spalte nicht NULL ist und die referenzierte Tabelle T3 keine solche Tastenkombination hat. Versuchen Sie beispielsweise, die folgende Zeile zu T3FK hinzuzufügen:

INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES(5, NULL, 'A');

Diese Zeile wurde erfolgreich hinzugefügt, obwohl es keine zugehörige Zeile in T3 gibt. Laut Standard sollte diese Zeile nicht erlaubt sein.

Zurück zum Reißbrett…

Lösung 3, mit Ersatzschlüssel und berechneter Spalte

Das Problem mit der vorherigen Lösung (Lösung 2) tritt auf, wenn Sie einen zusammengesetzten Fremdschlüssel unterstützen müssen. Es erlaubt Zeilen in der referenzierenden Tabelle, die in mindestens einer referenzierenden Spalte eine NULL enthalten, selbst wenn es Nicht-NULL-Werte in anderen referenzierenden Spalten und keine zugehörige Zeile in der referenzierten Tabelle gibt. Um dies zu beheben, können Sie eine Variation der vorherigen Lösung verwenden, die wir Lösung 3 nennen werden.

Verwenden Sie zunächst den folgenden Code, um die vorhandenen Tabellen zu löschen:

DROP TABLE IF EXISTS dbo.T3FK, dbo.T3;

In der neuen Lösung in der referenzierten Tabelle (in unserem Fall T3) verwenden Sie weiterhin die identitätsbasierte ID-Ersatzschlüsselspalte. Sie verwenden auch eine permanente berechnete Spalte namens unqpath. Wenn alle eindeutigen Spalten (col1 und col2 in unserem Beispiel) NULL sind, setzen Sie unqpath auf eine Zeichenkettendarstellung von id (keine Trennzeichen ). Wenn eine der eindeutigen Spalten nicht NULL ist, setzen Sie unqpath mithilfe der CONCAT-Funktion auf eine Zeichenfolgendarstellung einer getrennten Liste der eindeutigen Spaltenwerte. Diese Funktion ersetzt eine NULL durch eine leere Zeichenfolge. Wichtig ist, darauf zu achten, ein Trennzeichen zu verwenden, das normalerweise nicht in den Daten selbst vorkommen kann. Bei ganzzahligen col1- und col2-Werten haben Sie beispielsweise nur Ziffern, sodass jedes andere Trennzeichen als eine Ziffer funktionieren würde. In meinem Beispiel verwende ich einen Punkt (.). Anschließend erzwingen Sie eine Unique-Einschränkung für unqpath. Sie werden niemals einen Konflikt zwischen dem unqpath-Wert haben, wenn alle eindeutigen Spalten NULL sind (auf id gesetzt) ​​und wenn eine der eindeutigen Spalten nicht NULL ist, da unqpath im ersten Fall kein Trennzeichen enthält und im letzteren Fall . Denken Sie daran, dass Sie Lösung 3 verwenden, wenn Sie einen zusammengesetzten Schlüsselfall haben, und wahrscheinlich Lösung 2 bevorzugen, die einfacher ist, wenn Sie einen Schlüsselfall mit einer einzelnen Spalte haben. Wenn Sie Lösung 3 auch mit einem einspaltigen Schlüssel und nicht Lösung 2 verwenden möchten, stellen Sie einfach sicher, dass Sie das Trennzeichen hinzufügen, wenn die eindeutige Spalte nicht NULL ist, obwohl es nur einen Wert gibt. Auf diese Weise entsteht kein Konflikt, wenn id in einer Zeile, in der col1 NULL ist, gleich col1 in einer anderen Zeile ist, da erstere kein Trennzeichen haben und letztere.

Hier ist der Code zum Erstellen von T3 mit den oben genannten Ergänzungen:

CREATE TABLE dbo.T3
(
  col1 INT NULL,
  col2 INT NULL,
  id INT NOT NULL IDENTITY,
  unqpath AS CASE WHEN col1 IS NULL AND col2 IS NULL THEN CAST(id AS VARCHAR(10)) 
                  ELSE CONCAT(CAST(col1 AS VARCHAR(11)), '.', CAST(col2 AS VARCHAR(11)))
             END PERSISTED,
  CONSTRAINT UNQ_T3 UNIQUE(unqpath)
);

Bevor wir uns mit einem Fremdschlüssel und der referenzierenden Tabelle befassen, testen wir die eindeutige Einschränkung. Denken Sie daran, es soll doppelte Kombinationen von Nicht-NULL-Werten in den eindeutigen Spalten zulassen, aber es soll das mehrfache Vorkommen von reinen NULL-Werten in den eindeutigen Spalten zulassen.

Führen Sie den folgenden Code aus, um einige Zeilen hinzuzufügen, darunter zwei Vorkommen von (NULL, NULL) in (col1, col2):

INSERT INTO dbo.T3(col1, col2) VALUES(1, 100),(1, 200),(NULL, NULL),(NULL, NULL);

Dieser Code wird wie erwartet erfolgreich abgeschlossen.

Versuchen Sie, zwei Vorkommen von (1, NULL) in (Spalte1, Spalte2) hinzuzufügen:

INSERT INTO dbo.T3(col1, col2) VALUES(1, NULL),(1, NULL);

Dieser Code schlägt wie vorgesehen mit dem folgenden Fehler fehl:

Nachricht 2627, Level 14, Status 1
Verletzung der UNIQUE KEY-Einschränkung 'UNQ_T3'. Doppelter Schlüssel kann nicht in Objekt „dbo.T3“ eingefügt werden. Der doppelte Schlüsselwert ist (1.).

Ebenso wird auch der folgende Versuch abgelehnt:

INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 100),(NULL, 100);

Sie erhalten die folgende Fehlermeldung:

Nachricht 2627, Level 14, Status 1
Verletzung der UNIQUE KEY-Einschränkung 'UNQ_T3'. Doppelter Schlüssel kann nicht in Objekt „dbo.T3“ eingefügt werden. Der doppelte Schlüsselwert ist (.100).

Führen Sie den folgenden Code aus, um ein paar weitere Zeilen hinzuzufügen:

INSERT INTO dbo.T3(col1, col2) VALUES(3, NULL),(NULL, 300);

Dieser Code wird erfolgreich ausgeführt, wie er sollte.

Fragen Sie an dieser Stelle T3:

ab
SELECT * FROM dbo.T3;

Sie erhalten die folgende Ausgabe:

col1        col2        id          unqpath
----------- ----------- ----------- -----------------------
1           100         1           1.100
1           200         2           1.200
NULL        NULL        3           3
NULL        NULL        4           4
3           NULL        9           3.
NULL        300         10          .300

Beobachten Sie die unqpath-Werte und vergewissern Sie sich, dass Sie die Logik hinter ihrer Konstruktion und den Unterschied zwischen einem Fall verstehen, in dem alle eindeutigen Spalten NULL sind (kein Trennzeichen), und wenn mindestens eine nicht NULL ist (Trennzeichen vorhanden).

Wie für die Referenzierungstabelle T3FK; Sie definieren auch eine berechnete Spalte namens unqpath, aber wenn alle referenzierenden Spalten NULL sind, setzen Sie die Spalte auf NULL – nicht auf id. Wenn eine der referenzierenden Spalten nicht NULL ist, erstellen Sie dieselbe getrennte Liste von Werten wie in T3. Dann definieren Sie einen Fremdschlüssel auf T3FK.unqpath, der auf T3.unqpath zeigt, etwa so:

CREATE TABLE dbo.T3FK
(
  id INT NOT NULL IDENTITY CONSTRAINT PK_T3FK PRIMARY KEY,
  col1 INT NULL, 
  col2 INT NULL, 
  othercol VARCHAR(10) NOT NULL,
  unqpath AS CASE WHEN col1 IS NULL AND col2 IS NULL THEN NULL
                  ELSE CONCAT(CAST(col1 AS VARCHAR(11)), '.', CAST(col2 AS VARCHAR(11)))
             END PERSISTED,
  CONSTRAINT FK_T3_T3FK
    FOREIGN KEY(unqpath) REFERENCES dbo.T3(unqpath)
);

Dieser Fremdschlüssel weist Zeilen in T3FK zurück, in denen eine der referenzierenden Spalten nicht NULL ist und es keine zugehörige Zeile in der referenzierten Tabelle T3 gibt, wie der folgende Versuch zeigt:

INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES(5, NULL, 'A');

Dieser Code generiert den folgenden Fehler:

Nachricht 547, Ebene 16, Status 0
Die INSERT-Anweisung stand in Konflikt mit der FOREIGN KEY-Einschränkung „FK_T3_T3FK“. Der Konflikt ist in Datenbank „TSQLV5“, Tabelle „dbo.T3“, Spalte „unqpath“ aufgetreten.

Diese Lösung berücksichtigt Zeilen in T3FK, in denen eine der referenzierenden Spalten nicht NULL ist, solange eine zugehörige Zeile in T3 vorhanden ist, sowie Zeilen mit NULL-Werten in allen referenzierenden Spalten, da solche Zeilen als nicht mit Zeilen in T3 verknüpft betrachtet werden. Der folgende Code fügt T3FK solche gültigen Zeilen hinzu:

INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES
  (1   , 100 , 'A'),
  (1   , 200 , 'B'),
  (3   , NULL, 'C'),
  (NULL, 300 , 'D'),
  (NULL, NULL, 'E'),
  (NULL, NULL, 'F');

Dieser Code wurde erfolgreich abgeschlossen.

Führen Sie den folgenden Code aus, um T3FK abzufragen:

SELECT * FROM dbo.T3FK;

Sie erhalten die folgende Ausgabe:

id          col1        col2        othercol   unqpath
----------- ----------- ----------- ---------- -----------------------
2           1           100         A          1.100
3           1           200         B          1.200
4           3           NULL        C          3.
5           NULL        300         D          .300
6           NULL        NULL        E          NULL
7           NULL        NULL        F          NULL

Es brauchte also ein wenig Kreativität, aber jetzt haben Sie eine Problemumgehung für die Standardeindeutigkeitsbeschränkung, einschließlich Fremdschlüsselunterstützung.

Schlussfolgerung

Sie würden denken, dass eine Unique-Einschränkung ein einfaches Feature ist, aber es kann ein bisschen schwierig werden, wenn Sie NULLs in den Unique-Spalten unterstützen müssen. Komplexer wird es, wenn Sie die standardmäßige Unique-Constraint-Funktionalität in T-SQL implementieren müssen, da die beiden unterschiedliche Regeln in Bezug auf die Behandlung von NULLen verwenden. In diesem Artikel habe ich den Unterschied zwischen den beiden erklärt und Problemumgehungen bereitgestellt, die in T-SQL funktionieren. Sie können einen einfachen gefilterten Index verwenden, wenn Sie die Eindeutigkeit nur für eine NULL-fähige Spalte erzwingen müssen und keinen Fremdschlüssel unterstützen müssen, der auf diese Spalte verweist. Wenn Sie jedoch entweder einen Fremdschlüssel oder eine zusammengesetzte eindeutige Einschränkung mit der Standardfunktionalität unterstützen müssen, benötigen Sie eine komplexere Implementierung mit einem Ersatzschlüssel und einer berechneten Spalte.