web-dev-qa-db-fra.com

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

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 NOCHECK mais 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 à faire en sorte que les INSERT utilisent une procédure stockée qui échouera pour les doublons ... S'il était possible que SQL vérifie seul l'unicité, ce serait préférable.

Ces données sont interrogées par nom d'entreprise. Pour les quelques doublons existants, cela signifie que plusieurs lignes sont renvoyées et affichées ... Bien que ce soit faux, c'est acceptable dans notre cas d'utilisation. Le but est de l'empêcher à l'avenir. Il me semble d'après les commentaires que je dois faire cette logique dans les procédures stockées.

41
Matthew

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 lignes. 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>);
34
Gordon Linoff

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 les existants et veillons à ce qu'aucun nouveau doublon 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

Essayons 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'.
23
A-K

L'index unique filtré est une excellente idée, mais il présente un inconvénient mineur - peu importe si vous utilisez la condition WHERE identity_column > <current value> Ou la 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 à l'avenir, des doublons de données existantes (maintenant). Par exemple, si vous avez (même une seule) ligne maintenant avec CompanyName = 'Software Inc.', L'index n'interdira pas l'insertion d'une ligne de plus avec le même nom de société. Il 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 bien.) Cependant, vous pourrez toujours insérer plus de doublons ou de doublons existants. Par exemple, si vous avez maintenant (deux ou plusieurs) lignes avec CompanyName = 'DoubleData Co.', L'index n'interdira pas l'insertion d'une ligne de plus avec le même nom de société. Il ne l'interdira que si vous essayez deux fois.

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

Avec la deuxième approche, un autre inconvénient est la condition encombrante (la quantité d'encombrement dépend du nombre de doublons en premier lieu), car SQL-Server semble ne pas prendre en charge l'opérateur NOT IN Dans le WHERE partie d'index filtrés. Voir SQL-Fiddle . Au lieu de WHERE (CompanyID NOT IN (3,7,4,6,8,9)), vous devrez avoir quelque chose comme WHERE (CompanyID <> 3 AND CompanyID <> 7 AND CompanyID <> 4 AND CompanyID <> 6 AND CompanyID <> 8 AND CompanyID <> 9) Je ne sais pas s'il y a des implications d'efficacité avec une telle condition, si vous avez des centaines de noms en double.


Une autre solution (similaire à celle de @Alex Kuznetsov) consiste à ajouter une autre colonne, à la remplir avec des numéros de classement 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 en raison de la propriété DEFAULT 1 Et de l'index unique. Ce n'est toujours pas infaillible à 100% (contrairement à celui d'Alex). Les doublons se glisseront toujours si Rn est explicitement défini dans l'instruction INSERT ou si les valeurs Rn sont mises à jour de manière malveillante.

SQL-Fiddle-2

16
ypercubeᵀᴹ