web-dev-qa-db-fra.com

Contrainte unique SQL sur plusieurs tables

J'essaie de créer une contrainte unique sur plusieurs tables. J'ai trouvé des réponses similaires ici, mais elles ne reflètent pas tout à fait l'esprit de ce que j'essaie de faire.

Par exemple, j'ai trois tables, t_Analog, t_Discrete, t_Message

CREATE TABLE t_Analog(
    [AppName] [nvarchar](20) NOT NULL,
    [ItemName] [nvarchar](32) NOT NULL,
    [Value] [float] NOT NULL,
    CONSTRAINT [uc_t_Analog] UNIQUE(AppName, ItemName)
)

CREATE TABLE t_Discrete(
    [AppName] [nvarchar](20) NOT NULL,
    [ItemName] [nvarchar](32) NOT NULL,
    [Value] [bit] NOT NULL,
    CONSTRAINT [uc_t_Discrete] UNIQUE(AppName, ItemName)
)

CREATE TABLE t_Message(
    [AppName] [nvarchar](20) NOT NULL,
    [ItemName] [nvarchar](32) NOT NULL,
    [Value] [nvarchar](256) NOT NULL,
    CONSTRAINT [uc_t_Message] UNIQUE(AppName, ItemName)
)

Mon objectif est de rendre AppName et ItemName uniques sur les 3 tables. Par exemple, un nom d'élément de Y dans l'application X ne peut pas exister dans les tables analogiques et discrètes.

Veuillez noter que cet exemple est artificiel, les données réelles pour chaque type sont différentes et suffisamment volumineuses pour rendre la combinaison de tables et l'ajout d'une colonne de type assez moche.

Si vous avez des suggestions sur les approches à adopter, j'aimerais les entendre!

---- BEGIN EDIT 2012-04-26 13:28 CST ----

Merci à tous pour vos réponses!

Il semble qu'il y ait une raison de modifier le schéma de cette base de données, et c'est très bien.

La combinaison des tables en une seule table n'est pas vraiment une option viable car il y a environ 30 colonnes pour chaque type qui ne correspondent pas (la modification de ces colonnes n'est malheureusement pas une option). Cela pourrait conduire à de grandes sections de colonnes non utilisées dans chaque ligne, ce qui semble être une mauvaise idée.

L'ajout d'une 4ème table, comme John Sikora et d'autres l'ont mentionné, peut être une option mais je voudrais vérifier ceci en premier.

Modifier le schéma pour être:

CREATE TABLE t_AllItems(
    [id] [bigint] IDENTITY(1,1) NOT NULL,
    [itemType] [int] NOT NULL,
    [AppName] [nvarchar](20) NOT NULL,
    [ItemName] [nvarchar](32) NOT NULL,
    CONSTRAINT [pk_t_AllItems] PRIMARY KEY CLUSTERED ( [id] )
    CONSTRAINT [uc_t_AllItems] UNIQUE([id], [AppName], [ItemName])
) ON [PRIMARY]

CREATE TABLE t_Analog(
    [itemId] [bigint] NOT NULL,
    [Value] [float] NOT NULL,
    FOREIGN KEY (itemId) REFERENCES t_AllItems(id)
)

CREATE TABLE t_Discrete(
    [itemId] [bigint] NOT NULL,
    [Value] [bit] NOT NULL,
    FOREIGN KEY (itemId) REFERENCES t_AllItems(id)
)

CREATE TABLE t_Message(
    [itemId] [bigint] NOT NULL,
    [Value] [nvarchar](256) NOT NULL,
    FOREIGN KEY (itemId) REFERENCES t_AllItems(id)
)

J'ai seulement une question concernant cette approche. Cela impose-t-il l'unicité des sous-tables?

Par exemple, ne pourrait-il pas exister un 'Item' ayant 'id' 9 avec les tables t_Analog ayant 'itemId' de 9 avec 'valeur' ​​de 9.3 et, en même temps, t_Message ayant 'itemId' 9 ​​avec 'Valeur' "foo"?

Je ne comprends peut-être pas tout à fait cette approche de table supplémentaire mais je ne suis pas contre.

S'il vous plaît corrigez-moi si je me trompe à ce sujet.

22
CoryC

Ajoutez une 4ème table spécifiquement pour ces valeurs que vous voulez être uniques, puis liez ces clés de cette table aux autres en utilisant une relation un à plusieurs. Par exemple, vous aurez la table unique avec un ID, AppName et ItemName pour constituer ses 3 colonnes. Ensuite, avoir ce lien de table vers les autres.

Voici un bon exemple de la façon de procéder. Créez une relation un à plusieurs avec SQL Server

EDIT: C'est ce que je ferais, mais compte tenu de vos besoins en matière de serveur, vous pouvez changer ce dont vous avez besoin:

CREATE TABLE AllItems(
    [id] [int] IDENTITY(1,1) NOT NULL,
    [itemType] [int] NOT NULL,
    [AppName] [nvarchar](20) NOT NULL,
    [ItemName] [nvarchar](32) NOT NULL,
    CONSTRAINT [pk_AllItems] PRIMARY KEY CLUSTERED ( [id] ASC )
) ON [PRIMARY]

CREATE TABLE Analog(
    [itemId] [int] NOT NULL,
    [Value] [float] NOT NULL
)

CREATE TABLE Discrete(
    [itemId] [int] NOT NULL,
    [Value] [bit] NOT NULL
)

CREATE TABLE Message(
    [itemId] [bigint] NOT NULL,
    [Value] [nvarchar](256) NOT NULL
)

ALTER TABLE [Analog] WITH CHECK 
    ADD CONSTRAINT [FK_Analog_AllItems] FOREIGN KEY([itemId])
REFERENCES [AllItems] ([id])
GO
ALTER TABLE [Analog] CHECK CONSTRAINT [FK_Analog_AllItems]
GO

ALTER TABLE [Discrete] WITH CHECK 
    ADD CONSTRAINT [FK_Discrete_AllItems] FOREIGN KEY([itemId])
REFERENCES [AllItems] ([id])
GO
ALTER TABLE [Discrete] CHECK CONSTRAINT [FK_Discrete_AllItems]
GO

ALTER TABLE [Message] WITH CHECK 
    ADD CONSTRAINT [FK_Message_AllItems] FOREIGN KEY([itemId])
REFERENCES [AllItems] ([id])
GO
ALTER TABLE [Message] CHECK CONSTRAINT [FK_Message_AllItems]
GO

D'après ce que je peux dire, votre syntaxe est correcte, je l'ai simplement changée de cette façon simplement parce que je la connais mieux, mais que cela devrait fonctionner.

12
John Sykor

Bien que vous souhaitiez ou non modifier votre schéma, comme d’autres réponses, un vue indexée peut appliquer la contrainte dont vous parlez:

CREATE VIEW v_Analog_Discrete_Message_UK WITH SCHEMABINDING AS
SELECT a.AppName, a.ItemName
FROM dbo.t_Analog a, dbo.t_Discrete b, dbo.t_Message c, dbo.Tally t
WHERE (a.AppName = b.AppName and a.ItemName = b.ItemName)
    OR (a.AppName = c.AppName and a.ItemName = c.ItemName)
    OR (b.AppName = c.AppName and b.ItemName = c.ItemName)
    AND t.N <= 2
GO
CREATE UNIQUE CLUSTERED INDEX IX_AppName_ItemName_UK
    ON v_Analog_Discrete_Message_UK (AppName, ItemName)
GO

Vous aurez besoin d'un "Tally" ou tableau de nombres ou vous devrez en générer un à la volée, à la manière de Celko:

-- Celko-style derived numbers table to 100k
select a.N + b.N * 10 + c.N * 100 + d.N * 1000 + e.N * 10000 + 1 as N
from (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) a
      , (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) b
      , (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) c
      , (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) d
      , (select 0 as N union all select 1 union all select 2 union all select 3 union all select 4 union all select 5 union all select 6 union all select 7 union all select 8 union all select 9) e
order by N
9
Tim Lehner

Une idée pourrait être de combiner les trois tableaux:

CREATE TABLE t_Generic(
[AppName] [nvarchar](20) NOT NULL,
[ItemName] [nvarchar](32) NOT NULL,
[Type] [nvarchar](32) NOT NULL,
[AnalogValue] [Float] NULL,
[DiscreteValue] [bit] NULL,
[MessageValue] [nvarchar](256) NULL,
CONSTRAINT [uc_t_Generic] UNIQUE(AppName, ItemName)
)

Votre logique d'application devrait imposer qu'une seule valeur a été renseignée et vous pouvez utiliser un champ Type pour garder une trace du type de cet enregistrement.

1
Victor Bruno

Vous pouvez également créer une contrainte comportant un peu plus de logique et vérifiant les trois tables.

Jetez un coup d'oeil ici pour un exemple de la façon de faire ceci en utilisant une fonction.

1
zimdanen

J'ai utilisé au lieu d'insérer et de mettre à jour des déclencheurs pour résoudre ce problème comme suit:

CREATE TRIGGER tI_Analog ON t_Analog
INSTEAD OF INSERT
AS 
BEGIN
    SET NOCOUNT ON ;

    IF EXISTS (SELECT 1 FROM inserted AS I INNER JOIN t_Analog AS T
                   ON T.AppName = I.AppName AND T.ItemName = I.ItemName
               UNION ALL
               SELECT 1 FROM inserted AS I INNER JOIN t_Discrete AS T
                   ON T.AppName = I.AppName AND T.ItemName = I.ItemName
               UNION ALL
               SELECT 1 FROM inserted AS I INNER JOIN t_Message AS T
                   ON T.AppName = I.AppName AND T.ItemName = I.ItemName
              )
    BEGIN
        RAISERROR('Duplicate key', 16, 10) ;
    END
    ELSE
    BEGIN
        INSERT INTO t_Analog ( AppName, ItemName, Value )
        SELECT AppName, ItemName, Value FROM inserted ;
    END
END
GO

CREATE TRIGGER tU_Analog ON t_Analog
INSTEAD OF UPDATE
AS 
BEGIN
    SET NOCOUNT ON ;

    IF EXISTS (SELECT TOP(1) 1
                 FROM (SELECT T.AppName, T.ItemName, COUNT(*) AS numRecs
                         FROM
                            (SELECT I.AppName, I.ItemName
                               FROM inserted AS I INNER JOIN t_Analog AS T
                                 ON T.AppName = I.AppName AND T.ItemName = I.ItemName
                             UNION ALL
                             SELECT I.AppName, I.ItemName
                               FROM inserted AS I INNER JOIN t_Discrete AS T
                                 ON T.AppName = I.AppName AND T.ItemName = I.ItemName
                             UNION ALL
                             SELECT I.AppName, I.ItemName
                               FROM inserted AS I INNER JOIN t_Message AS T
                                 ON T.AppName = I.AppName AND T.ItemName = I.ItemName
                            ) AS T
                          GROUP BY T.AppName, T.ItemName
                        ) AS T
                WHERE T.numRecs > 1
              )
    BEGIN
        RAISERROR('Duplicate key', 16, 10) ;
    END
    ELSE
    BEGIN
        UPDATE T
           SET AppName = I.AppName
             , ItemName = I.ItemName
             , Value = I.Value
          FROM inserted AS I INNER JOIN t_Message AS T
            ON T.AppName = I.AppName AND T.ItemName = I.ItemName
        ;
    END
END
GO

Un avertissement avec utilisation au lieu de déclencheurs est lorsqu'un champ d'identité est impliqué. Ce déclencheur empêche la clause OUTPUT de la commande INSERT INTO et la variable @@ IDENTITY de fonctionner correctement.

0
Michael Erickson

Cela suggérerait un problème de normalisation/conception de base de données, en particulier vous devriez avoir le nom de l'application stocké dans une seule table (en tant qu'unique/clé, quelle qu'elle soit), puis une 2ème colonne indiquant l'ID de ce à quoi elle est liée, et éventuellement une 3ème colonne indiquant le type.

PAR EXEMPLE:

AppName – PrimaryKey - unique
ID – Foreign Key of either Discrete, Analog or message
Type – SMALLINT representing Discrete, analog or message.
0
HeavenCore