99 votes

contrainte unique conditionnelle

Je suis dans une situation où je dois appliquer une contrainte unique sur un ensemble de colonnes, mais seulement pour une valeur d'une colonne.

Par exemple, j'ai une table comme Table(ID, Name, RecordStatus).

RecordStatus ne peut avoir que la valeur 1 ou 2 (actif ou supprimé), et je veux créer une contrainte unique sur (ID, RecordStatus) uniquement lorsque RecordStatus = 1, puisque je ne me soucie pas de savoir s'il existe plusieurs enregistrements supprimés avec le même ID.

En dehors de l'écriture des déclencheurs, puis-je faire cela ?

J'utilise SQL Server 2005.

1 votes

Cette conception est une douleur courante. Avez-vous envisagé de modifier la conception de sorte que les enregistrements théoriquement "supprimés" soient physiquement supprimés de la table et peut-être déplacés vers une table "d'archives" ?

1 votes

...parce que l'incapacité d'écrire une contrainte UNIQUE pour renforcer une clé simple devrait être considérée comme une "odeur de code", IMO. Si vous ne pouvez pas modifier la conception (DDL SQL) parce que de nombreuses autres tables font référence à cette table, je parie que votre DML SQL en pâtit également, c'est-à-dire que vous devez vous rappeler d'ajouter " ...AND Table.RecordStatus = 1 " à la plupart des conditions de recherche et des conditions de jointure impliquant cette table et que vous rencontrez des bogues subtils lorsque cette contrainte est inévitablement omise à l'occasion.

162voto

canon Points 14870

Regardez, l'indice filtré . Extrait de la documentation (c'est moi qui souligne) :

Un index filtré est un index optimisé non groupé particulièrement adapté aux requêtes qui sélectionnent un sous-ensemble bien défini de données. Il utilise un prédicat de filtre pour indexer une partie des lignes de la table. Un index filtré bien conçu peut améliorer les performances des requêtes et réduire les coûts de maintenance et de stockage de l'index par rapport aux index de tables complètes.

Et voici un exemple combinant un index unique avec un prédicat de filtre :

create unique index MyIndex
on MyTable(ID)
where RecordStatus = 1;

Cela renforce essentiellement l'unicité de ID quand RecordStatus est 1 .

Après la création de cet index, une violation de l'unicité entraînera l'émission d'un message d'avertissement :

Msg 2601, Niveau 14, Etat 1, Ligne 13
Impossible d'insérer une ligne de clé dupliquée dans l'objet 'dbo.MyTable' avec l'index unique 'MyIndex'. La valeur de la clé dupliquée est (9999).

Remarque : l'index filtré a été introduit dans SQL Server 2008. Pour les versions antérieures de SQL Server, veuillez consulter <a href="https://stackoverflow.com/a/866100/621962">cette réponse </a>.

0 votes

Notez que SQL Server requiert ansi_padding pour les index filtrés, donc assurez-vous que cette option est activée en exécutant SET ANSI_PADDING ON avant de créer un index filtré.

40voto

D. Patrick Points 1463

Ajoutez une contrainte de contrôle comme ceci. La différence est que vous retournerez false si Status = 1 et Count > 0.

http://msdn.microsoft.com/en-us/library/ms188258.aspx

CREATE TABLE CheckConstraint
(
  Id TINYINT,
  Name VARCHAR(50),
  RecordStatus TINYINT
)
GO

CREATE FUNCTION CheckActiveCount(
  @Id INT
) RETURNS INT AS BEGIN

  DECLARE @ret INT;
  SELECT @ret = COUNT(*) FROM CheckConstraint WHERE Id = @Id AND RecordStatus = 1;
  RETURN @ret;

END;
GO

ALTER TABLE CheckConstraint
  ADD CONSTRAINT CheckActiveCountConstraint CHECK (NOT (dbo.CheckActiveCount(Id) > 1 AND RecordStatus = 1));

INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 2);
INSERT INTO CheckConstraint VALUES (1, 'No Problems', 1);

INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 2);
-- Msg 547, Level 16, State 0, Line 14
-- The INSERT statement conflicted with the CHECK constraint "CheckActiveCountConstraint". The conflict occurred in database "TestSchema", table "dbo.CheckConstraint".
INSERT INTO CheckConstraint VALUES (2, 'Oh no!', 1);

SELECT * FROM CheckConstraint;
-- Id   Name         RecordStatus
-- ---- ------------ ------------
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  2
-- 1    No Problems  1
-- 2    Oh no!       1
-- 2    Oh no!       2

ALTER TABLE CheckConstraint
  DROP CONSTRAINT CheckActiveCountConstraint;

DROP FUNCTION CheckActiveCount;
DROP TABLE CheckConstraint;

0 votes

J'ai regardé les contraintes de contrôle au niveau de la table mais il ne semble pas y avoir de moyen de passer les valeurs insérées ou mises à jour à la fonction, savez-vous comment faire ?

0 votes

Ok, j'ai posté un exemple de script qui vous aidera à prouver ce dont je parle. Je l'ai testé et il fonctionne. Si vous regardez les deux lignes commentées, vous verrez le message que j'obtiens. Nota bene, dans mon implémentation, je m'assure simplement que vous ne pouvez pas ajouter un deuxième élément avec le même Id qui est actif s'il y en a déjà un d'actif. Vous pourriez modifier la logique de sorte que s'il y a un élément actif, vous ne pouvez pas ajouter d'élément avec le même Id. Avec ce modèle, les possibilités sont pratiquement infinies.

0 votes

Je préférerais la même logique dans un déclencheur. " une requête dans une fonction scalaire... peut créer de gros problèmes si votre contrainte CHECK repose sur une requête et si plus d'une ligne est affectée par une mise à jour. Ce qui se passe, c'est que la contrainte est vérifiée une fois pour chaque ligne avant que l'instruction ne se termine. Cela signifie que l'atomicité de la déclaration est rompue et que la fonction sera exposée à la base de données dans un état incohérent. Les résultats sont imprévisibles et imprécis." Voir : blogs.conchango.com/davidportas/archive/2007/02/19/

11voto

Carl Manaster Points 23696

Vous pourriez déplacer les enregistrements supprimés vers une table dépourvue de la contrainte, et peut-être utiliser une vue avec UNION des deux tables pour préserver l'apparence d'une seule table.

2 votes

C'est en fait très intelligent, Carl. Ce n'est pas une réponse à la question en soi, mais c'est une bonne solution. Si la table comporte beaucoup de lignes, cela pourrait également accélérer la recherche d'un enregistrement actif, car vous pourriez consulter la table des enregistrements actifs. La contrainte serait également plus rapide, car la contrainte unique utilise un index, contrairement à la contrainte de contrôle que j'ai écrite ci-dessous, qui doit exécuter un comptage. Cela me plaît.

9voto

Bill Karwin Points 204877

Vous pourriez utiliser un UNIQUE si vous utilisez NULL comme votre statut supprimé.

Plusieurs lignes avec NULL sont autorisées par un UNIQUE (du moins en SQL standard - je n'ai pas confirmé cela par un test contre SQL Server 2005).

3voto

Min Points 2712

Vous pouvez le faire d'une manière très peu pratique...

Créez une vue schemabound sur votre table.

CREATE VIEW Whatever SELECT * FROM Table Où RecordStatus = 1

Créez maintenant une contrainte unique sur la vue avec les champs que vous voulez.

Une remarque sur les vues liées à des schémas : si vous modifiez les tables sous-jacentes, vous devrez recréer la vue. Il y a beaucoup de problèmes à cause de cela.

0 votes

Il s'agit d'une bonne suggestion, qui n'est pas si "compliquée". Voici plus d'informations à ce sujet index filtré alternatif .

0 votes

C'est une mauvaise idée. La question n'est pas ça.

0 votes

J'ai utilisé une fois une vue schématisée et je n'ai jamais répété cette erreur. Ces vues peuvent être très difficiles à utiliser. Le problème n'est pas que vous devez recréer la vue si vous changez la table sous-jacente - vous devez potentiellement le faire pour toutes les vues, au moins dans SQL Server. Le problème est que vous ne pouvez pas modifier la table sans d'abord supprimer la vue, ce que vous ne pourrez peut-être pas faire sans supprimer d'abord les références à celle-ci. Oh, de plus, le stockage pourrait être problématique - soit à cause de l'espace, soit à cause du coût qu'il ajoute à l'insertion et à la mise à jour.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X