sql >> Base de Datos >  >> RDS >> Database

Complejidades NULL – Parte 4, Restricción única estándar faltante

Este artículo es la Parte 4 de una serie sobre las complejidades de NULL. En los artículos anteriores (Parte 1, Parte 2 y Parte 3), cubrí el significado de NULL como marcador de un valor faltante, cómo se comportan los valores NULL en las comparaciones y en otros elementos de consulta, y las características estándar de manejo de NULL que no son aún disponible en T-SQL. Este mes cubro la diferencia entre la forma en que se define una restricción única en el estándar ISO/IEC SQL y la forma en que funciona en T-SQL. También proporcionaré soluciones personalizadas que puede implementar si necesita la funcionalidad estándar.

Restricción ÚNICA estándar

SQL Server maneja valores NULL como valores no NULL con el fin de imponer una restricción única. Es decir, una restricción única en T se satisface si y solo si no existen dos filas R1 y R2 de T tales que R1 y R2 tengan la misma combinación de valores NULL y no NULL en las columnas únicas. Por ejemplo, suponga que define una restricción única en col1, que es una columna NULLable de un tipo de datos INT. Se rechazará un intento de modificar la tabla de forma que resulte en más de una fila con un NULL en col1, al igual que se rechazará una modificación que resulte en más de una fila con el valor 1 en col1.

Suponga que define una restricción única compuesta en la combinación de las columnas INT anulables col1 y col2. Se rechazará un intento de modificar la tabla de forma que resulte en más de una ocurrencia de cualquiera de las siguientes combinaciones de valores (col1, col2):(NULL, NULL), (3, NULL), (NULL, 300 ), (1, 100).

Entonces, como puede ver, la implementación de T-SQL de la restricción única trata los NULL como valores no NULL con el propósito de hacer cumplir la unicidad.

Si desea definir una clave externa en alguna tabla X que haga referencia a alguna tabla Y, debe imponer la unicidad en las columnas a las que se hace referencia con una de las siguientes opciones:

  • Clave principal
  • Restricción única
  • Índice único no filtrado

No se permite una clave principal en las columnas NULLable. Tanto una restricción única (que crea un índice debajo de las cubiertas) como un índice único creado explícitamente están permitidos en las columnas que aceptan valores NULL y hacen cumplir su unicidad en T-SQL usando la lógica mencionada anteriormente. La tabla de referencia puede tener filas con NULL en la columna de referencia, independientemente de si la tabla a la que se hace referencia tiene una fila con NULL en la columna a la que se hace referencia. La idea es apoyar una relación opcional. Algunas filas de la tabla de referencia podrían ser aquellas que no están relacionadas con ninguna fila de la tabla a la que se hace referencia. Implementarás esto usando un NULL en la columna de referencia.

Para demostrar la implementación de T-SQL de una restricción única, ejecute el siguiente código, que crea una tabla llamada T3 con una restricción única definida en la columna col1 de INT que admite valores NULL y la rellena con algunas filas de muestra:

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

Use el siguiente código para consultar la tabla:

SELECT * FROM dbo.T3;

Esta consulta genera el siguiente resultado:

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

Intente insertar una segunda fila con un NULL en col1:

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

Este intento es rechazado y obtiene el siguiente error:

Mensaje 2627, Nivel 14, Estado 1
Violación de la restricción de CLAVE ÚNICA 'UNQ_T3'. No se puede insertar una clave duplicada en el objeto 'dbo.T3'. El valor de la clave duplicada es ().

La definición de restricción única estándar es un poco diferente a la versión de T-SQL. La principal diferencia tiene que ver con el manejo de NULL. Aquí está la definición de restricción única del estándar:

"Una restricción única en T se cumple si y solo si no existen dos filas R1 y R2 de T de modo que R1 y R2 tengan los mismos valores no NULL en las columnas únicas".

Por lo tanto, una tabla T con una restricción única en col1 permitirá múltiples filas con NULL en col1, pero no permitirá múltiples filas con el mismo valor no NULL en col1.

Lo que es un poco más complicado de explicar es lo que sucede de acuerdo con el estándar con una restricción única compuesta. Digamos que tiene una restricción única definida en (col1, col2). Puede tener varias filas con (NULL, NULL), pero no puede tener varias filas con (3, NULL), al igual que no puede tener varias filas con (1, 100). Del mismo modo, no puede tener varias filas con (NULL, 300). El punto es que no puede tener varias filas con los mismos valores no NULL en las columnas únicas. En cuanto a una clave externa, puede tener cualquier cantidad de filas en la tabla de referencia con NULL en todas las columnas de referencia, independientemente de lo que exista en la tabla de referencia. Estas filas no están relacionadas con ninguna fila de la tabla a la que se hace referencia (relación opcional). Sin embargo, si tiene algún valor no NULL en cualquiera de las columnas de referencia, debe existir una fila en la tabla a la que se hace referencia con los mismos valores no NULL en las columnas a las que se hace referencia.

Suponga que tiene una base de datos en una plataforma que admite la restricción única estándar y necesita migrar esa base de datos a SQL Server. Puede enfrentar problemas con la aplicación de restricciones únicas en SQL Server si las columnas únicas admiten valores NULL. Los datos que se consideraban válidos en el sistema de origen pueden considerarse no válidos en SQL Server. En las siguientes secciones, exploraré una serie de posibles soluciones en SQL Server.

Solución 1, usar índice filtrado o vista indexada

Una solución común en T-SQL para hacer cumplir la funcionalidad de restricción única estándar cuando solo hay una columna de destino involucrada es usar un índice filtrado único que filtra solo las filas donde la columna de destino no es NULL. El siguiente código elimina la restricción única existente de T3 e implementa dicho índice:

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

Dado que el índice filtra solo las filas donde col1 no es NULL, su propiedad ÚNICA se aplica solo en los valores de col1 que no son NULL.

Recuerde que T3 ya tiene una fila con NULL en col1. Para probar esta solución, use el siguiente código para agregar una segunda fila con NULL en col1:

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

Este código se ejecuta correctamente.

Recuerde que T3 ya tiene una fila con el valor 1 en col1. Ejecute el siguiente código para intentar agregar una segunda fila con 1 en col1:

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

Como era de esperar, este intento falla con el siguiente error:

Mensaje 2601, Nivel 14, Estado 1
No se puede insertar una fila de clave duplicada en el objeto 'dbo.T3' con índice único 'idx_col1_notnull'. El valor de la clave duplicada es (1).

Utilice el siguiente código para consultar T3:

SELECT * FROM dbo.T3;

Este código genera el siguiente resultado que muestra dos filas con NULL en col1:

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

Esta solución funciona bien cuando necesita hacer cumplir la unicidad en una sola columna y cuando no necesita hacer cumplir la integridad referencial con una clave externa que apunte a esa columna.

El problema con la clave externa es que SQL Server requiere una clave principal o una restricción única o un índice único no filtrado definido en la columna a la que se hace referencia. No funciona cuando solo hay un índice filtrado único definido en la columna a la que se hace referencia. Intentemos crear una tabla con una clave externa que haga referencia a T3.col1. Primero, use el siguiente código para crear la tabla 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
);

Luego intente ejecutar el siguiente código para intentar agregar una clave externa que apunte desde T3FK.col1 a T3.col1:

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

Este intento falla con el siguiente error:

Mensaje 1776, Nivel 16, Estado 0
No hay claves primarias o candidatas en la tabla de referencia 'dbo.T3' que coincidan con la lista de columnas de referencia en la clave externa 'FK_T3_T3FK'.

Mensaje 1750, nivel 16, estado 1
No se pudo crear la restricción o el índice. Ver errores anteriores.

En este punto, suelte el índice filtrado existente para la limpieza:

DROP INDEX idx_col1_notnull ON dbo.T3;

No descarte la tabla T3FK, ya que la usará en ejemplos posteriores.

El otro problema con la solución de índice filtrado, suponiendo que no necesita una clave externa, es que no funciona cuando necesita aplicar la funcionalidad de restricción única estándar en varias columnas, por ejemplo, en la combinación (col1, col2) . Recuerde que la restricción única estándar no permite combinaciones duplicadas de valores que no sean NULL en las columnas únicas. Para implementar esta lógica con un índice filtrado, debe filtrar solo las filas en las que alguna de las columnas únicas no sea NULL. Dicho de otra manera, debe filtrar solo las filas que no tienen NULL en todas las columnas únicas. Desafortunadamente, los índices filtrados solo permiten expresiones muy simples. No admiten OR, NOT o manipulación en las columnas. Por lo tanto, actualmente no se admite ninguna de las siguientes definiciones de índice:

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 solución en tal caso es crear una vista indexada basada en una consulta que devuelve col1 y col2 de T3 con una de las cláusulas WHERE anteriores, con un índice agrupado único en (col1, col2), así:

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

Se le permitirá agregar varias filas con (NULL, NULL) en (col1, col2), pero no podrá agregar varias apariciones de combinaciones de valores que no sean NULL en (col1, col2), como (3 , NULL) o (NULL, 300) o (1, 100). Aún así, esta solución no admite una clave externa.

En este punto, ejecute el siguiente código para la limpieza:

DROP VIEW IF EXISTS dbo.T3CustomUnique;

Solución 2, usando clave sustituta y columna calculada

Las soluciones con el índice filtrado y la vista indexada son buenas siempre que no necesite admitir una clave externa. Pero, ¿qué sucede si necesita hacer cumplir la integridad referencial? Una opción es seguir usando el índice filtrado o la solución de vista indexada para hacer cumplir la unicidad y usar disparadores para hacer cumplir la integridad referencial. Sin embargo, esta opción es bastante cara.

Otra opción es usar una solución completamente diferente para la parte de unicidad que admite una clave externa. La solución consiste en agregar dos columnas a la tabla de referencia (T3 en nuestro caso). Una columna llamada id es una clave sustituta con una propiedad de identidad. Otra columna llamada flag es una columna calculada persistente que devuelve id cuando col1 es NULL y 0 cuando no es NULL. Luego aplica una restricción única en la combinación de col1 y flag. Aquí está el código para agregar las dos columnas y la restricción única:

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

Utilice el siguiente código para consultar T3:

SELECT * FROM dbo.T3;

Este código genera el siguiente resultado:

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

En cuanto a la tabla de referencia (T3FK en nuestro caso), agrega una columna calculada llamada bandera que siempre se establece en 0, y una clave externa definida en (col1, bandera) que apunta a las columnas únicas de T3 (col1, bandera), así :

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

Probemos esta solución.

Intente agregar las siguientes filas:

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

Estas filas se agregan con éxito, como deberían, ya que todas tienen filas de referencia correspondientes.

Consulta la tabla T3FK:

SELECT * FROM dbo.T3FK;

Obtienes el siguiente resultado:

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

Intente agregar una fila que no tenga una fila correspondiente en la tabla a la que se hace referencia:

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

El intento es rechazado, como debe ser, con el siguiente error:

Mensaje 547, nivel 16, estado 0
La declaración INSERT entró en conflicto con la restricción FOREIGN KEY "FK_T3_T3FK". El conflicto ocurrió en la base de datos "TSQLV5", tabla "dbo.T3".

Intente agregar una fila a T3FK con un NULL en col1:

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

Se considera que esta fila no está relacionada con ninguna fila en T3FK (relación opcional) y, de acuerdo con el estándar, debe permitirse independientemente de si existe un NULL en la tabla a la que se hace referencia en col1. T-SQL admite este escenario y la fila se agrega correctamente.

Consulta la tabla T3FK:

SELECT * FROM dbo.T3FK;

Este código genera el siguiente resultado:

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 solución funciona bien cuando necesita aplicar la funcionalidad de exclusividad estándar en una sola columna. Pero tiene un problema cuando necesita imponer la unicidad en varias columnas. Para demostrar el problema, primero suelte las tablas T3 y T3FK:

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

Use el siguiente código para recrear T3 con una restricción única compuesta en (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)
);

Tenga en cuenta que el indicador se establece en id cuando tanto col1 como col2 son NULL y 0 en caso contrario.

La restricción única en sí misma funciona bien.

Ejecute el siguiente código para agregar algunas filas a T3, incluidas varias apariciones de (NULL, NULL) en (col1, col2):

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

Estas filas se agregaron con éxito como deberían.

Intente agregar dos ocurrencias de (1, NULL) en (col1, col2):

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

Este intento falla como debería con el siguiente error:

Mensaje 2627, Nivel 14, Estado 1
Violación de la restricción de CLAVE ÚNICA 'UNQ_T3'. No se puede insertar una clave duplicada en el objeto 'dbo.T3'. El valor de la clave duplicada es (1, , 0).

Intente agregar dos ocurrencias de (NULL, 100) en (col1, col2):

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

Este intento también falla como debería con el siguiente error:

Mensaje 2627, Nivel 14, Estado 1
Violación de la restricción de CLAVE ÚNICA 'UNQ_T3'. No se puede insertar una clave duplicada en el objeto 'dbo.T3'. El valor de la clave duplicada es (, 100, 0).

Intente agregar las siguientes dos filas, donde no debería ocurrir ninguna infracción:

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

Estas filas se agregaron con éxito.

Consulta la tabla T3 en este punto:

SELECT * FROM dbo.T3;

Obtienes el siguiente resultado:

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

Hasta ahora todo bien.

A continuación, ejecute el siguiente código para crear la tabla T3FK con una clave externa compuesta que haga referencia a las columnas únicas de 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)
);

Esta solución, naturalmente, permite agregar filas a T3FK con (NULL, NULL) en (col1, col2). El problema es que también permite agregar filas NULL en col1 o col2, incluso cuando la otra columna no es NULL, y la tabla T3 a la que se hace referencia no tiene esa combinación de teclas. Por ejemplo, intente agregar la siguiente fila a T3FK:

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

Esta fila se agregó correctamente aunque no haya una fila relacionada en T3. De acuerdo con el estándar, esta fila no debe permitirse.

Volviendo a la mesa de dibujo...

Solución 3, usando clave sustituta y columna calculada

El problema con la solución anterior (Solución 2) surge cuando necesita admitir una clave externa compuesta. Permite filas en la tabla de referencia que tienen un NULL en la lista de una columna de referencia, incluso cuando hay valores que no son NULL en otras columnas de referencia y ninguna fila relacionada en la tabla de referencia. Para abordar esto, puede usar una variación de la solución anterior, que llamaremos Solución 3.

Primero, use el siguiente código para eliminar las tablas existentes:

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

En la nueva solución en la tabla a la que se hace referencia (T3 en nuestro caso), aún usa la columna de clave sustituta de identificación basada en identidad. También usa una columna calculada persistente llamada unqpath. Cuando todas las columnas únicas (col1 y col2 en nuestro ejemplo) son NULL, establece unqpath en una representación de cadena de caracteres de id (sin separadores ). Cuando alguna de las columnas únicas no es NULL, establece unqpath en una representación de cadena de caracteres de una lista separada de los valores de columna únicos mediante la función CONCAT. Esta función sustituye un NULL con una cadena vacía. Lo importante es asegurarse de usar un separador que normalmente no puede aparecer en los datos en sí. Por ejemplo, con los valores enteros col1 y col2 solo tiene dígitos, por lo que cualquier separador que no sea un dígito funcionaría. En mi ejemplo, usaré un punto (.). Luego aplica una restricción única en unqpath. Nunca tendrá un conflicto entre el valor de unqpath cuando todas las columnas únicas son NULL (establecidas en id) y cuando alguna de las columnas únicas no es NULL porque en el primer caso, unqpath no contiene un separador, y en el último caso sí. . Recuerde que utilizará la Solución 3 cuando tenga un caso de clave compuesta y probablemente prefiera la Solución 2, que es más simple, cuando tenga un caso de clave de una sola columna. Si desea usar la Solución 3 también con una clave de una sola columna y no la Solución 2, solo asegúrese de agregar el separador cuando la columna única no sea NULL, aunque solo haya un valor involucrado. De esta manera, no tendrá un conflicto cuando id en una fila donde col1 es NULL es igual a col1 en otra fila, ya que el primero no tendrá separador y el segundo sí.

Aquí está el código para crear T3 con las adiciones antes mencionadas:

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

Antes de tratar con una clave externa y la tabla de referencia, probemos la restricción única. Recuerde, se supone que debe prohibir combinaciones duplicadas de valores que no sean NULL en las columnas únicas, pero se supone que debe permitir múltiples apariciones de todos NULL en las columnas únicas.

Ejecute el siguiente código para agregar algunas filas, incluidas dos apariciones de (NULL, NULL) en (col1, col2):

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

Este código se completa con éxito como debería.

Intente agregar dos ocurrencias de (1, NULL) en (col1, col2):

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

Este código falla con el siguiente error como debería:

Mensaje 2627, Nivel 14, Estado 1
Violación de la restricción de CLAVE ÚNICA 'UNQ_T3'. No se puede insertar una clave duplicada en el objeto 'dbo.T3'. El valor de la clave duplicada es (1.).

Del mismo modo, también se rechaza el siguiente intento:

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

Obtiene el siguiente error:

Mensaje 2627, Nivel 14, Estado 1
Violación de la restricción de CLAVE ÚNICA 'UNQ_T3'. No se puede insertar una clave duplicada en el objeto 'dbo.T3'. El valor de la clave duplicada es (.100).

Ejecute el siguiente código para agregar un par de filas más:

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

Este código se ejecuta correctamente como debería.

En este punto, consulta T3:

SELECT * FROM dbo.T3;

Obtienes el siguiente resultado:

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

Observe los valores de unqpath y asegúrese de comprender la lógica detrás de su construcción y la diferencia entre un caso en el que todas las columnas únicas son NULL (sin separador) y cuando al menos una no es NULL (existe un separador).

En cuanto a la tabla de referencia, T3FK; también define una columna calculada llamada unqpath, pero en el caso de que todas las columnas de referencia sean NULL, establezca la columna en NULL, no en id. Cuando alguna de las columnas de referencia no es NULL, construye la misma lista separada de valores como lo hizo en T3. Luego define una clave externa en T3FK.unqpath que apunta a T3.unqpath, así:

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

Esta clave externa rechazará las filas en T3FK donde alguna de las columnas de referencia no sea NULL y no haya una fila relacionada en la tabla T3 a la que se hace referencia, como muestra el siguiente intento:

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

Este código genera el siguiente error:

Mensaje 547, nivel 16, estado 0
La declaración INSERT entró en conflicto con la restricción FOREIGN KEY "FK_T3_T3FK". El conflicto ocurrió en la base de datos "TSQLV5", tabla "dbo.T3", columna 'unqpath'.

Esta solución incluirá filas en T3FK donde alguna de las columnas de referencia no sea NULL siempre que exista una fila relacionada en T3, así como filas con NULL en todas las columnas de referencia, ya que se considera que dichas filas no están relacionadas con ninguna fila en T3. El siguiente código agrega tales filas válidas 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');

Este código se completa con éxito.

Ejecute el siguiente código para consultar T3FK:

SELECT * FROM dbo.T3FK;

Obtienes el siguiente resultado:

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

Así que tomó un poco de creatividad, pero ahora tiene una solución para la restricción única estándar, incluida la compatibilidad con claves externas.

Conclusión

Podría pensar que una restricción única es una característica sencilla, pero puede ser un poco complicado cuando necesita admitir valores NULL en las columnas únicas. Se vuelve más complejo cuando necesita implementar la funcionalidad de restricción única estándar en T-SQL, ya que los dos usan reglas diferentes en términos de cómo manejan los valores NULL. En este artículo, expliqué la diferencia entre los dos y brindé soluciones que funcionan en T-SQL. Puede usar un índice filtrado simple cuando necesite imponer la exclusividad en una sola columna que admite valores NULL y no necesite admitir una clave externa que haga referencia a esa columna. Sin embargo, si necesita admitir una clave externa o una restricción única compuesta con la funcionalidad estándar, necesitará una implementación más compleja con una clave sustituta y una columna calculada.