PAYATHON 2026

SQL71516:FOREIGN KEY 无法引用复合 PRIMARY KEY

支付小周

结论

报错的原因是:CustomerHistory 使用复合主键 ([ID], [CustomerID]),外键却只引用了其中的 [ID]。在 SQL Server 中,外键引用的列必须完整对应被引用表的 PRIMARY KEY 或 UNIQUE candidate key,不能只引用复合主键中的一部分。

更合适的做法通常是让 CustomerHistory.ID 单独作为主键。它是 IDENTITY 列,适合用来唯一标识每条历史记录:

CONSTRAINT [PK_CUSTOMERHISTORY]
PRIMARY KEY CLUSTERED ([ID] ASC)

这样,CustomerPaymentHistory.HistoryID 就能合法引用 CustomerHistory.ID。

推荐方案:让 ID 成为单列主键

修改 CustomerHistory:

CREATE TABLE [dbo].[CustomerHistory]
(
    [ID] INT IDENTITY (1, 1) NOT NULL,
    [HistoryID] AS
        ('CLI' + RIGHT(
            '000000000' + CONVERT([varchar](10), [ID]),
            5
        )) PERSISTED NOT NULL,
    [CustomerID] INT NOT NULL,
    [InvoiceNumber] NVARCHAR(30) NOT NULL,
    [InvoiceDate] DATETIME NOT NULL,
    [InvoiceTotal] NVARCHAR(20) NOT NULL,
    [Balance] NVARCHAR(20) NOT NULL,

    CONSTRAINT [PK_CUSTOMERHISTORY]
        PRIMARY KEY CLUSTERED ([ID] ASC),

    CONSTRAINT [FK_CUSTOMER_CH]
        FOREIGN KEY ([CustomerID])
        REFERENCES [dbo].[Customers] ([ID])
);

CustomerPaymentHistory 中的外键可以保持不变:

CONSTRAINT [FK_CUSTOMERHISTORY_CPH]
    FOREIGN KEY ([HistoryID])
    REFERENCES [dbo].[CustomerHistory] ([ID])

如果付款表中的 ID 也是全表唯一的自增标识,主键通常不需要再包含 HistoryID:

CREATE TABLE [dbo].[CustomerPaymentHistory]
(
    [ID] INT IDENTITY (1, 1) NOT NULL,
    [PaymentID] AS
        ('CLI' + RIGHT(
            '000000000' + CONVERT([varchar](10), [ID]),
            5
        )) PERSISTED NOT NULL,
    [HistoryID] INT NOT NULL,
    [PaymentDate] DATETIME NOT NULL,
    [PaymentAmount] NVARCHAR(20) NOT NULL,
    [BalanceDue] NVARCHAR(20) NULL,
    [PaidInFull] BIT NOT NULL,

    CONSTRAINT [PK_CUSTOMERPAYMENTHISTORY]
        PRIMARY KEY CLUSTERED ([ID] ASC),

    CONSTRAINT [FK_CUSTOMERHISTORY_CPH]
        FOREIGN KEY ([HistoryID])
        REFERENCES [dbo].[CustomerHistory] ([ID])
);

这种结构表示一条 CustomerHistory 记录可以对应多条付款记录,每条付款记录通过 HistoryID 指向一条历史记录。

方案二:保留复合主键,为 ID 添加 candidate key

如果业务上必须保留 ([ID], [CustomerID]) 作为主键,可以为 [ID] 单独添加 UNIQUE 约束:

CREATE TABLE [dbo].[CustomerHistory]
(
    [ID] INT IDENTITY (1, 1) NOT NULL,
    [HistoryID] AS
        ('CLI' + RIGHT(
            '000000000' + CONVERT([varchar](10), [ID]),
            5
        )) PERSISTED NOT NULL,
    [CustomerID] INT NOT NULL,
    [InvoiceNumber] NVARCHAR(30) NOT NULL,
    [InvoiceDate] DATETIME NOT NULL,
    [InvoiceTotal] NVARCHAR(20) NOT NULL,
    [Balance] NVARCHAR(20) NOT NULL,

    CONSTRAINT [PK_CUSTOMERHISTORY]
        PRIMARY KEY CLUSTERED ([ID] ASC, [CustomerID] ASC),

    CONSTRAINT [UQ_CUSTOMERHISTORY_ID]
        UNIQUE ([ID]),

    CONSTRAINT [FK_CUSTOMER_CH]
        FOREIGN KEY ([CustomerID])
        REFERENCES [dbo].[Customers] ([ID])
);

此时 [ID] 已经是 candidate key,下面的外键就是有效的:

CONSTRAINT [FK_CUSTOMERHISTORY_CPH]
    FOREIGN KEY ([HistoryID])
    REFERENCES [dbo].[CustomerHistory] ([ID])

不过,ID 是 IDENTITY 列,并且已经唯一。将 CustomerID 加入主键通常不会进一步保证实体完整性,反而会增加主键及相关索引的宽度。除非复合主键有明确的业务或兼容性要求,否则更建议使用单列主键。

方案三:让外键完整引用复合主键

如果 CustomerHistory 的逻辑身份确实需要由 ID 和 CustomerID 共同确定,子表也必须保存这两列,并按照相同顺序引用:

CREATE TABLE [dbo].[CustomerPaymentHistory]
(
    [ID] INT IDENTITY (1, 1) NOT NULL,
    [PaymentID] AS
        ('CLI' + RIGHT(
            '000000000' + CONVERT([varchar](10), [ID]),
            5
        )) PERSISTED NOT NULL,
    [HistoryID] INT NOT NULL,
    [CustomerID] INT NOT NULL,
    [PaymentDate] DATETIME NOT NULL,
    [PaymentAmount] NVARCHAR(20) NOT NULL,
    [BalanceDue] NVARCHAR(20) NULL,
    [PaidInFull] BIT NOT NULL,

    CONSTRAINT [PK_CUSTOMERPAYMENTHISTORY]
        PRIMARY KEY CLUSTERED ([ID] ASC),

    CONSTRAINT [FK_CUSTOMERHISTORY_CPH]
        FOREIGN KEY ([HistoryID], [CustomerID])
        REFERENCES [dbo].[CustomerHistory] ([ID], [CustomerID])
);

外键两端需要满足以下条件:

  • 列数相同;
  • 列的对应顺序相同;
  • 数据类型兼容;
  • 被引用的列组合是 PRIMARY KEY,或受 UNIQUE 约束保护。

修改已有表

如果表已经存在,并且没有其他依赖阻止修改,可以删除原主键,再创建单列主键:

ALTER TABLE [dbo].[CustomerHistory]
DROP CONSTRAINT [PK_CUSTOMERHISTORY];

ALTER TABLE [dbo].[CustomerHistory]
ADD CONSTRAINT [PK_CUSTOMERHISTORY]
PRIMARY KEY CLUSTERED ([ID] ASC);

执行前需要检查:

  • 是否有其他外键引用原复合主键;
  • 是否有索引或部署脚本依赖原主键的名称和列结构;
  • 表中是否存在不符合新约束的数据;
  • 修改主键是否会改变聚集索引布局,并带来较大的重建开销。

注意事项

错误信息虽然提到了 computed column,但当前外键引用的是普通 INT 列 [ID],不是计算列 [HistoryID],因此 PERSISTED 与这次错误无关。问题在于 [ID] 没有单独构成主键或 candidate key。

如果 InvoiceTotal、Balance、PaymentAmount 和 BalanceDue 用于存储金额,不建议使用 NVARCHAR。通常更适合使用:

DECIMAL(19, 4)

具体的精度和小数位数应根据业务要求确定。使用数值类型可以避免字符串比较、排序和金额计算引发的问题。

备注:内容仅供参考。