【问题标题】:SQL Unique constraint across multiple tables跨多个表的 SQL 唯一约束
【发布时间】:2012-05-07 10:52:02
【问题描述】:

我正在尝试跨多个表创建唯一约束。我发现这里回答了类似的问题,但它们并没有完全抓住我想要做的事情的精神。

例如,我有三个表,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)
)

我的目标是让 AppName 和 ItemName 在所有 3 个表中都是唯一的。例如,应用程序 X 中的项目名称 Y 不能同时存在于模拟表和离散表中。

请注意,此示例是人为设计的,每个 Type 的实际数据不同且大到足以使组合表和添加 Type 列变得非常难看。

如果您对此方法有任何建议,我很乐意听取他们的意见!

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

谢谢大家的回答!

看来可能需要修改这个数据库的架构,没关系。

将这些表组合成一个表并不是一个真正可行的选择,因为每种类型大约有 30 列不匹配(遗憾的是,修改这些列不是一种选择)。这可能会导致每一行中没有使用大部分列,这似乎是个坏主意。

像 John Sikora 和其他人提到的那样,添加第四张桌子可能是一种选择,但我想先验证一下。

将架构修改为:

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)
)

我对这种方法只有一个问题。这会在子表中强制执行唯一性吗?

例如,是否不存在具有 'id' 9 的 'Item' 表 t_Analog 的 'itemId' 为 9 和 'value' 为 9.3,同时 t_Message 的 'itemId' 为 9 和 ' “foo”的值?

我可能不完全理解这种额外的表格方法,但我并不反对。

如果我在这方面错了,请纠正我。

【问题讨论】:

  • 您在正确的轨道上,但完整性约束还不够好。例如,ID 号 100 可能出现在每个表中。要更严格地使用项目类型,请参阅 this answerthis answer
  • 这不是一个约束,但你可以使用 CREATE SEQUENCE 来获取一个唯一的数字来放入每条记录:docs.microsoft.com/en-us/sql/t-sql/statements/… 我认为这个功能是在 2012 版中添加的。

标签: sql-server database-design unique-constraint


【解决方案1】:

专门为您希望唯一的这些值添加第 4 个表,然后使用一对多关系将此表中的这些键链接到其他表中。 例如,您将拥有一个具有 ID、AppName 和 ItemName 的唯一表来组成其 3 列。然后将此表链接到其他表。

这里是一个很好的例子 Create a one to many relationship using SQL Server

编辑:这是我会做的,但考虑到您的服务器需求,您可以更改需要的内容:

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

据我所知,您的语法很好,我只是将其更改为这种方式只是因为我更熟悉它,但两者都应该可以工作。

【讨论】:

  • 约翰,我编辑了我的帖子以添加我相信您正在谈论的架构。您能否验证以确保我理解您的建议?谢谢:)
  • 如果你看我的编辑,你会看到我习惯了,但你的语法对我来说很好
  • 你在正确的轨道上,但这还不够好。问题是没有任何约束可以防止 ID 号 100 出现在这些表中的每一个中。要更严格地使用项目类型,请参阅 this answerthis answer
  • 我个人只会让查询弥补 in 而不是创建这样的深入表。例如。一旦在我们的主表中使用了一个 ID,我们的其他 2 个表就不再可以使用它。所以基本上我会在第一个表上添加一个标志,说明它正在使用中,如果选中该标志,它就不再可访问。还请查看 catcall 的答案,我只是不熟悉它。
  • Catcall,您的回答正是我所需要的。感谢 John 和 Cat 的帮助!
【解决方案2】:

虽然您可能想也可能不想改变您的架构,就像其他答案所说的那样,indexed view 可以应用您正在谈论的约束:

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

您将需要一个"Tally" or numbers table 或者必须以其他方式动态生成一个,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

【讨论】:

  • 谢谢,我觉得这是最优雅的解决方案
  • 我同意 - 这是最优雅的解决方案。但是,您不需要包含 100,000 行的完整计数表,只需两行。你有 WHERE ... t.N
  • 幸运的是,在这种情况下,MS SQL 将 NULL 视为唯一约束的值(这当然是错误的),因此它甚至适用于表 b 和 c 之间重复的值,在这种情况下a.AppName、a.ItemName 均为 NULL。两个错误在这里正确
  • P L U S _ O N E 无论如何 - 这真的很有帮助
  • 改进的 Tally 表生成: -- Celko 风格派生数字表到 100k SELECT aN + bN * 10 + cN * 100 + dN * 1000 + eN * 10000 + 1 as N FROM (SELECT *从 (值 (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) AS t(N)) AS a , (SELECT * FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) AS t (N)) AS b , (SELECT * FROM (值 (0),(1),(2),(3),(4),(5),(6),(7),(8),( 9)) AS t(N)) AS c , (SELECT * FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7), (8),(9)) AS t(N)) AS d , (SELECT * FROM (VALUES (0),(1),(2),(3),(4),(5),(6) ,(7),(8),(9)) AS t(N)) AS e ORDER BY N
【解决方案3】:

一个想法可能是合并三个表:

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)
)

您的应用程序逻辑必须强制只填充一个值,并且您可以使用类型字段来跟踪该记录的类型。

【讨论】:

    【解决方案4】:

    您还可以创建一个具有更多逻辑的约束并检查所有三个表。

    查看here 以了解如何使用函数执行此操作的示例。

    【讨论】:

    • Zimdanen,您能否举例说明此约束如何检查多个表?
    • 这是一个带有函数的选项:stackoverflow.com/questions/2588072/…
    • zimdanen,看起来很有希望,我可能会采用这种方法。
    • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
    • @ice1000:这适用于指向 SO 的链接吗?
    【解决方案5】:

    这表明存在规范化/数据库设计问题,特别是您应该将应用程序名称单独存储在一个表中(作为唯一/键),然后是第二列,表示它所链接的 ID,也许第三列指示类型。

    EG:

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

    【讨论】:

      【解决方案6】:

      我使用而不是插入和更新触发器来解决此问题,如下所示:

      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
      

      使用而不是触发器的一个警告是涉及身份字段时。此触发器会阻止 INSERT INTO 命令的 OUTPUT 子句和 @@IDENTITY 变量正常工作。

      【讨论】:

        猜你喜欢
        • 2021-12-13
        • 2018-09-14
        • 2011-03-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-06-29
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多