Puis-je ajouter une contrainte unique qui ignore les violations existantes?

41

J'ai une table qui a actuellement des valeurs en double dans une colonne.

Je ne peux pas supprimer ces doublons erronés, mais j'aimerais empêcher l'ajout de valeurs non uniques supplémentaires.

Puis-je créer un UNIQUE qui ne vérifie pas la conformité existante?

J'ai essayé d'utiliser NOCHECKmais j'ai échoué.

Dans ce cas, j'ai un tableau qui lie les informations de licence à "CompanyName"

EDIT: avoir plusieurs lignes avec le même "CompanyName" est une mauvaise donnée, mais nous ne pouvons pas supprimer ou mettre à jour ces doublons pour le moment. Une approche consiste à INSERTutiliser une procédure stockée qui échouera pour les doublons ... S'il était possible que SQL vérifie lui-même l'unicité, ce serait préférable.

Ces données sont interrogées par nom de société. Pour les quelques doublons existants, cela signifie que plusieurs lignes sont renvoyées et affichées ... Bien que cela ne soit pas correct, cela est acceptable dans notre cas d'utilisation. Le but est de le prévenir à l'avenir. Il me semble d'après les commentaires que je dois faire cette logique dans les procédures stockées.

Matthieu
la source
Êtes-vous autorisé à modifier la table (ajouter une colonne supplémentaire)?
Ypercubeᵀᴹ
@ypercube malheureusement pas.
Matthew

Réponses:

34

La réponse est oui". Vous pouvez le faire avec un index filtré (voir ici pour la documentation).

Par exemple, vous pouvez faire:

create unique index t_col on t(col) where id > 1000;

Cela crée un index unique, uniquement sur les nouvelles lignes, plutôt que sur les anciennes. Cette formulation particulière permettrait des doublons avec les valeurs existantes.

Si vous n'avez qu'une poignée de doublons, vous pouvez faire quelque chose comme:

create unique index t_col on t(col) where id not in (<list of ids for duplicate values here>);
Gordon Linoff
la source
2
Que cela soit bon ou non dépend de la question de savoir si les "anciens" éléments existants doivent empêcher la création de nouveaux éléments avec la même valeur.
Supercat
1
@supercat. . . J'ai donné une formulation alternative pour construire l'index sur tout sauf les valeurs existantes en double.
Gordon Linoff
1
Pour que cette dernière fonctionne, il faut s’assurer que l’on omet de la liste un identifiant pour chaque valeur de clé distincte qui a des doublons et s’assurer également que si l’élément omis délibérément de la liste est supprimé du tableau. , un élément avec une clé égale serait retiré de la liste.
Supercat
@supercat. . . Je suis d'accord. Maintenir l'index cohérent pour les mises à jour et les suppressions est d'autant plus difficile que vous ne pouvez pas recréer l'index dans un déclencheur. Dans tous les cas, le PO m'a donné l'impression que les données - ou du moins les doublons - ne changent pas souvent, voire pas du tout.
Gordon Linoff
Pourquoi ne pas exclure une liste de valeurs plutôt qu'une liste d'identifiants? Ensuite, vous ne devez pas exclure un ID par valeur dupliquée de la liste des ID exclus
JMD Coalesce
23

Oui, tu peux faire ça.

Voici un tableau avec des doublons:

CREATE TABLE dbo.Party
  (
    ID INT NOT NULL
           IDENTITY ,
    CONSTRAINT PK_Party PRIMARY KEY ( ID ) ,
    Name VARCHAR(30) NOT NULL
  ) ;
GO

INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Frodo Baggins' ),
        ( 'Luke Skywalker' ),
        ( 'Luke Skywalker' ),
        ( 'Harry Potter' ) ;
GO

Ignorons ceux qui existent déjà et assurons-nous qu'aucun nouveau duplicata ne puisse être ajouté:

-- Add a new column to mark grandfathered duplicates.
ALTER TABLE dbo.Party ADD IgnoreThisDuplicate INT NULL ;
GO

-- The *first* instance will be left NULL.
-- *Secondary* instances will be set to their ID (a unique value).
UPDATE  dbo.Party
SET     IgnoreThisDuplicate = ID
FROM    dbo.Party AS my
WHERE   EXISTS ( SELECT *
                 FROM   dbo.Party AS other
                 WHERE  other.Name = my.Name
                        AND other.ID < my.ID ) ;
GO

-- This constraint is not strictly necessary.
-- It prevents granting further exemptions beyond the ones we made above.
ALTER TABLE dbo.Party WITH NOCHECK
ADD CONSTRAINT CHK_Party_NoNewExemptions 
CHECK(IgnoreThisDuplicate IS NULL);
GO

SELECT * FROM dbo.Party;
GO

-- **THIS** is our pseudo-unique constraint.
-- It works because the grandfathered duplicates have a unique value (== their ID).
-- Non-grandfathered records just have NULL, which is not unique.
CREATE UNIQUE INDEX UNQ_Party_UniqueNewNames ON dbo.Party(Name, IgnoreThisDuplicate);
GO

Laissez-nous tester cette solution:

-- cannot add a name that exists
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Frodo Baggins' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.

-- cannot add a name that exists and has an ignored duplicate
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Luke Skywalker' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.


-- can add a new name 
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Hamlet' );

-- but only once
INSERT  INTO dbo.Party
        ( Name )
VALUES  ( 'Hamlet' );

Cannot insert duplicate key row in object 'dbo.Party' with unique index 'UNQ_Party_UniqueNewNames'.
AK
la source
4
Sauf qu'il ne peut pas ajouter de colonne à la table.
Aaron Bertrand
3
J'aime la façon dont cette réponse transforme la manière dont les valeurs NULL sont traitées de manière non standard dans une contrainte unique en quelque chose d'utile. Astuce ruse.
Ypercubeᵀᴹ
@ ypercubeᵀᴹ, pourriez-vous expliquer en quoi des contraintes non standard concernent la gestion de la valeur NULL? En quoi est-ce différent de ce que vous auriez prévu? Merci!
Noach
1
@Noach dans SQL Server, une UNIQUEcontrainte dans une colonne nullable garantit qu'il existe au plus une NULLvaleur unique . Le standard SQL (et presque tous les autres SGBD SQL) indique qu'il doit autoriser un nombre quelconque de NULLvaleurs (c'est-à-dire que la contrainte doit ignorer les valeurs nulles).
Ypercubeᵀᴹ
@ ypercubeᵀᴹ Donc, pour implémenter cela sur un SGBD différent, il suffit d'utiliser DEFAULT 0 plutôt que NULL. Correct?
Noach
16

L'index unique filtré est une idée brillante, mais il présente un inconvénient mineur - que vous utilisiez la WHERE identity_column > <current value>condition ou le WHERE identity_column NOT IN (<list of ids for duplicate values here>).

Avec la première approche, vous pourrez toujours insérer des données en double dans le futur, des copies de données existantes (à présent). Par exemple, si vous avez (même une seule) ligne avec CompanyName = 'Software Inc.', l'index n'interdit pas l'insertion d'une ligne supplémentaire avec le même nom de société. Cela ne l'interdira que si vous essayez deux fois.

Avec la deuxième approche, il y a une amélioration, ce qui précède ne fonctionnera pas (ce qui est bon.) Cependant, vous pourrez toujours insérer plus de doublons ou des doublons existants. Par exemple, si vous avez (deux ou plus) lignes maintenant avecCompanyName = 'DoubleData Co.' , l'index n'interdit pas l'insertion d'une ligne supplémentaire avec le même nom de société. Cela ne l'interdira que si vous essayez deux fois.

(Mise à jour) Cela peut être corrigé si, pour chaque nom en double, vous gardez un identifiant sur la liste d'exclusion. Si, comme dans l'exemple ci-dessus, il y a 4 lignes avec doublons CompanyName = DoubleData Co.et identifiants 4,6,8,9, la liste d'exclusion ne devrait contenir que 3 de ces identifiants.

Avec la seconde approche, un autre inconvénient est la lourdeur (son importance dépend du nombre de doublons existant en premier lieu), car SQL-Server ne semble pas prendre en charge l' NOT INopérateur dans la WHEREpartie des index filtrés. Voir SQL-Fiddle . Au lieu de WHERE (CompanyID NOT IN (3,7,4,6,8,9)), vous devrez avoir quelque chose commeWHERE (CompanyID <> 3 AND CompanyID <> 7 AND CompanyID <> 4 AND CompanyID <> 6 AND CompanyID <> 8 AND CompanyID <> 9) je ne suis pas sûr que cette condition ait des conséquences sur l'efficacité, si vous avez des centaines de noms en double.


Une autre solution (similaire à celle de @Alex Kuznetsov) consiste à ajouter une autre colonne, à la renseigner avec des numéros de rang et à ajouter un index unique comprenant cette colonne:

ALTER TABLE Company
  ADD Rn TINYINT DEFAULT 1;

UPDATE x
SET Rn = Rnk
FROM
  ( SELECT 
      CompanyID,
      Rn,
      Rnk = ROW_NUMBER() OVER (PARTITION BY CompanyName 
                               ORDER BY CompanyID)
    FROM Company 
  ) x ;

CREATE UNIQUE INDEX CompanyName_UQ 
  ON Company (CompanyName, Rn) ; 

Ensuite, l'insertion d'une ligne avec un nom en double échouera à cause de la DEFAULT 1propriété et de l'index unique. Ce n'est toujours pas à 100% infaillible (alors qu'Alex l'est). Les doublons resteront quand même insérés si le Rnest explicitement défini dans l' INSERTinstruction ou si les Rnvaleurs sont mises à jour de manière malveillante.

SQL-Fiddle-2

ypercubeᵀᴹ
la source
-2

Une autre alternative consiste à écrire une fonction scalaire qui vérifie si une valeur existe déjà dans la table, puis à appeler cette fonction à partir d'une contrainte de vérification.

Cela fera des choses horribles à la performance.

Greenstone Walker
la source
Outre les points soulevés par Aaron, la réponse n'explique pas comment cette contrainte de vérification peut être ajoutée, elle ignore donc les doublons existants.
Ypercubeᵀᴹ
-2

Je cherche la même chose - créer un index unique non fiable pour que les mauvaises données existantes soient ignorées, mais les nouveaux enregistrements ne peuvent pas être des doublons de tout ce qui existe déjà.

En lisant ce fil de discussion, il me semble qu’une meilleure solution consiste à écrire un déclencheur qui vérifie la présence de doublons dans la table parent [inséré] et, s’il existe des doublons, ROLLBACK TRAN.

Brad
la source