contrainte unique conditionnelle

93

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

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

RecordStatus ne peut avoir qu'une valeur 1 ou 2 (active ou supprimée), et je veux créer une contrainte unique sur (ID, RecordStatus) uniquement lorsque RecordStatus = 1, car je m'en fiche s'il y a plusieurs enregistrements supprimés avec le même ID.

En plus d'écrire des déclencheurs, puis-je faire cela?

J'utilise SQL Server 2005.

np-dur
la source
1
Cette conception est une douleur courante. Avez-vous envisagé de changer la conception afin que les enregistrements théoriquement «supprimés» soient physiquement supprimés de la table et peut-être déplacés vers une table «d'archive»?
jour
1
... parce que l'impossibilité d'écrire une contrainte UNIQUE pour appliquer une clé simple doit être considérée comme une «odeur de code», IMO. Si vous ne pouvez pas changer la conception (SQL DDL) parce que de nombreuses autres tables font référence à cette table, je parie que votre SQL DML en souffre également, c'est-à-dire que vous devez vous rappeler d'ajouter ... AND Table.RecordStatus = 1 ' à la plupart des conditions de recherche et de jointure impliquant cette table et rencontrant des bogues subtils lorsqu'elle est inévitablement omise à l'occasion.
jour

Réponses:

36

Ajoutez une contrainte de vérification comme celle-ci. 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;
D. Patrick
la source
J'ai regardé les contraintes de vérification au niveau de la table mais je ne vois pas qu'il existe un moyen de transmettre les valeurs insérées ou mises à jour à la fonction, savez-vous comment faire?
np-hard
D'accord, j'ai posté un exemple de script qui vous aidera à prouver de quoi je parle. Je l'ai testé et ça marche. Si vous regardez les deux lignes commentées, vous verrez le message que je reçois. Nota bene, dans ma mise en œuvre, je m'assure simplement que vous ne pouvez pas ajouter un deuxième élément avec le même identifiant qui est actif s'il y en a déjà un actif. Vous pouvez modifier la logique de telle sorte que s'il y en a un actif, vous ne pouvez pas ajouter d'élément avec le même identifiant. Avec ce modèle, les possibilités sont pratiquement infinies.
D.Patrick
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 la fin de l'instruction . Cela signifie que l'atomicité de l'instruction est interrompue et que la fonction sera exposée à la base de données dans un état incohérent. Les résultats sont imprévisibles et inexacts. " Voir: blogs.conchango.com/davidportas/archive/2007/02/19/…
jour
Ce n'est que partiellement vrai un jour quand. La base de données se comporte de manière cohérente et prévisible. La contrainte de vérification s'exécutera après l'ajout de la ligne à la table et avant que la transaction ne soit validée par les dbms et vous pouvez compter dessus. Ce blog parlait d'un problème assez unique où vous devez exécuter la contrainte sur un ensemble d'inserts plutôt que sur un seul insert à la fois. ashish demande une contrainte sur un insert à la fois et cette contrainte fonctionnera de manière précise, prévisible et cohérente. Je suis désolé si cela semble laconique; Je manquais de personnages.
D.Patrick
3
Cela fonctionne très bien pour les insertions mais ne semble pas fonctionner pour les mises à jour. EG Ajouter ceci après les autres inserts fonctionne ici quand je ne m'y attendais pas. INSERT INTO CheckConstraint VALUES (1, 'No ProblemsA', 2); mise à jour CheckConstraint set Recordstatus = 1 où name = 'No ProblemsA'
dwidel
149

Voici, l'index filtré . De la documentation (c'est moi qui souligne):

Un index filtré est un index non clusterisé optimisé, particulièrement adapté pour couvrir les requêtes qui sélectionnent dans un sous-ensemble de données bien défini. 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 des index par rapport aux index de table complète.

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

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

Ceci impose essentiellement l' unicité de IDquand RecordStatusest 1.

Suite à la création de cet index, une violation d'unicité soulèvera une arror:

Msg 2601, niveau 14, état 1, ligne 13
Impossible d'insérer une ligne de clé en double dans l'objet «dbo.MyTable» avec l'index unique «MyIndex». La valeur de clé en double est (9999).

Remarque: l'index filtré a été introduit dans SQL Server 2008. Pour les versions antérieures de SQL Server, veuillez consulter cette réponse .

canon
la source
Notez que SQL Server requiert ansi_paddingdes index filtrés, assurez-vous donc que cette option est activée en exécutant SET ANSI_PADDING ONavant de créer un index filtré.
naXa
10

Vous pouvez déplacer les enregistrements supprimés vers une table qui n'a pas la contrainte, et peut-être utiliser une vue avec UNION des deux tables pour conserver l'apparence d'une seule table.

Carl Manaster
la source
2
C'est en fait assez 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 peut également accélérer la recherche d'un enregistrement actif car vous pouvez consulter la table d'enregistrement active. Cela accélérerait également la contrainte car la contrainte unique utilise un index par opposition à la contrainte de vérification que j'ai écrite ci-dessous qui doit exécuter un décompte. Je l'aime.
D.Patrick
3

Vous pouvez le faire d'une manière vraiment pirate ...

Créez une vue schématique sur votre table.

CREATE VIEW Quel que soit SELECT * FROM Table WHERE RecordStatus = 1

Créez maintenant une contrainte unique sur la vue avec les champs souhaités.

Une note sur les vues schématisées cependant, si vous modifiez les tables sous-jacentes, vous devrez recréer la vue. Beaucoup de pièges à cause de cela.

Min
la source
C'est une très bonne suggestion, et pas si "hacky". Voici plus d'informations sur cette alternative d'index filtré .
Scott Whitlock
C'est une mauvaise idée. La question n'est pas celle-là.
FabianoLothor
J'ai utilisé une vue schématique une fois et je n'ai jamais répété l'erreur. Ils peuvent être une douleur royale avec laquelle travailler. Ce n'est pas que vous deviez recréer la vue si vous modifiez la table sous-jacente - vous devez potentiellement le faire pour toutes les vues, au moins dans le serveur SQL. C'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 d'abord supprimer les références à celle-ci. Oh, en 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.
MattW
1

Parce que vous allez autoriser les doublons, une contrainte unique ne fonctionnera pas. Vous pouvez créer une contrainte de vérification pour la colonne RecordStatus et une procédure stockée pour INSERT qui vérifie les enregistrements actifs existants avant d'insérer des ID en double.

ichiban
la source
1

Si vous ne pouvez pas utiliser NULL comme RecordStatus comme l'a suggéré Bill, vous pouvez combiner son idée avec un index basé sur une fonction. Créez une fonction qui renvoie NULL si RecordStatus n'est pas l'une des valeurs que vous souhaitez prendre en compte dans votre contrainte (et RecordStatus sinon) et créez un index sur cela.

Cela présente l'avantage de ne pas avoir à examiner explicitement les autres lignes de la table dans votre contrainte, ce qui peut entraîner des problèmes de performances.

Je devrais dire que je ne connais pas du tout le serveur SQL, mais j'ai utilisé avec succès cette approche dans Oracle.

Clochard
la source
bonne idée, mais il n'y a pas de fonction indexée dans le serveur sql, merci pour la réponse
np-hard