web-dev-qa-db-fra.com

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

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

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 ma contrainte de schéma et/ou d'unicité afin qu'elle autorise plusieurs valeurs NULL tout en continuant de rechercher l'unicité des données réelles?

574
Stuart

SQL Server 2008 +

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

Avant SQL Server 2008

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

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

137
Jose Basilio

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.

Cependant, dans le monde Microsoft de SQL Server, un seul NULL est autorisé mais plusieurs NULL ne 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 à VIEW avec un prédicat NOT NULL pour appliquer la contrainte.

1227
Vincent Buck

SQL Server 2008 et supérieur

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 les versions antérieures, vous pouvez le faire sans afficher. Je viens d'ajouter une contrainte unique comme celle que vous demandez à l'une de mes tables. Étant donné que je veux 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 placer dans la colonne calculée quelque chose qui sera garanti comme étant unique dans toute la table lorsque la colonne unique souhaitée sera NULL. Dans ce cas, PartyID est une colonne d'identité et le fait d'être numérique ne correspondra jamais à aucune 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 de faire précéder un caractère de différenciation comme ceci:

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

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

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 nécessite davantage d'espace disque.

Notez que si vous ne voulez pas d'index, vous pouvez quand même économiser de la CPU en faisant 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 les versions ultérieures, utilisez définitivement 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", ce qui pose certainement des problèmes (principalement en raison de problèmes liés à la tentative de déterminer quand quelque chose est une valeur réelle ou a 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 qui se multiplient 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 ne se trouve pas déjà séparément dans d'autres colonnes correctement définies. Il ne devrait jamais être sélectionné ou utilisé.

Donc, mon histoire est que ce n'est pas une substitut NULL, et je m'y tiens! Etant donné que nous ne souhaitons pas que la valeur non-NULL serve à tromper l'index UNIQUE de façon à ignorer les valeurs NULL, notre cas d'utilisation ne présente aucun des problèmes rencontrés lors de la création normale de substitution NULL.

Cela dit, l'utilisation d'une vue indexée ne me pose pas de problème, mais elle pose certains problèmes, tels que l'obligation d'utiliser SCHEMABINDING. Amusez-vous en ajoutant une nouvelle colonne à votre table de base (vous devrez au minimum supprimer l'index, puis la vue ou modifier la vue pour qu'elle ne soit pas liée au schéma). Voir l'intégralité (long) liste des exigences pour la création d'une vue indexée dans SQL Server (2005) (également dans les versions ultérieures), (2000) .

Mettre à jour

Si votre colonne est numérique, il peut être difficile de s'assurer que la contrainte unique utilisant Coalesce ne provoque pas de collision. Dans ce cas, il y a quelques options. Une solution consisterait à utiliser un nombre négatif, à placer les "valeurs NULL de substitution" uniquement dans la plage négative et les "valeurs réelles" uniquement dans la plage positive. Alternativement, le modèle suivant pourrait être utilisé. Dans la table Issue (où IssueID est le PRIMARY KEY), il peut y avoir ou non une TicketID, mais s'il en existe 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 contrainte UNIQUE sera sur les valeurs (123, NULL). Si IssueID 2 n'a pas de ticket, il sera activé (NULL, 2). Certains penseront que cette contrainte ne peut être dupliquée pour aucune ligne de la table et autorise toujours plusieurs NULL.

32
ErikE

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

([YourColumnName] IS NOT NULL)

Cela fonctionne avec MSSQL 2012

16
Howard

Quand 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 null a échoué avec l'erreur ci-dessous:

UPDATE a échoué car les options SET suivantes ont des paramètres incorrects: 'ARITHABORT'.

J'ai trouvé ceci 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 désactivé, les instructions CREATE, UPDATE, INSERT et DELETE sur les tables comportant des index sur des colonnes calculées ou des vues indexées échouent.

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

Cliquez avec le bouton droit de la souris sur [Base de données] -> Propriétés -> Options -> Autres options -> Messages spontanés -> Abandon arithmétique activé -> true

Je crois 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é cela

9
Mike Taylor

Créez une vue qui sélectionne uniquement les colonnes non -NULL et créez le UNIQUE INDEX sur 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 devez exécuter INSERT et UPDATE sur la vue au lieu de la table.

Vous pouvez le faire avec un déclencheur INSTEAD OF:

CREATE TRIGGER trg_mytable_insert ON mytable
INSTEAD OF INSERT
AS
BEGIN
        INSERT
        INTO    myview
        SELECT  *
        FROM    inserted
END
6
Quassnoi

Cela peut être fait aussi bien chez le designer

Faites un clic droit sur les propriétés Index> pour afficher cette fenêtre.

capture

4
Yonatan Tuchinsky

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

Peut-être envisager un déclencheur "INSTEAD OF" et vérifier vous-même? Avec un index non clusterisé (non unique) sur la colonne pour permettre la recherche.

2
Marc Gravell

Comme indiqué précédemment, SQL Server n'implémente pas le standard ANSI en ce qui concerne UNIQUE CONSTRAINT. Il existe un ticket sur Microsoft Connect pour cela depuis 2007. Comme suggéré ici et ici , à ce jour, les meilleures options consistent à utiliser un index filtré, comme indiqué dans ne 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])
)
1
Baris Akar

Vous pouvez créer un déclencheur INSTEAD OF pour vérifier les conditions spécifiques et les erreurs éventuelles si elles sont remplies. Créer un index peut être coûteux sur des tables plus volumineuses.

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