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

Complessità NULL – Parte 4, Vincolo unico standard mancante

Questo articolo è la parte 4 di una serie sulle complessità NULL. Negli articoli precedenti (Parte 1, Parte 2 e Parte 3), ho trattato il significato di NULL come indicatore di un valore mancante, il modo in cui i NULL si comportano nei confronti e in altri elementi di query e le funzionalità di gestione NULL standard che non lo sono ancora disponibile in T-SQL. Questo mese tratterò la differenza tra il modo in cui un vincolo univoco è definito nello standard SQL ISO/IEC e il modo in cui funziona in T-SQL. Fornirò anche soluzioni personalizzate che puoi implementare se hai bisogno della funzionalità standard.

Vincolo UNICO standard

SQL Server gestisce i valori NULL proprio come i valori non NULL allo scopo di applicare un vincolo univoco. Cioè, un vincolo univoco su T è soddisfatto se e solo se non esistono due righe R1 e R2 di T tali che R1 e R2 abbiano la stessa combinazione di valori NULL e non NULL nelle colonne univoche. Si supponga, ad esempio, di definire un vincolo univoco su col1, che è una colonna NULLable di un tipo di dati INT. Un tentativo di modificare la tabella in un modo che risulterebbe in più di una riga con un NULL in col1 verrà rifiutato, proprio come verrà rifiutata una modifica che risulterebbe in più di una riga con il valore 1 in col1.

Si supponga di definire un vincolo univoco composito sulla combinazione di colonne INT NULLable col1 e col2. Un tentativo di modificare la tabella in un modo che risulterebbe in più di un'occorrenza di una qualsiasi delle seguenti combinazioni di valori (col1, col2) verrà rifiutato:(NULL, NULL), (3, NULL), (NULL, 300 ), (1, 100).

Quindi, come puoi vedere, l'implementazione T-SQL del vincolo univoco tratta i NULL proprio come valori non NULL ai fini dell'imposizione dell'unicità.

Se vuoi definire una chiave esterna su una tabella X che fa riferimento a una tabella Y, devi applicare l'univocità alle colonne di riferimento con una delle seguenti opzioni:

  • Chiave primaria
  • Vincolo unico
  • Indice univoco non filtrato

Una chiave primaria non è consentita su colonne NULLable. Sia un vincolo univoco (che crea un indice sotto le copertine) sia un indice univoco creato in modo esplicito sono consentiti su colonne NULLable e ne impongono l'unicità in T-SQL utilizzando la logica sopra menzionata. La tabella di riferimento può avere righe con un NULL nella colonna di riferimento, indipendentemente dal fatto che la tabella di riferimento abbia una riga con un NULL nella colonna di riferimento. L'idea è di supportare una relazione facoltativa. Alcune righe nella tabella di riferimento potrebbero non essere correlate ad alcuna riga nella tabella di riferimento. Lo implementerai utilizzando un NULL nella colonna di riferimento.

Per dimostrare l'implementazione T-SQL di un vincolo univoco, esegui il codice seguente, che crea una tabella denominata T3 con un vincolo univoco definito nella colonna NULLable INT col1 e la popola con alcune righe di esempio:

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

Utilizzare il codice seguente per interrogare la tabella:

SELECT * FROM dbo.T3;

Questa query genera il seguente output:

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

Tentativo di inserire una seconda riga con un NULL in col1:

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

Questo tentativo viene rifiutato e viene visualizzato il seguente errore:

Msg 2627, livello 14, stato 1
Violazione del vincolo UNIQUE KEY 'UNQ_T3'. Impossibile inserire la chiave duplicata nell'oggetto 'dbo.T3'. Il valore della chiave duplicata è ().

La definizione di vincolo univoco standard è leggermente diversa dalla versione T-SQL. La differenza principale ha a che fare con la gestione NULL. Ecco la definizione univoca del vincolo dallo standard:

"Un vincolo univoco su T è soddisfatto se e solo se non esistono due righe R1 e R2 di T tali che R1 e R2 abbiano gli stessi valori non NULL nelle colonne univoche."

Quindi, una tabella T con un vincolo univoco su col1 consentirà più righe con NULL in col1, ma non consentirà più righe con lo stesso valore non NULL in col1.

Quello che è un po' più complicato da spiegare è cosa succede secondo lo standard con un vincolo unico composito. Supponiamo di avere un vincolo univoco definito su (col1, col2). Puoi avere più righe con (NULL, NULL), ma non puoi avere più righe con (3, NULL), proprio come non puoi avere più righe con (1, 100). Allo stesso modo, non puoi avere più righe con (NULL, 300). Il punto è che non puoi avere più righe con gli stessi valori non NULL nelle colonne univoche. Come per una chiave esterna, puoi avere un numero qualsiasi di righe nella tabella di riferimento con NULL in tutte le colonne di riferimento, indipendentemente da ciò che esiste nella tabella di riferimento. Tali righe non sono correlate ad alcuna riga nella tabella di riferimento (relazione facoltativa). Tuttavia, se hai un valore non NULL in una qualsiasi delle colonne di riferimento, deve esistere una riga nella tabella di riferimento con gli stessi valori non NULL nelle colonne di riferimento.

Si supponga di disporre di un database in una piattaforma che supporta il vincolo univoco standard e di dover migrare tale database a SQL Server. Potrebbero verificarsi problemi con l'applicazione di vincoli univoci in SQL Server se le colonne univoche supportano i valori NULL. I dati considerati validi nel sistema di origine possono essere considerati non validi in SQL Server. Nelle sezioni seguenti esplorerò una serie di possibili soluzioni alternative in SQL Server.

Soluzione 1, utilizzando l'indice filtrato o la vista indicizzata

Una soluzione alternativa comune in T-SQL per applicare la funzionalità di vincolo univoco standard quando è coinvolta solo una colonna di destinazione consiste nell'usare un indice filtrato univoco che filtra solo le righe in cui la colonna di destinazione non è NULL. Il codice seguente elimina il vincolo univoco esistente da T3 e implementa tale indice:

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

Poiché l'indice filtra solo le righe in cui col1 non è NULL, la relativa proprietà UNIQUE viene applicata solo ai valori col1 non NULL.

Ricordiamo che T3 ha già una riga con un NULL in col1. Per testare questa soluzione, usa il codice seguente per aggiungere una seconda riga con un NULL in col1:

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

Questo codice viene eseguito correttamente.

Ricordiamo che T3 ha già una riga con il valore 1 in col1. Esegui il codice seguente per tentare di aggiungere una seconda riga con 1 in col1:

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

Come previsto, questo tentativo non riesce con il seguente errore:

Msg 2601, livello 14, stato 1
Impossibile inserire una riga di chiave duplicata nell'oggetto 'dbo.T3' con indice univoco 'idx_col1_notnull'. Il valore della chiave duplicata è (1).

Utilizzare il codice seguente per interrogare T3:

SELECT * FROM dbo.T3;

Questo codice genera il seguente output che mostra due righe con un NULL in col1:

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

Questa soluzione funziona bene quando devi imporre l'univocità su una sola colonna e quando non è necessario imporre l'integrità referenziale con una chiave esterna che punta a quella colonna.

Il problema con la chiave esterna è che SQL Server richiede una chiave primaria o un vincolo univoco o un indice univoco non filtrato definito nella colonna a cui si fa riferimento. Non funziona quando c'è solo un indice filtrato univoco definito nella colonna di riferimento. Proviamo a creare una tabella con una chiave esterna che faccia riferimento a T3.col1. Innanzitutto, utilizza il codice seguente per creare la tabella T3:

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

Quindi prova a eseguire il codice seguente nel tentativo di aggiungere una chiave esterna che punta da T3FK.col1 a T3.col1:

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

Questo tentativo non riesce con il seguente errore:

Msg 1776, livello 16, stato 0
Non ci sono chiavi primarie o candidate nella tabella di riferimento 'dbo.T3' che corrispondono all'elenco delle colonne di riferimento nella chiave esterna 'FK_T3_T3FK'.

Msg 1750, livello 16, stato 1
Impossibile creare un vincolo o un indice. Vedi gli errori precedenti.

A questo punto, elimina l'indice filtrato esistente per la pulizia:

DROP INDEX idx_col1_notnull ON dbo.T3;

Non abbandonare la tabella T3FK, poiché la utilizzerai negli esempi successivi.

L'altro problema con la soluzione dell'indice filtrato, supponendo che non sia necessaria una chiave esterna, è che non funziona quando è necessario applicare la funzionalità di vincolo univoco standard su più colonne, ad esempio sulla combinazione (col1, col2) . Ricorda che il vincolo univoco standard non consente combinazioni di valori non NULL duplicate nelle colonne univoche. Per implementare questa logica con un indice filtrato, è necessario filtrare solo le righe in cui una qualsiasi delle colonne univoche non è NULL. Formulato in modo diverso, devi filtrare solo le righe che non hanno NULL in tutte le colonne univoche. Sfortunatamente, gli indici filtrati consentono solo espressioni molto semplici. Non supportano OR, NOT o manipolazione sulle colonne. Quindi nessuna delle seguenti definizioni di indice è attualmente supportata:

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;

La soluzione alternativa in questo caso è creare una vista indicizzata basata su una query che restituisce col1 e col2 da T3 con una delle clausole WHERE sopra, con un indice cluster univoco su (col1, col2), in questo modo:

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

Ti sarà consentito aggiungere più righe con (NULL, NULL) in (col1, col2), ma non ti sarà consentito aggiungere più occorrenze di combinazioni di valori non NULL in (col1, col2), ad esempio (3 , NULL) o (NULL, 300) o (1, 100). Tuttavia, questa soluzione non supporta una chiave esterna.

A questo punto, esegui il codice seguente per la pulizia:

DROP VIEW IF EXISTS dbo.T3CustomUnique;

Soluzione 2, utilizzando la chiave surrogata e la colonna calcolata

Le soluzioni con l'indice filtrato e la vista indicizzata sono buone purché non sia necessario supportare una chiave esterna. Ma cosa succede se è necessario rafforzare l'integrità referenziale? Un'opzione consiste nel continuare a utilizzare l'indice filtrato o la soluzione di visualizzazione indicizzata per imporre l'unicità e utilizzare i trigger per imporre l'integrità referenziale. Tuttavia, questa opzione è piuttosto costosa.

Un'altra opzione consiste nell'utilizzare una soluzione completamente diversa per la parte di unicità che supporta una chiave esterna. La soluzione prevede l'aggiunta di due colonne alla tabella di riferimento (T3 nel nostro caso). Una colonna denominata id è una chiave surrogata con una proprietà identity. Un'altra colonna denominata flag è una colonna calcolata persistente che restituisce id quando col1 è NULL e 0 quando non è NULL. Quindi imporre un vincolo univoco sulla combinazione di col1 e flag. Ecco il codice per aggiungere le due colonne e il vincolo univoco:

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

Utilizzare il codice seguente per interrogare T3:

SELECT * FROM dbo.T3;

Questo codice genera il seguente output:

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

Per quanto riguarda la tabella di riferimento (T3FK nel nostro caso), aggiungi una colonna calcolata chiamata flag che è sempre impostata su 0 e una chiave esterna definita su (col1, flag) che punta alle colonne univoche di T3 (col1, flag), in questo modo :

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

Proviamo questa soluzione.

Prova ad aggiungere le seguenti righe:

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

Queste righe vengono aggiunte correttamente, come dovrebbero, poiché tutte hanno righe di riferimento corrispondenti.

Interroga la tabella T3FK:

SELECT * FROM dbo.T3FK;

Ottieni il seguente output:

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

Prova ad aggiungere una riga che non ha una riga corrispondente nella tabella di riferimento:

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

Il tentativo viene rifiutato, come dovrebbe essere, con il seguente errore:

Msg 547, livello 16, stato 0
L'istruzione INSERT era in conflitto con il vincolo FOREIGN KEY "FK_T3_T3FK". Il conflitto si è verificato nel database "TSQLV5", tabella "dbo.T3".

Prova ad aggiungere una riga a T3FK con un NULL in col1:

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

Questa riga è considerata non correlata a nessuna riga in T3FK (relazione facoltativa) e, secondo lo standard, dovrebbe essere consentita indipendentemente dal fatto che nella tabella di riferimento in col1 sia presente un NULL. T-SQL supporta questo scenario e la riga viene aggiunta correttamente.

Interroga la tabella T3FK:

SELECT * FROM dbo.T3FK;

Questo codice genera il seguente output:

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

La soluzione funziona bene quando è necessario applicare la funzionalità di unicità standard su una singola colonna. Ma ha un problema quando è necessario imporre l'unicità su più colonne. Per dimostrare il problema, prima elimina le tabelle T3 e T3FK:

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

Usa il codice seguente per ricreare T3 con un vincolo univoco composito su (col1, col2, flag):

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

Nota che il flag è impostato su id quando sia col1 che col2 sono NULL e 0 altrimenti.

Il vincolo unico stesso funziona bene.

Esegui il codice seguente per aggiungere alcune righe a T3, incluse più occorrenze di (NULL, NULL) in (col1, col2):

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

Queste righe vengono aggiunte correttamente come dovrebbero.

Prova ad aggiungere due occorrenze di (1, NULL) in (col1, col2):

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

Questo tentativo fallisce come dovrebbe con il seguente errore:

Msg 2627, livello 14, stato 1
Violazione del vincolo UNIQUE KEY 'UNQ_T3'. Impossibile inserire la chiave duplicata nell'oggetto 'dbo.T3'. Il valore della chiave duplicata è (1, , 0).

Prova ad aggiungere due occorrenze di (NULL, 100) in (col1, col2):

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

Anche questo tentativo fallisce come dovrebbe con il seguente errore:

Msg 2627, livello 14, stato 1
Violazione del vincolo UNIQUE KEY 'UNQ_T3'. Impossibile inserire la chiave duplicata nell'oggetto 'dbo.T3'. Il valore della chiave duplicata è (, 100, 0).

Prova ad aggiungere le due righe seguenti, in cui non dovrebbe verificarsi alcuna violazione:

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

Queste righe sono state aggiunte correttamente.

Interroga la tabella T3 a questo punto:

SELECT * FROM dbo.T3;

Ottieni il seguente output:

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

Fin qui tutto bene.

Quindi, esegui il codice seguente per creare la tabella T3FK con una chiave esterna composita che fa riferimento alle colonne univoche di T3:

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

Questa soluzione consente naturalmente di aggiungere righe a T3FK con (NULL, NULL) in (col1, col2). Il problema è che consente anche di aggiungere righe NULL in col1 o col2, anche quando l'altra colonna non è NULL e la tabella di riferimento T3 non ha tale combinazione di tasti. Ad esempio, prova ad aggiungere la seguente riga a T3FK:

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

Questa riga è stata aggiunta correttamente anche se non ci sono righe correlate in T3. Secondo lo standard, questa riga non dovrebbe essere consentita.

Torna al tavolo da disegno...

Soluzione 3, utilizzando la chiave surrogata e la colonna calcolata

Il problema con la soluzione precedente (Soluzione 2) sorge quando è necessario supportare una chiave esterna composita. Consente le righe nella tabella di riferimento che hanno un NULL nell'elenco di una colonna di riferimento, anche quando sono presenti valori non NULL in altre colonne di riferimento e nessuna riga correlata nella tabella di riferimento. Per risolvere questo problema, puoi utilizzare una variante della soluzione precedente, che chiameremo Soluzione 3.

Innanzitutto, usa il codice seguente per eliminare le tabelle esistenti:

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

Nella nuova soluzione nella tabella di riferimento (T3 nel nostro caso), si utilizza ancora la colonna della chiave surrogata dell'ID basato sull'identità. Si utilizza anche una colonna calcolata persistente chiamata unqpath. Quando tutte le colonne univoche (col1 e col2 nel nostro esempio) sono NULL, imposti unqpath su una rappresentazione di stringa di caratteri di id (nessun separatore ). Quando una qualsiasi delle colonne univoche non è NULL, si imposta unqpath su una rappresentazione di stringa di caratteri di un elenco separato dei valori di colonna univoci utilizzando la funzione CONCAT. Questa funzione sostituisce un NULL con una stringa vuota. L'importante è assicurarsi di utilizzare un separatore che normalmente non può apparire nei dati stessi. Ad esempio, con valori interi col1 e col2 hai solo cifre, quindi qualsiasi separatore diverso da una cifra funzionerebbe. Nel mio esempio userò un punto (.). Quindi imposti un vincolo univoco su unqpath. Non avrai mai un conflitto tra il valore unqpath quando tutte le colonne univoche sono NULL (impostate su id) rispetto a quando una qualsiasi delle colonne univoche non è NULL perché nel primo caso unqpath non contiene un separatore e nel secondo caso lo fa . Ricorda che utilizzerai la Soluzione 3 quando disponi di un key case composto e probabilmente preferirai la Soluzione 2, che è più semplice, quando hai un key case a colonna singola. Se desideri utilizzare la Soluzione 3 anche con una chiave a colonna singola e non con la Soluzione 2, assicurati di aggiungere il separatore quando la colonna univoca non è NULL anche se è coinvolto un solo valore. In questo modo non avrai un conflitto quando id in una riga in cui col1 è NULL è uguale a col1 in un'altra riga, poiché il primo non avrà separatore e il secondo lo farà.

Ecco il codice per creare T3 con le suddette aggiunte:

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

Prima di occuparci di una chiave esterna e della tabella di riferimento, testiamo il vincolo univoco. Ricorda, dovrebbe impedire combinazioni duplicate di valori non NULL nelle colonne univoche, ma dovrebbe consentire più occorrenze di tutti NULL nelle colonne univoche.

Esegui il codice seguente per aggiungere alcune righe, incluse due occorrenze di (NULL, NULL) in (col1, col2):

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

Questo codice viene completato correttamente come dovrebbe.

Prova ad aggiungere due occorrenze di (1, NULL) in (col1, col2):

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

Questo codice non riesce con il seguente errore come dovrebbe:

Msg 2627, livello 14, stato 1
Violazione del vincolo UNIQUE KEY 'UNQ_T3'. Impossibile inserire la chiave duplicata nell'oggetto 'dbo.T3'. Il valore della chiave duplicata è (1.).

Allo stesso modo, viene rifiutato anche il seguente tentativo:

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

Viene visualizzato il seguente errore:

Msg 2627, livello 14, stato 1
Violazione del vincolo UNIQUE KEY 'UNQ_T3'. Impossibile inserire la chiave duplicata nell'oggetto 'dbo.T3'. Il valore della chiave duplicata è (.100).

Esegui il codice seguente per aggiungere un altro paio di righe:

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

Questo codice viene eseguito correttamente come dovrebbe.

A questo punto, interroga T3:

SELECT * FROM dbo.T3;

Ottieni il seguente output:

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

Osserva i valori di unqpath e assicurati di aver compreso la logica alla base della loro costruzione e la differenza tra un caso in cui tutte le colonne univoche sono NULL (nessun separatore) e quando almeno una non è NULL (esiste un separatore).

Per quanto riguarda la tabella di riferimento, T3FK; definisci anche una colonna calcolata chiamata unqpath, ma nel caso in cui tutte le colonne di riferimento siano NULL, imposti la colonna su NULL, non su id. Quando una qualsiasi delle colonne di riferimento non è NULL, costruisci lo stesso elenco separato di valori come hai fatto in T3. Quindi definisci una chiave esterna su T3FK.unqpath che punta a T3.unqpath, in questo modo:

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

Questa chiave esterna rifiuterà le righe in T3FK in cui una qualsiasi delle colonne di riferimento non è NULL e non è presente alcuna riga correlata nella tabella di riferimento T3, come mostra il seguente tentativo:

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

Questo codice genera il seguente errore:

Msg 547, livello 16, stato 0
L'istruzione INSERT era in conflitto con il vincolo FOREIGN KEY "FK_T3_T3FK". Il conflitto si è verificato nel database "TSQLV5", tabella "dbo.T3", colonna 'unqpath'.

Questa soluzione conterrà le righe in T3FK in cui una qualsiasi delle colonne di riferimento non è NULL purché esista una riga correlata in T3, nonché le righe con NULL in tutte le colonne di riferimento, poiché tali righe sono considerate non correlate a qualsiasi riga in T3. Il codice seguente aggiunge tali righe valide a T3FK:

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

Questo codice viene completato correttamente.

Eseguire il codice seguente per interrogare T3FK:

SELECT * FROM dbo.T3FK;

Ottieni il seguente output:

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

Quindi ci è voluta un po' di creatività, ma ora hai una soluzione alternativa per il vincolo univoco standard, incluso il supporto della chiave esterna.

Conclusione

Si potrebbe pensare che un vincolo univoco sia una caratteristica semplice, ma può diventare un po' complicato quando è necessario supportare i valori NULL nelle colonne univoche. Diventa più complesso quando è necessario implementare la funzionalità di vincolo univoco standard in T-SQL, poiché i due utilizzano regole diverse in termini di modalità di gestione dei NULL. In questo articolo ho spiegato la differenza tra i due e ho fornito soluzioni alternative che funzionano in T-SQL. È possibile utilizzare un semplice indice filtrato quando è necessario imporre l'univocità su una sola colonna NULLable e non è necessario supportare una chiave esterna che fa riferimento a quella colonna. Tuttavia, se devi supportare una chiave esterna o un vincolo univoco composito con la funzionalità standard, avrai bisogno di un'implementazione più complessa con una chiave surrogata e una colonna calcolata.