【问题标题】: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)

标签: sql sql-server


【解决方案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
    

    【讨论】:

    • 总是迟到...但是感谢这个现实世界的建议 - 我确实知道 - 辅助表中的数据是版本化的(除了键之外还有日期范围),并且我只想从我的主表链接最新版本...
    • 不错的现实世界建议!我可以想象很多遗留应用程序的场景,其中“最佳实践”由于某种原因是不可能的,并且检查约束会很好地工作。
    • @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
      猜你喜欢
      • 1970-01-01
      • 2015-10-03
      • 2013-05-12
      • 2015-08-24
      • 1970-01-01
      • 2020-10-19
      • 1970-01-01
      • 2012-03-02
      • 2021-04-10
      相关资源
      最近更新 更多