2009-05-14 9 views
72

J'ai une situation où je dois imposer une contrainte unique sur un ensemble de colonnes, mais seulement pour une valeur d'une colonne. Ainsi, par exemple, j'ai une table comme Table (ID, Name, RecordStatus)contrainte unique conditionnelle

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) seulement lorsque RecordStatus = 1, car je me fiche de savoir s'il y a plusieurs suppressions enregistrements avec le même ID. En dehors de l'écriture des déclencheurs, est-ce que je peux le faire?

J'utilise SQL Server 2005.

+1

Cette conception est une douleur commune. Avez-vous envisagé de modifier la conception de sorte que les enregistrements «supprimés» théoriquement soient physiquement supprimés de la table et éventuellement déplacés vers une table «archive»? – onedaywhen

+0

... car l'impossibilité d'écrire une contrainte UNIQUE pour appliquer une simple clé devrait être considérée comme une «odeur de code», IMO. Si vous ne pouvez pas changer le design (SQL DDL) parce que de nombreuses autres tables référencent cette table, je parierais que votre SQL DML souffre également de cela, vous devez vous souvenir d'ajouter ... ET Table.RecordStatus = 1 ' à la plupart des conditions de recherche et de rejoindre les conditions impliquant cette table et subissant des bugs subtils quand il est inévitablement omis à l'occasion. – onedaywhen

Répondre

33

Ajoutez une contrainte de vérification comme celle-ci. La différence est, vous retournerez false si Status = 1 et le nombre> 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

J'ai regardé les contraintes de vérification au niveau de la table, mais ne regarde pas il ya un moyen de passer les valeurs en cours d'insertion ou mis à jour à la fonction, savez-vous comment? –

+0

Bon, j'ai posté un exemple de script qui vous aidera à prouver de quoi je parle. Je l'ai testé et ça fonctionne. 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 en existe déjà un actif. Vous pouvez modifier la logique de sorte que s'il y en a une active, vous ne pouvez pas ajouter d'élément ayant le même identifiant. Avec ce modèle, les possibilités sont à peu près infinies. –

+0

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 fait 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 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: http://blogs.conchango.com/davidportas/archive/2007/02/19/Trouble-with-CHECK-Constraints.aspx – onedaywhen

1

Parce que, vous allez permettre des 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.

9

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

+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 contient beaucoup de lignes, cela peut également accélérer la recherche d'un enregistrement actif, car vous pouvez consulter la table d'enregistrements active. Cela accélérerait aussi la contrainte parce que la contrainte unique utilise un index contrairement à la contrainte de vérification que j'ai écrite ci-dessous qui doit exécuter un compte. J'aime ça. –

3

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

Créer une vue schemabound sur ta table.

Créer une vue Quelle que soit SELECT * FROM table OÙ RecordStatus = 1

Créez maintenant une contrainte unique sur la vue avec les champs que vous souhaitez. Cependant, si vous modifiez les tables sous-jacentes, vous devrez recréer la vue. Beaucoup de choses à cause de cela.

+0

Ceci est une très bonne suggestion, et pas que "hacky". Voici plus d'informations à ce propos [alternative à l'index filtré] (http://noelmckinney.com/2010/10/filtered-index-alternative/). –

+0

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

+0

J'ai utilisé une vue schemabound une fois, et je n'ai jamais répété l'erreur. Ils peuvent être une douleur royale à travailler avec. Ce n'est pas que vous devez recréer la vue si vous changez la table sous-jacente - vous devez potentiellement faire cela pour toutes les vues, au moins dans le serveur SQL. C'est que vous ne pouvez pas changer la table sans d'abord laisser tomber la vue, ce que vous pourriez ne pas pouvoir faire sans d'abord laisser tomber des références à elle. Oh, 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

Si vous ne pouvez pas utiliser NULL comme RecordStatus comme suggéré par Bill, vous pouvez combiner son idée avec un index basé sur une fonction. Créez une fonction qui renvoie NULL si le RecordStatus n'est pas l'une des valeurs que vous voulez prendre en compte dans votre contrainte (et le RecordStatus sinon) et créez un index dessus. Cela aura l'avantage de ne pas avoir à examiner explicitement les autres lignes de la table dans votre contrainte, ce qui pourrait vous causer des problèmes de performance.

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

+0

bonne idée, mais aucune fonction indexée dans SQL Server merci pour la réponse si –

104

Voyez, the filtered index. De la documentation (emphase mienne):

Un index filtré est un index non ordonné optimisé particulièrement adapté pour couvrir des requêtes qui choisissent parmi 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

Cette applique essentiellement unicité de ID quand RecordStatus est 1.

Remarque: l'index filtré a été introduit dans SQL Server 2008. Pour les versions antérieures de SQL Server, consultez this answer.

+0

Notez que SQL Server nécessite 'ansi_padding' pour les index filtrés, assurez-vous que cette option est activée en exécutant' SET ANSI_PADDING ON' avant créer un index filtré. – naXa

Questions connexes