非主键的外键

Posted

技术标签:

【中文标题】非主键的外键【英文标题】:Foreign Key to non-primary key 【发布时间】:2013-08-28 09:47:19 【问题描述】:

我有一个保存数据的表,其中一个行需要存在于另一个表中。所以,我想要一个外键来保持引用完整性。

CREATE TABLE table1
(
   ID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
   AnotherID INT NOT NULL,
   SomeData VARCHAR(100) NOT NULL
)

CREATE TABLE table2
(
   ID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
   AnotherID INT NOT NULL,
   MoreData VARCHAR(30) NOT NULL,

   CONSTRAINT fk_table2_table1 FOREIGN KEY (AnotherID) REFERENCES table1 (AnotherID)
)

但是,如您所见,我的外键表中的列不是 PK。有没有办法创建这个外键,或者有更好的方法来维护这个引用完整性?

【问题讨论】:

这样做没有多大意义。为什么不参考table1.ID 如果你的 AnothidID 不是主键,它应该是 ForeignKey,所以作为 ForeignKey,你的 table2 应该指向同一个表(可能是 table3) 【参考方案1】:

如果您确实想为非主键创建外键,则它必须是具有唯一约束的列。

来自Books Online:

FOREIGN KEY 约束不必只链接到 PRIMARY 另一个表中的 KEY 约束;也可以定义为引用 另一个表中 UNIQUE 约束的列。

因此,在您的情况下,如果您使 AnotherID 唯一,它将被允许。如果你不能应用唯一的约束,那你就不走运了,但仔细想想,这确实是有道理的。

尽管如前所述,如果您有一个非常好的主键作为候选键,为什么不使用它呢?

【讨论】:

与您的最后一个问题有关...我有一种情况,我希望复合候选键成为主键只是,因为它在语义上更重要并描述了我的模型最好。为了性能起见,我也希望有一个外键引用一个新创建的代理键(如上所述)。有没有人预见到这样的设置会出现任何问题? 先生,您能告诉我外键总是引用具有唯一约束的属性背后的逻辑吗? 如何在 asp net MVC 5 中做到这一点 普通的非主键整数可以在其他表中声明为外键吗?像这个。这可能吗?创建表项目(PSLNO Numeric(8,0) Not Null, PrMan Numeric(8,0), StEng Numeric(8,0), CONSTRAINT PK_Project PRIMARY KEY (PSLNO), CONSTRAINT FK_Project1 FOREIGN KEY (PrMan) REFERENCES Employee(EmpID) , CONSTRAINT FK_Project2 FOREIGN KEY (StEng) REFERENCES Employee(EmpID), ) @ShivangiGupta 非空外键必须是唯一标识主记录的值,与一条记录中的引用列完全匹配。引用主键可以保证这一点。如果返回的引用键可以识别多条记录,那么您将无法确定您引用了正确的记录。【参考方案2】:

正如其他人所指出的,理想情况下,外键将被创建为对主键(通常是 IDENTITY 列)的引用。然而,我们并不是生活在一个理想的世界中,有时即使是对模式的“小”更改也会对应用程序逻辑产生重大的连锁反应。

考虑具有 SSN 列(和哑主键)的 Customer 表和还包含 SSN 列(由来自 Customer 数据的业务逻辑填充,但不存在 FK)的 Claim 表的情况。该设计存在缺陷,但已经使用了几年,并且在该模式上构建了三个不同的应用程序。很明显,删除 Claim.SSN 并建立真正的 PK-FK 关系将是理想的,但也将是一个重大的大修。另一方面,对 Customer.SSN 设置一个 UNIQUE 约束,并在 Claim.SSN 上添加一个 FK,可以提供引用完整性,对应用程序几乎没有影响。

不要误会我的意思,我完全赞成正常化,但有时实用主义胜过理想主义。如果可以用创可贴来帮助平庸的设计,则可以避免手术。

【讨论】:

【参考方案3】:

死灵术。 我假设当有人登陆这里时,他需要一个外键来列在包含非唯一键的表中。 问题是,如果你有这个问题,数据库模式是非规范化的。

例如,您将房间保存在一个表中,其中包含一个房间 uid 主键、一个 DateFrom 和一个 DateTo 字段,以及另一个 uid,这里是 RM_ApertureID 来跟踪同一个房间,以及一个软删除字段,像 RM_Status,其中 99 表示“已删除”, 99 表示“活动”。

因此,当您创建第一个房间时,您插入 RM_UID 和 RM_ApertureID 作为与 RM_UID 相同的值。 然后,当您将房间终止到某个日期,并使用新的日期范围重新建立它时,RM_UID 为 newid(),而上一个条目中的 RM_ApertureID 将成为新的 RM_ApertureID。

因此,如果是这种情况,RM_ApertureID 是一个非唯一字段,因此您不能在另一个表中设置外键。

并且没有办法将外键设置为非唯一列/索引,例如在 T_ZO_REM_AP_Raum_Reinigung 中(其中 RM_UID 实际上是 RM_ApertureID)。 但是要禁止无效值,需要设置外键,否则迟早会造成数据垃圾……

现在您可以在这种情况下(无需重写整个应用程序)插入一个 CHECK 约束,并使用一个标量函数检查密钥的存在:

IF  EXISTS (SELECT * FROM sys.check_constraints WHERE object_id = OBJECT_ID(N'[dbo].[Check_RM_ApertureIDisValid_T_ZO_REM_AP_Raum_Reinigung]') AND parent_object_id = OBJECT_ID(N'[dbo].[T_ZO_REM_AP_Raum_Reinigung]'))
ALTER TABLE dbo.T_ZO_REM_AP_Raum_Reinigung DROP CONSTRAINT [Check_RM_ApertureIDisValid_T_ZO_REM_AP_Raum_Reinigung]
GO


IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[fu_Constaint_ValidRmApertureId]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
DROP FUNCTION [dbo].[fu_Constaint_ValidRmApertureId]
GO




CREATE FUNCTION [dbo].[fu_Constaint_ValidRmApertureId](
     @in_RM_ApertureID uniqueidentifier 
    ,@in_DatumVon AS datetime 
    ,@in_DatumBis AS datetime 
    ,@in_Status AS integer 
) 
    RETURNS bit 
AS 
BEGIN   
    DECLARE @bNoCheckForThisCustomer AS bit 
    DECLARE @bIsInvalidValue AS bit 
    SET @bNoCheckForThisCustomer = 'false' 
    SET @bIsInvalidValue = 'false' 

    IF @in_Status = 99 
        RETURN 'false' 


    IF @in_DatumVon > @in_DatumBis 
    BEGIN 
        RETURN 'true' 
    END 


    IF @bNoCheckForThisCustomer = 'true'
        RETURN @bIsInvalidValue 


    IF NOT EXISTS
    ( 
        SELECT 
             T_Raum.RM_UID 
            ,T_Raum.RM_Status 
            ,T_Raum.RM_DatumVon 
            ,T_Raum.RM_DatumBis 
            ,T_Raum.RM_ApertureID 
        FROM T_Raum 
        WHERE (1=1) 
        AND T_Raum.RM_ApertureID = @in_RM_ApertureID 
        AND @in_DatumVon >= T_Raum.RM_DatumVon 
        AND @in_DatumBis <= T_Raum.RM_DatumBis 
        AND T_Raum.RM_Status <> 99  
    ) 
        SET @bIsInvalidValue = 'true' -- IF ! 

    RETURN @bIsInvalidValue 
END 



GO



IF  EXISTS (SELECT * FROM sys.check_constraints WHERE object_id = OBJECT_ID(N'[dbo].[Check_RM_ApertureIDisValid_T_ZO_REM_AP_Raum_Reinigung]') AND parent_object_id = OBJECT_ID(N'[dbo].[T_ZO_REM_AP_Raum_Reinigung]'))
ALTER TABLE dbo.T_ZO_REM_AP_Raum_Reinigung DROP CONSTRAINT [Check_RM_ApertureIDisValid_T_ZO_REM_AP_Raum_Reinigung]
GO


-- ALTER TABLE dbo.T_AP_Kontakte WITH CHECK ADD CONSTRAINT [Check_RM_ApertureIDisValid_T_ZO_REM_AP_Raum_Reinigung]  
ALTER TABLE dbo.T_ZO_REM_AP_Raum_Reinigung WITH NOCHECK ADD CONSTRAINT [Check_RM_ApertureIDisValid_T_ZO_REM_AP_Raum_Reinigung] 
CHECK 
( 
    NOT 
    ( 
        dbo.fu_Constaint_ValidRmApertureId(ZO_RMREM_RM_UID, ZO_RMREM_GueltigVon, ZO_RMREM_GueltigBis, ZO_RMREM_Status) = 1 
    ) 
) 
GO


IF  EXISTS (SELECT * FROM sys.check_constraints WHERE object_id = OBJECT_ID(N'[dbo].[Check_RM_ApertureIDisValid_T_ZO_REM_AP_Raum_Reinigung]') AND parent_object_id = OBJECT_ID(N'[dbo].[T_ZO_REM_AP_Raum_Reinigung]')) 
ALTER TABLE dbo.T_ZO_REM_AP_Raum_Reinigung CHECK CONSTRAINT [Check_RM_ApertureIDisValid_T_ZO_REM_AP_Raum_Reinigung] 
GO

【讨论】:

总是迟到...但是感谢这个现实世界的建议 - 我确实知道 - 辅助表中的数据是版本化的(除了键之外还有日期范围),并且我只想从我的主表链接最新版本... 不错的现实世界建议!我可以想象很多遗留应用程序的场景,其中“最佳实践”由于某种原因是不可能的,并且检查约束会很好地工作。 此解决方案不可靠。见:dba.stackexchange.com/…/how-are-my-sql-server-constraints-being-bypassed @stomy:这不是一个解决方案。解决方案是规范化模式。但这需要一个通常没有的时间(可悲的是)。 sql-server 中的标量函数非常慢并不是什么新鲜事。有人可以同时/稍后修改 UDF 中引用的表是很清楚的,并且检查约束也不会捕获应该清楚的。但总比没有好,除非你需要做很多插入,在这种情况下你可以禁用 CHECK 约束并在之后重新启用它。 (检查数据前后是否有效)。但是,是的,强制执行 ref integ。这样是不可靠的。【参考方案4】:

如果表是一对多关系,主键总是需要唯一,外键需要允许非唯一值。如果表是通过一对一关系而不是一对多关系连接的,则使用外键作为主键是完全可以的。

FOREIGN KEY 约束不必只链接到另一个表中的 PRIMARY KEY 约束;它也可以定义为引用另一个表中唯一约束的列。

【讨论】:

【参考方案5】:

是的,您通常至少会将其编入索引。

create table student(
    id int,
    name varchar(30),
    index inName(id)
);

CREATE TABLE grade(
    id int,
    subject varchar(30),
    mark double,
    foreign key(id) references student(id)
);

【讨论】:

索引是不够的。这里的第一个答案已经告诉你它必须是一个唯一的索引/约束,甚至引用了手册!如果您实际上测试了自己的答案,您会发现它没有运行...dbfiddle.uk/… 但是我提到的代码在 mysql 上运行流畅。 @MatBailie 问题标记为 SQL-Server,这不是 MySQL。 谢谢。 @MatBailie

以上是关于非主键的外键的主要内容,如果未能解决你的问题,请参考以下文章

数据库—超键候选键主键外键

使用非主键的列创建外键

EntityFramework db.where()查询符合条件数据主键的外键的外键 在括号里怎么写

3.啥是键、候选键、主键和外键?

SQL Server 中的“键”是啥意思?主键和外键是啥?

MySQL面试题