T-SQL中的复合键和参照完整性

时间:2016-06-28 14:24:40

标签: sql tsql

在T-SQL中,是否可以使用由1列定义表类型组成的复合键和从表类型列中引用的表中定义行的Id的另一列组成的关系表?

对于共享电子邮件地址示例:
三个不同的用户表(UserA,UserB,UserC)
一个用户类型表(UserType)
一个电子邮件表(EmailAddress)
一个电子邮件 - 用户关系表(EmailRelationship)

EmailRelationship表包含三列,EmailId,UserTypeId和UserId

我可以从每个User表到EmailRelationship表(或其他方式?)建立关系以保持参照完整性吗?

我已经尝试将EmailRelationship表中的所有三列都设为主键,我尝试只将UserTypeId和UserId设为主键。

CREATE TABLE [dbo].[UserType](
[Id] [int] IDENTITY(1,1) NOT NULL ,
[Type] [varchar](50) NOT NULL)
insert into [dbo].[UserType]
([Type])
values
('A'),('B'),('C')

CREATE TABLE [dbo].[UserA](
[Id] [int] IDENTITY(1,1) NOT NULL,
[UserTypeId] [int] NOT NULL,
[Name] [varchar](50) NOT NULL)

insert into [dbo].[UserA]
(UserTypeId,Name)
values
(1,'UserA')

CREATE TABLE [dbo].[UserB](
[Id] [int] IDENTITY(1,1) NOT NULL,
[UserTypeId] [int] NOT NULL,
[Name] [varchar](50) NOT NULL)
insert into [dbo].[UserB]
(UserTypeId,Name)
values
(2,'UserB')

CREATE TABLE [dbo].[UserC](
[Id] [int] IDENTITY(1,1) NOT NULL,
[UserTypeId] [int] NOT NULL,
[Name] [varchar](50) NOT NULL)
insert into [dbo].[UserC]
(UserTypeId,Name)
values
(3,'UserC') 

CREATE TABLE [dbo].[Email](
[Id] [int] IDENTITY(1,1) NOT NULL,
[EmailAddress] [varchar](50) NOT NULL)
insert into [dbo].[email]
(EmailAddress)
values
('SharedEmail@SharedEmail.com')

CREATE TABLE [dbo].[EmailRelationship](
[EmailId] [int] NOT NULL,
[UserTypeId] [int] NOT NULL,
[UserId] [int] NOT NULL)
insert into [dbo].[EmailRelationship]
(EmailId, UserTypeId, UserId)
values
(1,1,1),(1,2,1),(1,3,1)

3 个答案:

答案 0 :(得分:1)

不,没有,外键可以引用一个表,只有一个表,我可以想到三种方法。

第一个是有3列,每个用户表一个列,每个列都有一个外键,还有一个检查约束来检查一个,只有一个值不为空

CREATE TABLE dbo.EmailRelationship
(
        EmailId INT NOT NULL,
        UserTypeId INT NOT NULL,
        UserAId INT NULL,
        UserBId INT NULL,
        UserCId INT NULL,
    CONSTRAINT FK_EmailRelationship__UserAID FOREIGN KEY (UserAId) 
        REFERENCES dbo.UserA (Id),
    CONSTRAINT FK_EmailRelationship__UserBID FOREIGN KEY (UserBId) 
        REFERENCES dbo.UserB (Id),
    CONSTRAINT FK_EmailRelationship__UserCID FOREIGN KEY (UserCId) 
        REFERENCES dbo.UserC (Id),

    CONSTRAINT CK_EmailRelationship__ValidUserId CHECK 
        (CASE WHEN UserTypeID = 1 AND UserAId IS NOT NULL AND ISNULL(UserBId, UserCId) IS NULL THEN 1
                WHEN UserTypeID = 2 AND UserBId IS NOT NULL AND ISNULL(UserAId, UserCId) IS NULL THEN 1
                WHEN UserTypeID = 3 AND UserCId IS NOT NULL AND ISNULL(UserAId, UserBId) IS NULL THEN 1
                ELSE 0
            END = 1)
);

然后,作为一个快速示例,尝试插入用户类型ID为2的UserAId会给您一个错误:

INSERT EmailRelationship (EmailID, UserTypeID, UserAId)
VALUES (1, 1, 1);
  

INSERT语句与CHECK约束冲突" CK_EmailRelationship__ValidUserId"。

第二种方法是只有一个用户表,并存储用户类型以及任何其他常见属性

CREATE TABLE dbo.[User]
(   
        Id INT IDENTITY(1, 1) NOT NULL,
        UserTypeID INT NOT NULL,
        Name VARCHAR(50) NOT NULL,
    CONSTRAINT PK_User__UserID PRIMARY KEY (Id),
    CONSTRAINT FK_User__UserTypeID FOREIGN KEY (UserTypeID) REFERENCES dbo.UserType (UserTypeID),
    CONSTRAINT UQ_User__Id_UserTypeID UNIQUE (Id, UserTypeID)
);
-- NOTE THE UNIQUE CONSTRAINT, THIS WILL BE USED LATER

然后,您可以在电子邮件关系表中使用正常的外键约束:

CREATE TABLE dbo.EmailRelationship
(
        EmailId INT NOT NULL,
        UserId INT NOT NULL,
    CONSTRAINT PK_EmailRelationship PRIMARY KEY (EmailID),
    CONSTRAINT FK_EmailRelationship__EmailId 
        FOREIGN KEY (EmailID) REFERENCES dbo.Email (Id),
    CONSTRAINT FK_EmailRelationship__UserId 
        FOREIGN KEY (UserId) REFERENCES dbo.[User] (Id)
);

然后,不再需要针对电子邮件关系存储UserTypeId,因为您可以加入User以获取此信息。

然后,如果出于某种原因,您确实需要针对不同用户类型的特定表(这不是闻所未闻),您可以创建这些表,并对用户表强制引用完整性:

CREATE TABLE dbo.UserA
(
        UserID INT NOT NULL,
        UserTypeID AS 1 PERSISTED,
        SomeOtherCol VARCHAR(50),
    CONSTRAINT PK_UserA__UserID PRIMARY KEY (UserID),
    CONSTRAINT FK_UserA__UserID_UserTypeID FOREIGN KEY (UserID, UserTypeID) 
        REFERENCES dbo.[User] (Id, UserTypeID)
);

来自UserID的外键和计算列UserTypeID返回User表,确保您只能在此表中输入UserTypeID为1的用户

第三个选项是为每个User表建立一个单独的联结表:

CREATE TABLE dbo.UserAEmailRelationship
(
        EmailId INT NOT NULL,
        UserAId INT NOT NULL,
    CONSTRAINT PK_UserAEmailRelationship PRIMARY KEY (EmailId, UserAId),
    CONSTRAINT FK_UserAEmailRelationship__EmailId FOREIGN KEY (EmailId)
        REFERENCES dbo.Email (Id),
    CONSTRAINT FK_UserAEmailRelationship__UserAId FOREIGN KEY (UserAId)
        REFERENCES dbo.UserA (Id)
);

CREATE TABLE dbo.UserBEmailRelationship
(
        EmailId INT NOT NULL,
        UserBId INT NOT NULL,
    CONSTRAINT PK_UserBEmailRelationship PRIMARY KEY (EmailId, UserBId),
    CONSTRAINT FK_UserBEmailRelationship__EmailId FOREIGN KEY (EmailId)
        REFERENCES dbo.Email (Id),
    CONSTRAINT FK_UserBEmailRelationship__UserBId FOREIGN KEY (UserBId)
        REFERENCES dbo.UserB (Id)
);

每种方法都有其优点和缺点,因此您需要评估最适合您情景的方法。

答案 1 :(得分:0)

不,它不起作用。您不能将列值用作对不同表的动态引用。

一般来说,数据设计存在缺陷。

答案 2 :(得分:-1)

感谢@GarethD我创建了一个CHECK约束,该约束调用了一个强制参照完整性的标量函数(仅在插入时,参考下面的警告):

使用我上面的例子:

alter FUNCTION [dbo].[UserTableConstraint](@Id int, @UserTypeId int)
RETURNS int
AS 
BEGIN
    IF EXISTS (SELECT Id From [dbo].[UserA] WHERE Id = @Id and UserTypeId = @UserTypeId)
    return 1
    ELSE IF EXISTS (SELECT Id From [dbo].[UserB] WHERE Id = @Id and UserTypeId = @UserTypeId)
    return 1
    ELSE IF EXISTS (SELECT Id From [dbo].[UserC] WHERE Id = @Id and UserTypeId = @UserTypeId)
    return 1
    return 0
end;

alter table [dbo].[emailrelationship]
--drop constraint CK_UserType
with CHECK add constraint CK_UserType
CHECK([dbo].[UserTableConstraint](UserId,UserTypeId) = 1)

我确信在CONSTRAINT中对标量函数调用有一个不可忽视的开销。如果上述内容变得令人望而却步,我将在这里报告,尽管有问题的表格不需要处理大量的INSERT。

如果有其他原因不能做到,请听听。谢谢!

更新

我用100k行测试了INSERT和UPDATE(SQL Server 2014,2.1ghz quadcore w / 8gb ram):

  • 在没有CONSTRAINT
  • 的情况下INSERT需要2秒 使用CHECK CONSTRAINT
  • 和3秒

启用IO和TIME STATISTICS会导致INSERT测试运行:

  • 1.7秒,没有CONSTRAINT
  • 使用CHECK CONSTRAINT
  • 和10秒

我将STATISTICS留给UPDATE 100k行测试:

  • 在没有CONSTRAINT的情况下超过1秒
  • 使用CHECK CONSTRAINT
  • 和1.5秒

我引用的表(UserA,UserB,UserC,来自我的例子)每个只包含大约10k行,所以任何想要实现上述内容的人都可能想要运行一些额外的测试,特别是如果你引用的表包含数百万行。 / p>

警告:

上述解决方案可能不适合大多数用途,因为唯一一次检查参照完整性是在INSERT时的CHECK CONSTRAINT期间。对数据的任何其他操作或修改都需要考虑到这一点。例如,使用上述内容,如果删除电子邮件,则任何相关的EmailRelationship条目都将指向无效数据。