Database
 sql >> Base de Dados >  >> RDS >> Database

Complexidades NULL - Parte 4, Restrição exclusiva padrão ausente


Este artigo é a Parte 4 de uma série sobre complexidades NULL. Nos artigos anteriores (Parte 1, Parte 2 e Parte 3), abordei o significado do NULL como um marcador para um valor ausente, como os NULLs se comportam em comparações e em outros elementos de consulta e os recursos padrão de manipulação de NULL que não são ainda disponível em T-SQL. Este mês abordo a diferença entre a forma como uma restrição exclusiva é definida no padrão ISO/IEC SQL e a forma como ela funciona no T-SQL. Também fornecerei soluções personalizadas que você pode implementar se precisar da funcionalidade padrão.

Restrição ÚNICA padrão


O SQL Server trata NULLs como valores não NULL com a finalidade de impor uma restrição exclusiva. Ou seja, uma restrição exclusiva em T é satisfeita se e somente se não existirem duas linhas R1 e R2 de T tais que R1 e R2 tenham a mesma combinação de valores NULLs e não NULL nas colunas exclusivas. Por exemplo, suponha que você defina uma restrição exclusiva em col1, que é uma coluna NULLable de um tipo de dados INT. Uma tentativa de modificar a tabela de uma forma que resulte em mais de uma linha com um NULL em col1 será rejeitada, assim como uma modificação que resultaria em mais de uma linha com o valor 1 em col1 será rejeitada.

Suponha que você defina uma restrição exclusiva composta na combinação das colunas INT NULLable col1 e col2. Uma tentativa de modificar a tabela de forma que resulte em mais de uma ocorrência de qualquer uma das seguintes combinações de valores (col1, col2) será rejeitada:(NULL, NULL), (3, NULL), (NULL, 300 ), (1, 100).

Portanto, como você pode ver, a implementação T-SQL da restrição exclusiva trata NULLs como valores não NULL para fins de imposição de exclusividade.

Se você deseja definir uma chave estrangeira em alguma tabela X referenciando alguma tabela Y, você deve impor exclusividade na(s) coluna(s) referenciada(s) com uma das seguintes opções:
  • Chave primária
  • Restrição exclusiva
  • Índice exclusivo não filtrado

Uma chave primária não é permitida em colunas NULLable. Tanto uma restrição exclusiva (que cria um índice oculto) quanto um índice exclusivo criado explicitamente são permitidos em colunas NULLable e impõem sua exclusividade no T-SQL usando a lógica mencionada acima. A tabela de referência pode ter linhas com um NULL na coluna de referência, independentemente de a tabela referenciada ter uma linha com um NULL na coluna referenciada. A ideia é apoiar um relacionamento opcional. Algumas linhas na tabela de referência podem ser aquelas que não estão relacionadas a nenhuma linha na tabela referenciada. Você implementará isso usando um NULL na coluna de referência.

Para demonstrar a implementação T-SQL de uma restrição exclusiva, execute o código a seguir, que cria uma tabela chamada T3 com uma restrição exclusiva definida na coluna NULLable INT col1 e a preenche com algumas linhas de amostra:
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 o seguinte código para consultar a tabela:
SELECT * FROM dbo.T3;

Essa consulta gera a seguinte saída:
col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Tente inserir uma segunda linha com um NULL em col1:
INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);

Esta tentativa é rejeitada e você recebe o seguinte erro:
Msg 2627, Level 14, State 1
Violação da restrição UNIQUE KEY 'UNQ_T3'. Não é possível inserir a chave duplicada no objeto 'dbo.T3'. O valor da chave duplicada é ().
A definição de restrição exclusiva padrão é um pouco diferente da versão T-SQL. A principal diferença tem a ver com o tratamento NULL. Aqui está a definição de restrição exclusiva do padrão:

“Uma restrição única em T é satisfeita se e somente se não existirem duas linhas R1 e R2 de T tais que R1 e R2 tenham os mesmos valores não NULL nas colunas únicas.”

Portanto, uma tabela T com uma restrição exclusiva em col1 permitirá várias linhas com um NULL em col1, mas não permitirá várias linhas com o mesmo valor não NULL em col1.

O que é um pouco mais complicado de explicar é o que acontece de acordo com o padrão com uma restrição única composta. Digamos que você tenha uma restrição exclusiva definida em (col1, col2). Você pode ter várias linhas com (NULL, NULL), mas não pode ter várias linhas com (3, NULL), assim como não pode ter várias linhas com (1, 100). Da mesma forma, você não pode ter várias linhas com (NULL, 300). O ponto é que você não tem permissão para ter várias linhas com os mesmos valores não NULL nas colunas exclusivas. Quanto a uma chave estrangeira, você pode ter qualquer número de linhas na tabela de referência com NULLs em todas as colunas de referência, independentemente do que existe na tabela referenciada. Essas linhas não estão relacionadas a nenhuma linha na tabela referenciada (relação opcional). No entanto, se você tiver qualquer valor não NULL em qualquer uma das colunas de referência, deve existir uma linha na tabela referenciada com os mesmos valores não NULL nas colunas referenciadas.

Suponha que você tenha um banco de dados em uma plataforma que dê suporte à restrição exclusiva padrão e precise migrar esse banco de dados para o SQL Server. Você pode enfrentar problemas com a imposição de restrições exclusivas no SQL Server se as colunas exclusivas oferecerem suporte a NULLs. Dados considerados válidos no sistema de origem podem ser considerados inválidos no SQL Server. Nas seções a seguir, explorarei várias soluções alternativas possíveis no SQL Server.

Solução 1, usando índice filtrado ou visualização indexada


Uma solução comum no T-SQL para impor a funcionalidade de restrição exclusiva padrão quando há apenas uma coluna de destino envolvida é usar um índice filtrado exclusivo que filtra apenas as linhas em que a coluna de destino não é NULL. O código a seguir elimina a restrição exclusiva existente de T3 e implementa esse í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;

Como o índice filtra apenas as linhas em que col1 não é NULL, sua propriedade UNIQUE é aplicada apenas nos valores col1 não NULL.

Lembre-se de que T3 já possui uma linha com NULL em col1. Para testar essa solução, use o código a seguir para adicionar uma segunda linha com um NULL em col1:
INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);

Este código é executado com sucesso.

Lembre-se de que T3 já possui uma linha com o valor 1 em col1. Execute o seguinte código para tentar adicionar uma segunda linha com 1 em col1:
INSERT INTO dbo.T3(col1, col2) VALUES(1, 500);

Como esperado, esta tentativa falha com o seguinte erro:
Msg 2601, Level 14, State 1
Não é possível inserir linha de chave duplicada no objeto 'dbo.T3' com índice exclusivo 'idx_col1_notnull'. O valor da chave duplicada é (1).
Use o seguinte código para consultar T3:
SELECT * FROM dbo.T3;

Este código gera a seguinte saída mostrando duas linhas com um NULL em col1:
col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300
NULL        400

Essa solução funciona bem quando você precisa impor exclusividade em apenas uma coluna e quando não precisa impor integridade referencial com uma chave estrangeira apontando para essa coluna.

O problema com a chave estrangeira é que o SQL Server requer uma chave primária ou uma restrição exclusiva ou um índice exclusivo não filtrado definido na coluna referenciada. Não funciona quando há apenas um índice filtrado exclusivo definido na coluna referenciada. Vamos tentar criar uma tabela com uma chave estrangeira referenciando T3.col1. Primeiro, use o seguinte código para criar a tabela 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
);

Em seguida, tente executar o seguinte código na tentativa de adicionar uma chave estrangeira apontando de T3FK.col1 para T3.col1:
ALTER TABLE dbo.T3FK ADD CONSTRAINT FK_T3_T3FK
  FOREIGN KEY(col1) REFERENCES dbo.T3(col1);

Esta tentativa falha com o seguinte erro:
Msg 1776, Level 16, State 0
Não há chaves primárias ou candidatas na tabela referenciada 'dbo.T3' que correspondam à lista de colunas de referência na chave estrangeira 'FK_T3_T3FK'.

Msg 1750, Level 16, State 1
Não foi possível criar restrição ou índice. Veja os erros anteriores.

Neste ponto, elimine o índice filtrado existente para limpeza:
DROP INDEX idx_col1_notnull ON dbo.T3;

Não descarte a tabela T3FK, pois você a usará em exemplos posteriores.

O outro problema com a solução de índice filtrado, supondo que você não precise de uma chave estrangeira, é que ela não funciona quando você precisa impor a funcionalidade de restrição exclusiva padrão em várias colunas, por exemplo, na combinação (col1, col2) . Lembre-se de que a restrição exclusiva padrão não permite combinações duplicadas não NULL de valores nas colunas exclusivas. Para implementar essa lógica com um índice filtrado, você precisa filtrar apenas as linhas em que qualquer uma das colunas exclusivas não seja NULL. Em outras palavras, você precisa filtrar apenas as linhas que não têm NULLs em todas as colunas exclusivas. Infelizmente, índices filtrados permitem apenas expressões muito simples. Eles não suportam OR, NOT ou manipulação nas colunas. Portanto, nenhuma das seguintes definições de índice tem suporte no momento:
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;

A solução alternativa nesse caso é criar uma exibição indexada com base em uma consulta que retorna col1 e col2 de T3 com uma das cláusulas WHERE acima, com um índice clusterizado exclusivo em (col1, col2), assim:
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

Você poderá adicionar várias linhas com (NULL, NULL) em (col1, col2), mas não poderá adicionar várias ocorrências de combinações de valores não NULL em (col1, col2), como (3 , NULL) ou (NULL, 300) ou (1, 100). Ainda assim, esta solução não suporta uma chave estrangeira.

Neste ponto, execute o seguinte código para limpeza:
DROP VIEW IF EXISTS dbo.T3CustomUnique;

Solução 2, usando chave substituta e coluna computada


As soluções com o índice filtrado e a visualização indexada são boas desde que você não precise dar suporte a uma chave estrangeira. Mas e se você precisar impor a integridade referencial? Uma opção é continuar usando o índice filtrado ou a solução de exibição indexada para impor exclusividade e usar gatilhos para impor a integridade referencial. No entanto, esta opção é bastante cara.

Outra opção é usar uma solução completamente diferente para a parte de exclusividade que suporta uma chave estrangeira. A solução envolve adicionar duas colunas à tabela referenciada (T3 no nosso caso). Uma coluna chamada id é uma chave substituta com uma propriedade de identidade. Outra coluna chamada flag é uma coluna computada persistente que retorna id quando col1 é NULL e 0 quando não é NULL. Em seguida, você impõe uma restrição exclusiva na combinação de col1 e sinalizador. Aqui está o código para adicionar as duas colunas e a restrição exclusiva:
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);

Use o seguinte código para consultar T3:
SELECT * FROM dbo.T3;

Este código gera a seguinte saída:
col1        col2        id          flag
----------- ----------- ----------- -----------
1           100         1           0
2           -1          2           0
NULL        -1          3           3
3           300         4           0
NULL        400         5           5

Quanto à tabela de referência (T3FK no nosso caso), você adiciona uma coluna computada chamada flag que é sempre definida como 0 e uma chave estrangeira definida em (col1, flag) apontando para as colunas exclusivas de T3 (col1, flag), assim :
ALTER TABLE dbo.T3FK
  ADD flag AS 0 PERSISTED,
      CONSTRAINT FK_T3_T3FK
        FOREIGN KEY(col1, flag) REFERENCES dbo.T3(col1, flag);

Vamos testar esta solução.

Tente adicionar as seguintes linhas:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES
  (1, 100, 'A'),
  (2, -1, 'B'),
  (3, 300, 'C');

Essas linhas são adicionadas com sucesso, como deveriam, pois todas têm linhas referenciadas correspondentes.

Consulte a tabela T3FK:
SELECT * FROM dbo.T3FK;

Você obtém a seguinte saída:
id          col1        col2        othercol   flag
----------- ----------- ----------- ---------- -----------
1           1           100         A          0
2           2           -1          B          0
3           3           300         C          0

Tente adicionar uma linha que não tenha uma linha correspondente na tabela referenciada:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES
  (4, 400, 'D');

A tentativa é rejeitada, como deveria ser, com o seguinte erro:
Msg 547, Level 16, State 0
A instrução INSERT entrou em conflito com a restrição FOREIGN KEY "FK_T3_T3FK". O conflito ocorreu no banco de dados "TSQLV5", tabela "dbo.T3".
Tente adicionar uma linha ao T3FK com um NULL em col1:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES
  (NULL, NULL, 'E');

Esta linha é considerada como não relacionada a nenhuma linha em T3FK (relação opcional) e, de acordo com o padrão, deve ser permitida independentemente de existir um NULL na tabela referenciada em col1. O T-SQL oferece suporte a esse cenário e a linha é adicionada com êxito.

Consulte a tabela T3FK:
SELECT * FROM dbo.T3FK;

Este código gera a seguinte saída:
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

A solução funciona bem quando você precisa impor a funcionalidade de exclusividade padrão em uma única coluna. Mas há um problema quando você precisa impor exclusividade em várias colunas. Para demonstrar o problema, primeiro elimine as tabelas T3 e T3FK:
DROP TABLE IF EXISTS dbo.T3FK, dbo.T3;

Use o código a seguir para recriar T3 com uma restrição exclusiva composta em (col1, col2, sinalizador):
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)
);

Observe que o sinalizador é definido como id quando col1 e col2 são NULLs e 0 caso contrário.

A restrição exclusiva em si funciona bem.

Execute o código a seguir para adicionar algumas linhas a T3, incluindo várias ocorrências de (NULL, NULL) em (col1, col2):
INSERT INTO dbo.T3(col1, col2) VALUES(1, 100),(1, 200),(NULL, NULL),(NULL, NULL);

Essas linhas são adicionadas com sucesso como deveriam.

Tente adicionar duas ocorrências de (1, NULL) em (col1, col2):
INSERT INTO dbo.T3(col1, col2) VALUES(1, NULL),(1, NULL);

Esta tentativa falha como deveria com o seguinte erro:
Msg 2627, Level 14, State 1
Violação da restrição UNIQUE KEY 'UNQ_T3'. Não é possível inserir a chave duplicada no objeto 'dbo.T3'. O valor da chave duplicada é (1, , 0).
Tente adicionar duas ocorrências de (NULL, 100) em (col1, col2):
INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 100),(NULL, 100);

Essa tentativa também falha como deveria com o seguinte erro:
Msg 2627, Level 14, State 1
Violação da restrição UNIQUE KEY 'UNQ_T3'. Não é possível inserir a chave duplicada no objeto 'dbo.T3'. O valor da chave duplicada é (, 100, 0).
Tente adicionar as duas linhas a seguir, onde nenhuma violação deve ocorrer:
INSERT INTO dbo.T3(col1, col2) VALUES(3, NULL),(NULL, 300);

Essas linhas são adicionadas com sucesso.

Consulte a tabela T3 neste momento:
SELECT * FROM dbo.T3;

Você obtém a seguinte saída:
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

Até agora tudo bem.

Em seguida, execute o seguinte código para criar a tabela T3FK com uma chave estrangeira composta referenciando as colunas exclusivas 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 solução naturalmente permite adicionar linhas ao T3FK com (NULL, NULL) em (col1, col2). O problema é que também permite adicionar linhas NULL em col1 ou col2, mesmo quando a outra coluna não é NULL e a tabela referenciada T3 não possui essa combinação de teclas. Por exemplo, tente adicionar a seguinte linha ao T3FK:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES(5, NULL, 'A');

Esta linha é adicionada com sucesso mesmo que não haja nenhuma linha relacionada em T3. De acordo com a norma, esta linha não deve ser permitida.

De volta à prancheta…

Solução 3, usando chave substituta e coluna computada


O problema com a solução anterior (Solução 2) surge quando você precisa dar suporte a uma chave estrangeira composta. Ele permite linhas na tabela de referência que tenham um NULL na lista de uma coluna de referência, mesmo quando houver valores não NULL em outras colunas de referência e nenhuma linha relacionada na tabela referenciada. Para resolver isso, você pode usar uma variação da solução anterior, que chamaremos de Solução 3.

Primeiro, use o seguinte código para descartar as tabelas existentes:
DROP TABLE IF EXISTS dbo.T3FK, dbo.T3;

Na nova solução na tabela referenciada (T3 em nosso caso), você ainda usa a coluna de chave substituta de ID baseada em identidade. Você também usa uma coluna computada persistente chamada unqpath. Quando todas as colunas exclusivas (col1 e col2 em nosso exemplo) são NULL, você define unqpath como uma representação de cadeia de caracteres de id (sem separadores ). Quando qualquer uma das colunas exclusivas não for NULL, você define unqpath como uma representação de cadeia de caracteres de uma lista separada dos valores de coluna exclusivos usando a função CONCAT. Esta função substitui um NULL por uma string vazia. O importante é certificar-se de usar um separador que normalmente não pode aparecer nos próprios dados. Por exemplo, com valores inteiros col1 e col2 você tem apenas dígitos, então qualquer separador que não seja um dígito funcionaria. No meu exemplo vou usar um ponto (.). Em seguida, você impõe uma restrição exclusiva no unqpath. Você nunca terá um conflito entre o valor unqpath quando todas as colunas exclusivas forem NULL (definidas como id) versus quando qualquer uma das colunas exclusivas não for NULL porque no primeiro caso unqpath não contém um separador e, no último caso, contém . Lembre-se de que você usará a Solução 3 quando tiver um caso de chave composto e provavelmente preferirá a Solução 2, que é mais simples, quando tiver um caso de chave de coluna única. Se você quiser usar a Solução 3 também com uma chave de coluna única e não a Solução 2, apenas certifique-se de adicionar o separador quando a coluna exclusiva não for NULL, mesmo que haja apenas um valor envolvido. Dessa forma você não terá conflito quando id em uma linha onde col1 for NULL for igual a col1 em outra linha, pois a primeira não terá separador e a segunda terá.

Aqui está o código para criar T3 com as adições acima 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 lidar com uma chave estrangeira e a tabela de referência, vamos testar a restrição exclusiva. Lembre-se, é suposto não permitir combinações duplicadas de valores não NULL nas colunas exclusivas, mas deve permitir várias ocorrências de todos os NULLs nas colunas exclusivas.

Execute o seguinte código para adicionar algumas linhas, incluindo duas ocorrências de (NULL, NULL) em (col1, col2):
INSERT INTO dbo.T3(col1, col2) VALUES(1, 100),(1, 200),(NULL, NULL),(NULL, NULL);

Este código é concluído com êxito como deveria.

Tente adicionar duas ocorrências de (1, NULL) em (col1, col2):
INSERT INTO dbo.T3(col1, col2) VALUES(1, NULL),(1, NULL);

Este código falha com o seguinte erro como deveria:
Msg 2627, Level 14, State 1
Violação da restrição UNIQUE KEY 'UNQ_T3'. Não é possível inserir a chave duplicada no objeto 'dbo.T3'. O valor da chave duplicada é (1.).
Da mesma forma, a seguinte tentativa também é rejeitada:
INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 100),(NULL, 100);

Você recebe o seguinte erro:
Msg 2627, Level 14, State 1
Violação da restrição UNIQUE KEY 'UNQ_T3'. Não é possível inserir a chave duplicada no objeto 'dbo.T3'. O valor da chave duplicada é (.100).
Execute o seguinte código para adicionar mais algumas linhas:
INSERT INTO dbo.T3(col1, col2) VALUES(3, NULL),(NULL, 300);

Este código é executado com sucesso como deveria.

Neste ponto, consulte T3:
SELECT * FROM dbo.T3;

Você obtém a seguinte saída:
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 os valores unqpath e certifique-se de entender a lógica por trás de sua construção e a diferença entre um caso em que todas as colunas exclusivas são NULL (sem separador) versus quando pelo menos uma não é NULL (existe separador).

Quanto à tabela de referência, T3FK; você também define uma coluna computada chamada unqpath, mas no caso em que todas as colunas de referência são NULL, você define a coluna como NULL—não como id. Quando qualquer uma das colunas de referência não é NULL, você constrói a mesma lista separada de valores como fez em T3. Você então define uma chave estrangeira em T3FK.unqpath apontando para T3.unqpath, assim:
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)
);

Essa chave estrangeira rejeitará linhas em T3FK onde qualquer uma das colunas de referência não for NULL e não houver linha relacionada na tabela referenciada T3, como mostra a seguinte tentativa:
INSERT INTO dbo.T3FK(col1, col2, othercol) VALUES(5, NULL, 'A');

Este código gera o seguinte erro:
Msg 547, Level 16, State 0
A instrução INSERT entrou em conflito com a restrição FOREIGN KEY "FK_T3_T3FK". O conflito ocorreu no banco de dados "TSQLV5", tabela "dbo.T3", coluna 'unqpath'.
Esta solução irá linhas em T3FK onde qualquer uma das colunas de referência não for NULL, desde que exista uma linha relacionada em T3, bem como linhas com NULLs em todas as colunas de referência, uma vez que tais linhas são consideradas não relacionadas a nenhuma linha em T3. O código a seguir adiciona essas linhas válidas ao 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 é concluído com sucesso.

Execute o seguinte código para consultar o T3FK:
SELECT * FROM dbo.T3FK;

Você obtém a seguinte saída:
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

Portanto, foi preciso um pouco de criatividade, mas agora você tem uma solução alternativa para a restrição exclusiva padrão, incluindo suporte a chave estrangeira.

Conclusão


Você pensaria que uma restrição exclusiva é um recurso simples, mas pode ser um pouco complicado quando você precisa dar suporte a NULLs nas colunas exclusivas. Fica mais complexo quando você precisa implementar a funcionalidade de restrição exclusiva padrão no T-SQL, pois os dois usam regras diferentes em termos de como lidam com NULLs. Neste artigo, expliquei a diferença entre os dois e forneci soluções alternativas que funcionam no T-SQL. Você pode usar um índice filtrado simples quando precisar impor exclusividade em apenas uma coluna NULLable e não precisar oferecer suporte a uma chave estrangeira que faça referência a essa coluna. No entanto, se você precisar dar suporte a uma chave estrangeira ou uma restrição exclusiva composta com a funcionalidade padrão, precisará de uma implementação mais complexa com uma chave substituta e uma coluna computada.