Como crio uma restrição exclusiva que também permite nulos?

620

Eu quero ter uma restrição exclusiva em uma coluna que eu vou preencher com GUIDs. No entanto, meus dados contêm valores nulos para essas colunas. Como crio a restrição que permite vários valores nulos?

Aqui está um exemplo de cenário . Considere este esquema:

CREATE TABLE People (
  Id INT CONSTRAINT PK_MyTable PRIMARY KEY IDENTITY,
  Name NVARCHAR(250) NOT NULL,
  LibraryCardId UNIQUEIDENTIFIER NULL,
  CONSTRAINT UQ_People_LibraryCardId UNIQUE (LibraryCardId)
)

Então veja este código para o que estou tentando alcançar:

-- This works fine:
INSERT INTO People (Name, LibraryCardId) 
 VALUES ('John Doe', 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA');

-- This also works fine, obviously:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Marie Doe', 'BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB');

-- This would *correctly* fail:
--INSERT INTO People (Name, LibraryCardId) 
--VALUES ('John Doe the Second', 'AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA');

-- This works fine this one first time:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Richard Roe', NULL);

-- THE PROBLEM: This fails even though I'd like to be able to do this:
INSERT INTO People (Name, LibraryCardId) 
VALUES ('Marcus Roe', NULL);

A declaração final falha com uma mensagem:

Violação da restrição UNIQUE KEY 'UQ_People_LibraryCardId'. Não é possível inserir chave duplicada no objeto 'dbo.People'.

Como posso alterar meu esquema e / ou restrição de exclusividade para permitir vários NULLvalores, enquanto continuo verificando a exclusividade nos dados reais?

Stuart
fonte
Problema de conexão para compatibilidade padrão para votar em: connect.microsoft.com/SQLServer/Feedback/Details/299229
Vadzim
Possível duplicata de Como criar um índice exclusivo em uma coluna NULL?
Frédéric
Restrição ÚNICA e permitir NULLs. ? É senso comum. Não é possível
flik
13
@ Flik, ​​melhor não se referir ao "bom senso". Esse não é um argumento válido. Especialmente quando se considera que nullnão é um valor, mas a ausência de valor. De acordo com o padrão SQL, nullnão é considerado igual a null. Então, por que o múltiplo nulldeve ser uma violação de exclusividade?
Frédéric

Respostas:

144

SQL Server 2008 +

Você pode criar um índice exclusivo que aceite vários NULLs com uma WHEREcláusula. Veja a resposta abaixo .

Antes do SQL Server 2008

Você não pode criar uma restrição UNIQUE e permitir NULLs. Você precisa definir um valor padrão de NEWID ().

Atualize os valores existentes para NEWID () onde NULL antes de criar a restrição UNIQUE.

Jose Basilio
fonte
2
e isso adicionará valores retrospectivamente às linhas existentes; se for, é isso que preciso fazer, obrigado?
Stuart
1
Você precisará executar uma instrução UPDATE para definir os valores existentes para NEWID () onde o campo existente IS NULL
Jose Basilio
54
Se você estiver usando o SQL Server 2008 ou posterior, consulte a resposta abaixo com mais de 100 votos positivos. Você pode adicionar uma cláusula WHERE à sua restrição exclusiva.
Darren Griffith
1
Esse problema também afeta o ADO.NET DataTables. Assim, mesmo que eu possa permitir nulos no campo de suporte usando esse método, o DataTable não permitirá que eu armazene NULLs em uma coluna exclusiva em primeiro lugar. Se alguém souber uma solução para isso, poste-o aqui
dotNET 2/15
6
Caras, certifique-se de rolar para baixo e ler a resposta com 600 votos positivos. Já não é pouco mais de 100.
Luminous
1288

O que você está procurando é de fato parte dos padrões ANSI SQL: 92, SQL: 1999 e SQL: 2003, ou seja, uma restrição UNIQUE deve proibir valores duplicados não NULL, mas aceitar vários valores NULL.

No mundo da Microsoft do SQL Server, no entanto, um único NULL é permitido, mas vários NULLs não são ...

No SQL Server 2008 , você pode definir um índice filtrado exclusivo com base em um predicado que exclui NULLs:

CREATE UNIQUE NONCLUSTERED INDEX idx_yourcolumn_notnull
ON YourTable(yourcolumn)
WHERE yourcolumn IS NOT NULL;

Nas versões anteriores, você pode recorrer ao VIEWS com um predicado NOT NULL para impor a restrição.

Vincent Buck
fonte
3
essa é provavelmente a melhor maneira de fazer isso. não tem certeza se há algum impacto no desempenho? alguém?
22610 Simon Ontem
3
Estou tentando fazer exatamente isso na edição do SQL Server 2008 Express e recebo um erro da seguinte maneira: CRIAR ÍNDICE NÃO EXCLUSIVO EXCLUSIVO UC_MailingId ON [SLS-CP] .dbo.MasterFileEntry (MailingId) ONDE MailingId NÃO É NULL Resultados em: Msg 156, Nível 15, estado 1, linha 3 Sintaxe incorreta perto da palavra-chave 'WHERE'. Se eu remover a cláusula where, a DDL funciona bem, mas é claro, não faz o que eu preciso. Alguma ideia?
Kenneth Baltrinic
4
A menos que eu esteja enganado, você não pode criar uma Chave Estrangeira a partir de um Índice Único, como em uma Restrição Exclusiva. (Pelo menos o SSMS reclamou comigo quando tentei.) Seria bom poder ter uma coluna anulável que seja sempre única (quando não nula) seja a fonte de um relacionamento de Chave Estrangeira.
Vaccano
8
Verdadeiramente uma ótima resposta. Pena que foi escondido por quem aceitou como resposta. Essa solução quase não chamou minha atenção, mas funciona como maravilhas na minha implementação agora.
Coral Doe
2
Outra alternativa para o SQL 2005 e abaixo é um truque de Coluna Computada, também conhecido como "Nullbuster". stackoverflow.com/a/191729/132461 Isso evita que você atrapalhe o banco de dados com outra visualização; em vez disso, você tem outra coluna - geralmente denominada ColumnA-Nullbuster se ColumnA for a que você deseja que seja ANSI ANULÁVEL UNIQUE. Coloque um índice exclusivo (ou restrição para expressar a intenção de negócios) em ColumnA-Nullbuster e vai impor exclusividade em ColumnA
Dano
34

SQL Server 2008 e superior

Basta filtrar um índice exclusivo:

CREATE UNIQUE NONCLUSTERED INDEX UQ_Party_SamAccountName
ON dbo.Party(SamAccountName)
WHERE SamAccountName IS NOT NULL;

Nas versões inferiores, uma exibição materializada ainda não é necessária

Para o SQL Server 2005 e versões anteriores, você pode fazer isso sem uma exibição. Acabei de adicionar uma restrição única, como você está pedindo em uma das minhas tabelas. Dado que desejo exclusividade na coluna SamAccountName, mas desejo permitir vários NULLs, usei uma coluna materializada em vez de uma exibição materializada:

ALTER TABLE dbo.Party ADD SamAccountNameUnique
   AS (Coalesce(SamAccountName, Convert(varchar(11), PartyID)))
ALTER TABLE dbo.Party ADD CONSTRAINT UQ_Party_SamAccountName
   UNIQUE (SamAccountNameUnique)

Você simplesmente precisa colocar algo na coluna computada que será garantida exclusiva em toda a tabela quando a coluna exclusiva desejada real for NULL. Nesse caso, PartyIDé uma coluna de identidade e ser numérico nunca corresponderá a nenhum SamAccountName, portanto funcionou para mim. Você pode tentar seu próprio método - certifique-se de entender o domínio de seus dados para que não haja possibilidade de interseção com dados reais. Isso pode ser tão simples quanto adicionar um caractere diferenciador como este:

Coalesce('n' + SamAccountName, 'p' + Convert(varchar(11), PartyID))

Mesmo que PartyIDum dia se torne não numérico e possa coincidir com um SamAccountName, agora não importa.

Observe que a presença de um índice incluindo a coluna computada faz com que cada resultado de expressão seja salvo no disco com os outros dados da tabela, o que ocupa espaço em disco adicional.

Observe que, se você não quiser um índice, ainda poderá salvar a CPU, fazendo com que a expressão seja pré-calculada em disco, adicionando a palavra PERSISTED- chave ao final da definição da expressão da coluna.

No SQL Server 2008 e versões posteriores, use definitivamente a solução filtrada, se possível!

Controvérsia

Observe que alguns profissionais de banco de dados verão isso como um caso de "NULLs substitutos", que definitivamente apresentam problemas (principalmente devido a problemas ao tentar determinar quando algo é um valor real ou um valor substituto para a falta de dados ; também pode haver problemas com o número de valores substitutos que não sejam NULL multiplicando-se como loucos).

No entanto, acredito que este caso seja diferente. A coluna computada que estou adicionando nunca será usada para determinar nada. Ele não tem significado próprio e não codifica nenhuma informação que ainda não foi encontrada separadamente em outras colunas definidas corretamente. Nunca deve ser selecionado ou usado.

Então, minha história é que esse não é um NULL substituto, e estou cumprindo! Como na verdade, não queremos que o valor não-NULL seja para outra finalidade além de enganar o UNIQUEíndice para ignorar NULLs, nosso caso de uso não apresenta nenhum dos problemas que surgem com a criação normal de NULL substituta.

Tudo isso dito, não tenho nenhum problema em usar uma exibição indexada - mas isso traz alguns problemas, como a exigência de uso SCHEMABINDING. Divirta-se adicionando uma nova coluna à sua tabela base (você precisará, no mínimo, soltar o índice e, em seguida, soltar a visualização ou alterar a visualização para não ser vinculada ao esquema). Consulte a lista completa (longa) de requisitos para criar uma exibição indexada no SQL Server (2005) (também versões posteriores), (2000) .

Atualizar

Se sua coluna for numérica, pode haver o desafio de garantir que a restrição exclusiva usando Coalescenão resulte em colisões. Nesse caso, existem algumas opções. Pode-se usar um número negativo, colocar os "NULL substitutos" somente na faixa negativa e os "valores reais" somente na faixa positiva. Como alternativa, o seguinte padrão pode ser usado. Na tabela Issue(onde IssueIDestá o PRIMARY KEY), pode ou não haver um TicketID, mas se houver, ele deve ser único.

ALTER TABLE dbo.Issue ADD TicketUnique
   AS (CASE WHEN TicketID IS NULL THEN IssueID END);
ALTER TABLE dbo.Issue ADD CONSTRAINT UQ_Issue_Ticket_AllowNull
   UNIQUE (TicketID, TicketUnique);

Se o IssueID 1 tiver o ticket 123, a UNIQUErestrição estará nos valores (123, NULL). Se o IssueID 2 não tiver um ticket, ele estará ativado (NULL, 2). Alguns pensam que mostrarão que essa restrição não pode ser duplicada para nenhuma linha da tabela e ainda permite vários NULLs.

ErikE
fonte
16

Para pessoas que estão usando o Microsoft SQL Server Manager e desejam criar um índice Único, mas que pode ser anulado, você pode criar seu índice exclusivo, como faria normalmente nas Propriedades do Índice para o seu novo índice, selecione "Filtro" no painel esquerdo e digite seu filtro (que é sua cláusula where). Deve ler algo como isto:

([YourColumnName] IS NOT NULL)

Isso funciona com o MSSQL 2012

Howard
fonte
Como criar um índice filtrado no Microsoft SQL Server Management Studio é descrito aqui e funciona perfeitamente: msdn.microsoft.com/en-us/library/cc280372.aspx
Jan
9

Quando apliquei o índice exclusivo abaixo:

CREATE UNIQUE NONCLUSTERED INDEX idx_badgeid_notnull
ON employee(badgeid)
WHERE badgeid IS NOT NULL;

todas as atualizações e inserções não nulas falharam com o erro abaixo:

UPDATE falhou porque as seguintes opções de SET possuem configurações incorretas: 'ARITHABORT'.

Encontrei isso no MSDN

SET ARITHABORT deve estar LIGADO quando você estiver criando ou alterando índices em colunas computadas ou visualizações indexadas. Se SET ARITHABORT estiver desativado, as instruções CREATE, UPDATE, INSERT e DELETE em tabelas com índices em colunas computadas ou visualizações indexadas falharão.

Então, para que isso funcione corretamente, eu fiz isso

Clique com o botão direito do mouse em [Banco de Dados] -> Propriedades -> Opções -> Outras Opções -> Diversos -> Interrupção Aritmética Ativada -> true

Eu acredito que é possível definir esta opção no código usando

ALTER DATABASE "DBNAME" SET ARITHABORT ON

mas eu não testei isso

Mike Taylor
fonte
6

Crie uma exibição que selecione apenas não NULLcolunas e crie a UNIQUE INDEXna exibição:

CREATE VIEW myview
AS
SELECT  *
FROM    mytable
WHERE   mycolumn IS NOT NULL

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

Observe que você precisará executar INSERT'e UPDATE' na exibição em vez de na tabela.

Você pode fazer isso com um INSTEAD OFgatilho:

CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END
Quassnoi
fonte
então eu preciso alterar meu dal para inserir na exibição?
Stuart
1
Você pode criar um gatilho INSTEAD OF INSERT.
Quassnoi 20/04/09
6

Isso também pode ser feito no designer

Clique com o botão direito do mouse em Índice> Propriedades para obter esta janela

capturar

Yonatan Tuchinsky
fonte
Alternativa muito boa, se você tiver acesso ao designer #
Francisco Francisco
Embora, como acabei de descobrir, depois de ter dados em sua tabela, você não possa mais usar o designer. Parece ignorar o filtro e qualquer tentativa de atualização da tabela é recebida com a mensagem "Chave duplicada não permitida"
MortimerCat
4

É possível criar uma restrição exclusiva em uma Visualização Indexada em Cluster

Você pode criar a visualização assim:

CREATE VIEW dbo.VIEW_OfYourTable WITH SCHEMABINDING AS
SELECT YourUniqueColumnWithNullValues FROM dbo.YourTable
WHERE YourUniqueColumnWithNullValues IS NOT NULL;

e a restrição única como esta:

CREATE UNIQUE CLUSTERED INDEX UIX_VIEW_OFYOURTABLE 
  ON dbo.VIEW_OfYourTable(YourUniqueColumnWithNullValues)
Lieven Keersmaekers
fonte
2

Talvez considere um " INSTEAD OF" gatilho e faça a verificação você mesmo? Com um índice não clusterizado (não exclusivo) na coluna para ativar a pesquisa.

Marc Gravell
fonte
1

Como afirmado anteriormente, o SQL Server não implementa o padrão ANSI quando se trata UNIQUE CONSTRAINT. Há um tíquete no Microsoft Connect para isso desde 2007. Conforme sugerido aqui e aqui, as melhores opções atualmente são usar um índice filtrado, conforme indicado em outra resposta ou coluna computada, por exemplo:

CREATE TABLE [Orders] (
  [OrderId] INT IDENTITY(1,1) NOT NULL,
  [TrackingId] varchar(11) NULL,
  ...
  [ComputedUniqueTrackingId] AS (
      CASE WHEN [TrackingId] IS NULL
      THEN '#' + cast([OrderId] as varchar(12))
      ELSE [TrackingId_Unique] END
  ),
  CONSTRAINT [UQ_TrackingId] UNIQUE ([ComputedUniqueTrackingId])
)
Baris Akar
fonte
1

Você pode criar um gatilho INSTEAD OF para verificar condições específicas e erros, se eles forem atendidos. Criar um índice pode ser caro em tabelas maiores.

Aqui está um exemplo:

CREATE TRIGGER PONY.trg_pony_unique_name ON PONY.tbl_pony
 INSTEAD OF INSERT, UPDATE
 AS
BEGIN
 IF EXISTS(
    SELECT TOP (1) 1 
    FROM inserted i
    GROUP BY i.pony_name
    HAVING COUNT(1) > 1     
    ) 
     OR EXISTS(
    SELECT TOP (1) 1 
    FROM PONY.tbl_pony t
    INNER JOIN inserted i
    ON i.pony_name = t.pony_name
    )
    THROW 911911, 'A pony must have a name as unique as s/he is. --PAS', 16;
 ELSE
    INSERT INTO PONY.tbl_pony (pony_name, stable_id, pet_human_id)
    SELECT pony_name, stable_id, pet_human_id
    FROM inserted
 END
Paulo
fonte
-1

Você não pode fazer isso com uma UNIQUErestrição, mas pode fazer isso em um gatilho.

    CREATE TRIGGER [dbo].[OnInsertMyTableTrigger]
   ON  [dbo].[MyTable]
   INSTEAD OF INSERT
AS 
BEGIN
    SET NOCOUNT ON;

    DECLARE @Column1 INT;
    DECLARE @Column2 INT; -- allow nulls on this column

    SELECT @Column1=Column1, @Column2=Column2 FROM inserted;

    -- Check if an existing record already exists, if not allow the insert.
    IF NOT EXISTS(SELECT * FROM dbo.MyTable WHERE Column1=@Column1 AND Column2=@Column2 @Column2 IS NOT NULL)
    BEGIN
        INSERT INTO dbo.MyTable (Column1, Column2)
            SELECT @Column2, @Column2;
    END
    ELSE
    BEGIN
        RAISERROR('The unique constraint applies on Column1 %d, AND Column2 %d, unless Column2 is NULL.', 16, 1, @Column1, @Column2);
        ROLLBACK TRANSACTION;   
    END

END
Michael Brown
fonte
-1
CREATE UNIQUE NONCLUSTERED INDEX [UIX_COLUMN_NAME]
ON [dbo].[Employee]([Username] ASC) WHERE ([Username] IS NOT NULL) 
WITH (ALLOW_PAGE_LOCKS = ON, ALLOW_ROW_LOCKS = ON, PAD_INDEX = OFF, SORT_IN_TEMPDB = OFF, 
DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF, STATISTICS_NORECOMPUTE = OFF, ONLINE = OFF, 
MAXDOP = 0) ON [PRIMARY];
user5536124
fonte
-1

este código se você criar um formulário de registro com o textBox e usar insert e ur textBox estiver vazio e clicar no botão enviar.

CREATE UNIQUE NONCLUSTERED INDEX [IX_tableName_Column]
ON [dbo].[tableName]([columnName] ASC) WHERE [columnName] !=`''`;
Ahmed Soliman Flasha
fonte