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)
具体的精度和小数位数应根据业务要求确定。使用数值类型可以避免字符串比较、排序和金额计算引发的问题。
备注:内容仅供参考。
