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?
Vous pouvez créer un index unique qui accepte plusieurs NULL avec une clause WHERE
. Voir le réponse ci-dessous .
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.
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.
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.
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
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
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
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)
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.
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])
)
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