Comment créer une contrainte unique qui autorise également les valeurs NULL?

620

Je veux avoir une contrainte unique sur une colonne que je vais remplir avec des GUID. Cependant, mes données contiennent des valeurs nulles pour ces colonnes. Comment créer la contrainte qui autorise plusieurs valeurs nulles?

Voici un exemple de scénario . Considérez ce schéma:

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

Ensuite, voyez ce code pour ce que j'essaie de réaliser:

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

La déclaration finale échoue avec un message:

Violation de la contrainte UNIQUE KEY 'UQ_People_LibraryCardId'. Impossible d'insérer une clé en double dans l'objet 'dbo.People'.

Comment puis-je modifier mon schéma et / ou ma contrainte d'unicité afin qu'elle autorise plusieurs NULLvaleurs, tout en vérifiant l'unicité des données réelles?

Stuart
la source
Problème de
Vadzim
Contrainte UNIQUE et autorise les valeurs NULL. ? C'est du bon sens. Ce n'est pas possible
flik
13
@flik, mieux vaut ne pas se référer au "bon sens". Ce n'est pas un argument valable. Surtout quand on considère que ce nulln'est pas une valeur mais l'absence de valeur. Selon la norme SQL, nulln'est pas considéré comme égal à null. Alors pourquoi le multiple nulldevrait être une violation d'unicité?
Frédéric

Réponses:

144

SQL Server 2008 +

Vous pouvez créer un index unique qui accepte plusieurs NULL avec une WHEREclause. Voir la réponse ci-dessous .

Avant SQL Server 2008

Vous ne pouvez pas créer une contrainte UNIQUE et autoriser les valeurs NULL. Vous devez définir une valeur par défaut de NEWID ().

Mettez à jour les valeurs existantes vers NEWID () où NULL avant de créer la contrainte UNIQUE.

Jose Basilio
la source
2
et cela ajoutera rétrospectivement des valeurs aux lignes existantes, si c'est ce que je dois faire, merci?
Stuart
1
Vous devez exécuter une instruction UPDATE pour définir les valeurs existantes sur NEWID () où le champ existant EST NULL
Jose Basilio
55
Si vous utilisez SQL Server 2008 ou version ultérieure, consultez la réponse ci-dessous avec plus de 100 votes positifs. Vous pouvez ajouter une clause WHERE à votre contrainte unique.
Darren Griffith
1
Ce problème touche également ADO.NET DataTables. Donc, même si je peux autoriser des valeurs nulles dans le champ de support à l'aide de cette méthode, le DataTable ne me permettra pas de stocker des valeurs NULL dans une colonne unique en premier lieu. Si quelqu'un connaît une solution pour cela, veuillez la poster ici
dotNET
6
Les gars assurez-vous de faire défiler vers le bas et de lire la réponse avec 600 votes positifs. Ce n'est plus un peu plus de 100.
Luminous
1289

Ce que vous recherchez fait en effet partie des normes ANSI SQL: 92, SQL: 1999 et SQL: 2003, c'est-à-dire qu'une contrainte UNIQUE doit interdire les valeurs non NULL en double mais accepter plusieurs valeurs NULL.

Dans le monde Microsoft de SQL Server cependant, un seul NULL est autorisé mais plusieurs NULL ne le sont pas ...

Dans SQL Server 2008 , vous pouvez définir un index filtré unique basé sur un prédicat qui exclut les valeurs NULL:

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

Dans les versions antérieures, vous pouvez recourir à VIEWS avec un prédicat NOT NULL pour appliquer la contrainte.

Vincent Buck
la source
3
c'est probablement la meilleure façon de procéder. vous ne savez pas s'il y a des impacts sur les performances? n'importe qui?
Simon_Weaver
3
J'essaie de faire exactement cela dans l'édition SQL Server 2008 Express et j'obtiens une erreur comme suit: CRÉER UN INDEX UNCLUSTERED UNIQUE UC_MailingId ON [SLS-CP] .dbo.MasterFileEntry (MailingId) O Mail MailingId N'EST PAS NUL Résultats dans: Msg 156, Niveau 15, état 1, ligne 3 Syntaxe incorrecte près du mot-clé 'OERE'. Si je supprime la clause where, la DDL fonctionne correctement, mais bien sûr, ne fait pas ce dont j'ai besoin. Des idées?
Kenneth Baltrinic
4
Sauf erreur, vous ne pouvez pas créer une clé étrangère à partir d'un index unique comme vous pouvez désactiver une contrainte unique. (Au moins SSMS s'est plaint de moi lorsque j'ai essayé.) Ce serait bien de pouvoir avoir une colonne nullable qui est toujours unique (quand elle n'est pas nulle) comme source d'une relation de clé étrangère.
Vaccano
8
Vraiment une excellente réponse. Dommage qu'il ait été caché par celui accepté comme réponse. Cette solution n'a presque pas attiré mon attention, mais elle fonctionne maintenant comme des merveilles dans mon implémentation.
Coral Doe
2
Une autre alternative pour SQL 2005 et les versions antérieures est une astuce sur la colonne calculée, appelée "Nullbuster". stackoverflow.com/a/191729/132461 Cela vous évite d'encombrer la base de données avec une autre vue, vous avez juste une autre colonne à la place - généralement Nommé ColumnA-Nullbuster si ColumnA est celle que vous voulez être ANSI NULL UNIQUE. Mettez un index UNIQUE (ou une contrainte pour exprimer l'intention commerciale) sur ColumnA-Nullbuster et il appliquera l'unicité sur ColumnA
DanO
34

SQL Server 2008 et versions ultérieures

Il suffit de filtrer un index unique:

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

Dans les versions inférieures, une vue matérialisée n'est toujours pas requise

Pour SQL Server 2005 et versions antérieures, vous pouvez le faire sans vue. Je viens d'ajouter une contrainte unique comme celle que vous demandez à l'une de mes tables. Étant donné que je souhaite l'unicité de la colonne SamAccountName, mais que je souhaite autoriser plusieurs valeurs NULL, j'ai utilisé une colonne matérialisée plutôt qu'une vue matérialisée:

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

Vous devez simplement mettre quelque chose dans la colonne calculée qui sera garanti unique sur toute la table lorsque la colonne unique souhaitée réelle est NULL. Dans ce cas, PartyIDest une colonne d'identité et être numérique ne correspondra à aucun SamAccountName, donc cela a fonctionné pour moi. Vous pouvez essayer votre propre méthode - assurez-vous de bien comprendre le domaine de vos données afin qu'il n'y ait aucune possibilité d'intersection avec des données réelles. Cela pourrait être aussi simple que d'ajouter un caractère différenciateur comme celui-ci:

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

Même si un jour, il PartyIDdevenait non numérique et pouvait coïncider avec un SamAccountName, maintenant cela n'aurait plus d'importance.

Notez que la présence d'un index incluant la colonne calculée entraîne implicitement l'enregistrement de chaque résultat d'expression sur le disque avec les autres données de la table, ce qui prend de l'espace disque supplémentaire.

Notez que si vous ne voulez pas d'index, vous pouvez toujours économiser de l'UC en faisant précalculer l'expression sur le disque en ajoutant le mot-clé PERSISTEDà la fin de la définition de l'expression de colonne.

Dans SQL Server 2008 et versions ultérieures, utilisez plutôt la solution filtrée si vous le pouvez!

Controverse

Veuillez noter que certains professionnels de la base de données verront cela comme un cas de «NULL de substitution», qui ont certainement des problèmes (principalement en raison de problèmes pour essayer de déterminer quand quelque chose est une valeur réelle ou une valeur de substitution pour les données manquantes ; il peut également y avoir des problèmes avec le nombre de valeurs de substitution non NULL se multipliant comme un fou).

Cependant, je crois que ce cas est différent. La colonne calculée que j'ajoute ne sera jamais utilisée pour déterminer quoi que ce soit. Il n'a aucune signification en soi et n'encode aucune information qui n'est pas déjà trouvée séparément dans d'autres colonnes correctement définies. Il ne doit jamais être sélectionné ou utilisé.

Donc, mon histoire est que ce n'est pas un NULL de substitution, et je m'y tiens! Puisque nous ne voulons pas réellement la valeur non NULL à d'autres fins que de tromper l' UNIQUEindex pour ignorer les valeurs NULL, notre cas d'utilisation n'a aucun des problèmes qui surviennent avec la création NULL de substitution normale.

Cela dit, je n'ai pas de problème à utiliser une vue indexée à la place, mais cela pose certains problèmes, comme l'exigence d'utilisation SCHEMABINDING. Amusez-vous à ajouter une nouvelle colonne à votre table de base (vous devrez au minimum supprimer l'index, puis supprimer la vue ou modifier la vue pour ne pas être liée au schéma). Consultez la liste complète (longue) des exigences pour la création d'une vue indexée dans SQL Server (2005) (également les versions ultérieures), (2000) .

Mise à jour

Si votre colonne est numérique, il peut être difficile de vous assurer que la contrainte unique utilisant Coalescen'entraîne pas de collisions. Dans ce cas, il existe quelques options. L'une pourrait consister à utiliser un nombre négatif, à ne mettre les «NULL de substitution» que dans la plage négative et les «valeurs réelles» uniquement dans la plage positive. Alternativement, le modèle suivant pourrait être utilisé. Dans le tableau Issue(où IssueIDest le PRIMARY KEY), il peut y avoir ou non un TicketID, mais s'il y en a un, il doit être unique.

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

Si IssueID 1 a le ticket 123, la UNIQUEcontrainte sera sur les valeurs (123, NULL). Si IssueID 2 n'a pas de ticket, il sera activé (NULL, 2). Certaines réflexions montreront que cette contrainte ne peut être dupliquée pour aucune ligne du tableau et autorise toujours plusieurs NULL.

ErikE
la source
16

Pour les personnes qui utilisent Microsoft SQL Server Manager et souhaitent créer un index unique mais nul, vous pouvez créer votre index unique comme vous le feriez normalement dans vos propriétés d'index pour votre nouvel index, sélectionnez "Filtrer" dans le panneau de gauche, puis entrez votre filtre (qui est votre clause where). Il devrait lire quelque chose comme ceci:

([YourColumnName] IS NOT NULL)

Cela fonctionne avec MSSQL 2012

Howard
la source
Comment faire un index filtré sous Microsoft SQL Server Management Studio est décrit ici et fonctionne parfaitement: msdn.microsoft.com/en-us/library/cc280372.aspx
Jan
9

Lorsque j'ai appliqué l'index unique ci-dessous:

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

chaque mise à jour et insertion non nulle a échoué avec l'erreur ci-dessous:

La MISE À JOUR a échoué car les options SET suivantes ont des paramètres incorrects: 'ARITHABORT'.

J'ai trouvé cela sur MSDN

SET ARITHABORT doit être activé lorsque vous créez ou modifiez des index sur des colonnes calculées ou des vues indexées. Si SET ARITHABORT est OFF, les instructions CREATE, UPDATE, INSERT et DELETE sur les tables avec des index sur les colonnes calculées ou les vues indexées échoueront.

Donc, pour que cela fonctionne correctement, je l'ai fait

Cliquez avec le bouton droit sur [Base de données] -> Propriétés -> Options -> Autres options -> Misscellaneous -> Arithmetic Abort Enabled -> true

Je pense qu'il est possible de définir cette option dans le code en utilisant

ALTER DATABASE "DBNAME" SET ARITHABORT ON

mais je n'ai pas testé ça

Mike Taylor
la source
6

Créez une vue qui sélectionne uniquement les non- NULLcolonnes et créez la UNIQUE INDEXsur la vue:

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

CREATE UNIQUE INDEX ux_myview_mycolumn ON myview (mycolumn)

Notez que vous devrez effectuer des INSERT«et UPDATE» sur la vue au lieu de la table.

Vous pouvez le faire avec un INSTEAD OFdéclencheur:

CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END
Quassnoi
la source
dois-je donc changer mon dal pour l'insérer dans la vue?
Stuart
1
Vous pouvez créer un déclencheur AU LIEU DE L'INSERTION.
Quassnoi
6

Cela peut aussi être fait dans le concepteur

Faites un clic droit sur l'index> Propriétés pour obtenir cette fenêtre

Capturer

Yonatan Tuchinsky
la source
Très belle alternative si vous avez accès au créateur
Francisco
Bien que, comme je viens de le découvrir, une fois que vous avez des données dans votre table, vous ne pouvez plus utiliser le concepteur. Il semble ignorer le filtre et toute tentative de mise à jour de table est rencontrée avec le message "Clé en double non autorisée"
MortimerCat
4

Il est possible de créer une contrainte unique sur une vue indexée en cluster

Vous pouvez créer la vue comme ceci:

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

et la contrainte unique comme celle-ci:

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

Pensez peut-être à un INSTEAD OFdéclencheur " " et faites-le vous-même? Avec un index non clusterisé (non unique) sur la colonne pour activer la recherche.

Marc Gravell
la source
1

Comme indiqué précédemment, SQL Server n'implémente pas la norme ANSI en ce qui concerne UNIQUE CONSTRAINT. Il existe un ticket sur Microsoft Connect pour cela depuis 2007. Comme suggéré ici et ici, les meilleures options à ce jour sont d'utiliser un index filtré comme indiqué dans une autre réponse ou une colonne calculée, par exemple:

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
la source
1

Vous pouvez créer un déclencheur INSTEAD OF pour vérifier les conditions spécifiques et les erreurs si elles sont remplies. La création d'un index peut être coûteuse sur des tables plus grandes.

Voici un exemple:

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
Paul
la source
-1

Vous ne pouvez pas le faire avec une UNIQUEcontrainte, mais vous pouvez le faire dans un déclencheur.

    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
la source
-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
la source
-1

ce code si vous faites un formulaire d'inscription avec textBox et utilisez insert et ur textBox est vide et u cliquez sur le bouton soumettre.

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