# 23503 — foreign_key_violation：外键约束冲突

> 当子表外键写入或某些普通父表操作违反外键关系时，PostgreSQL 会报告 SQLSTATE 23503。应确认操作、关系和约束，再修复数据或事务边界。
---

## 速览 {#at-a-glance}

`23503` 是 PostgreSQL 类别 23 `integrity_constraint_violation` 中的 `foreign_key_violation` 条件。子表插入或更新找不到匹配的父键，或者普通父表键操作触发外键规则，都可能产生它。在 PostgreSQL 18.6 中，父表 `RESTRICT` 分支改用 `23001`，因此应先按操作和完整诊断判断 SQLSTATE。

最有用的诊断字段是 SQLSTATE（`C`）、主报文（`M`）、detail（`D`），以及服务器提供时的 schema、table 和 constraint 字段。在 psycopg 中，这些字段位于 `exc.sqlstate` 和 `exc.diag`。重试前应保存完整诊断，因为当服务器可以展示键值时，detail 会指出缺少或仍被引用的键。

对于立即检查的外键约束，错误由违反约束的语句报告；延迟约束可以让语句先完成，并在 `COMMIT` 时报告 `23503`。自动提交下，立即失败的语句结束后连接可以执行下一条命令；显式事务中的立即失败会让事务变为 `INERROR`，必须执行 `ROLLBACK` 或回滚到 savepoint 后才能发送无关命令。代表性案例用真实的 `parents`/`children` 外键、有效父行和新的子行写入验证了修复。

案例 `fk_insert_missing_parent` 使用真实的 `parents`/`children` 外键，在 PostgreSQL 18.6 和隔离的 PostgreSQL 10.21 目标上均通过。run ID 和逐案例断言保存在[公开证据 JSON](../../data/evidence/23503.json)中。

<!-- BEGIN SQLSTATE FACTS: generated by scripts/generate.py; do not edit -->

| 字段 | 值 |
| --- | --- |
| SQLSTATE | `23503` |
| 条件名 | `foreign_key_violation` |
| 状态 | `有效` |
| 已知存在于 | `7.4` |
| 锁定快照 | `9.0.23, 9.1.24, 9.2.24, 9.3.25, 9.4.26, 9.5.25, 9.6.24, 10.23, 11.22, 12.22, 13.23, 14.24, 15.19, 16.15, 17.11, 18.6, 19beta3` |
| 宏 | `ERRCODE_FOREIGN_KEY_VIOLATION` |
| 别名 | `—` |

<!-- source facts: data/errcodes/23503.json -->
<!-- END SQLSTATE FACTS -->

## 含义与触发路径 {#meaning}

子表上的外键指向父表的主键或合适的唯一键。PostgreSQL 会在子表 `INSERT`、改变键值的 `UPDATE` 时检查这种关系；父表键被更新或删除时，也会检查反向引用。约束是立即检查还是延迟检查，决定了错误出现的具体时点。

列的匹配规则同样重要。默认的 `MATCH SIMPLE` 下，只要引用键中有任意列为 NULL，该行就不必匹配父表。`MATCH FULL` 允许全为 NULL 的键，或全部非 NULL 且能匹配父键的键，但会拒绝 NULL 与非 NULL 混合的键。代表性案例只有一个 `NOT NULL` 列，因此有意没有演示这两种 NULL 规则。

对于父表 `DELETE` 或改变键值的 `UPDATE`，`ON DELETE/UPDATE NO ACTION` 可以在约束可延迟时等到约束检查时点；`RESTRICT` 要求立即检查且不能延迟。在固定的 PostgreSQL 18.6 源码中，`RESTRICT` 分支使用 `23001`（`restrict_violation`）及专用报文，而不是本页案例中的 `23503`。动作选择是关系契约的一部分；父表操作不能按“缺少父键”的子表插入来推断 SQLSTATE。

普通子行路径在 `ri_triggers.c` 中使用 `insert or update on table "%s" violates foreign key constraint "%s"` 模板。可选 detail 为 `Key (%s)=(%s) is not present in table "%s".`；服务器还会通过协议的 table 和 constraint 字段附加子表及约束身份。普通的父键删除或更新分支可使用另一套 `23503` 主报文；固定版本的 `RESTRICT` 分支例外地使用 `23001`。本页运行证据只覆盖缺少父行的子表写入，不覆盖父表动作。

`23503` 只说明关系检查失败，不直接给出业务修复。缺少父行可能是写入顺序错误、标识符错误、另一个事务尚未提交，或需要级联策略的有意删除。选择修复前应检查语句和约束定义。

## 报文与诊断 {#messages}

代表性操作创建父表和子表，不插入键 `99` 对应的父行，然后插入子行。下面的可执行摘录与 runner 使用同一触发和恢复顺序；实际运行时由隔离 harness 为名称加上模式限定。

<!-- BEGIN SQLSTATE SNIPPET: fk_insert_missing_parent -->
```sql
CREATE TABLE parents(id integer PRIMARY KEY);
CREATE TABLE children(
    id integer PRIMARY KEY,
    parent_id integer NOT NULL,
    CONSTRAINT children_parent_fk FOREIGN KEY (parent_id) REFERENCES parents(id)
);
BEGIN;
INSERT INTO children VALUES (1, 99);
-- 服务器报告 23503，事务此时为 INERROR。
ROLLBACK;
BEGIN;
INSERT INTO parents VALUES (99);
INSERT INTO children VALUES (1, 99);
COMMIT;
```
<!-- END SQLSTATE SNIPPET -->

PostgreSQL 18.6 的自然错误为：

```text
SQLSTATE: 23503
severity: ERROR
message_primary: insert or update on table "children" violates foreign key constraint "children_parent_fk"
message_detail: Key (parent_id)=(99) is not present in table "parents".
schema_name: c23503_fk_insert_missing_parent
table_name: children
constraint_name: children_parent_fk
source: ri_triggers.c / ri_ReportViolation / line 2783
```

PostgreSQL 10.21 的主报文和 detail 相同；对应源码行为位于第 3266 行。detail 取决于服务器是否有权限描述键值，可能不存在。不要把本地化的英文报文当作协议契约：应按 `23503` 分支，再读取结构化字段和操作上下文。

## 诊断 {#diagnosis}

先记录失败语句、SQLSTATE、严重级别、主报文、detail、hint、服务器版本和事务状态。显式事务要分别记录错误后（`INERROR`）以及恢复后（`IDLE` 或 `INTRANS`）的状态。后续的 `25P02` 表示客户端在事务已经失败时发送了命令；它是后续状态，不能替代 `23503`。

使用 `pg_constraint` 和 `pg_get_constraintdef()` 检查命名约束及其引用关系。结合适用的隔离级别检查尝试写入的键和父表。如果父行由另一个事务创建，应确认写入和提交顺序是否符合设计；固定等待并不能证明父行已经可见。

对于父键删除或更新，检查引用行以及 `ON DELETE`、`ON UPDATE` 声明的动作。延迟外键可能允许违规语句暂时成功，而在 `COMMIT` 时才报告 `23503`。日志和重试逻辑应保留这一时点。

## 处理与修复 {#response}

选择符合关系语义的修复：

- 像代表性案例一样，先创建或选择目标父行，再重试子行写入。
- 如果子标识符过期或格式错误，修正子行标识符；不要关闭约束来掩盖数据错误。
- 删除父行时，只有业务规则允许时才采用声明的级联、置空或限制动作；否则应先更新或归档引用行。
- 如果父行由另一个事务写入，应采用能建立预期顺序和隔离级别的事务设计。回滚后重新读取，再决定是否重放旧的子行请求。

显式事务失败后，`ROLLBACK` 会丢弃其中的待提交工作并让连接回到 `IDLE`。当子行操作可选时，可以使用 savepoint 保留之前的工作。真正的修复应包括父行的实际读取、提交后的子行以及提交后查询；只执行 `ROLLBACK` 或打开新连接不能证明关系已经修好。

## 版本与边界 {#versions}

目录在 PostgreSQL 7.4 的锁定定义中已观察到 `23503`，并持续到 8.4.22 的 pre-9.0 定义；随后在列出的所有正式快照直到 PostgreSQL 18.6 以及 PostgreSQL 19 Beta 3 预览中存在。这是 definition_only 的存在边界，不是确切实现引入版本或运行时使用断言。扫描范围内没有记录该条件的定义变化。

代表性案例在 PostgreSQL 18.6 和 10.21 上通过。两个版本的源码行号不同；本页采用 SQLSTATE、约束身份和外键诊断形状作为兼容边界。延迟时点、权限、级联动作以及并发创建父行是独立维度，本次立即约束案例不覆盖它们。

## 相关 {#related}

[`23505` — `unique_violation`](../23505/) 处理唯一性不变量中的重复值。[`23502` — `not_null_violation`](../23502/) 处理必填列收到 `NULL`。[`40001` — `serialization_failure`](../40001/) 和 [`40P01` — `deadlock_detected`](../40p01/) 描述可能围绕关系修复出现的并发结果。[`25P02` — `in_failed_sql_transaction`](../25p02/) 是未处理错误之后的后续事务状态。

## 来源 {#sources}

本页结构化证据记录在[公开证据 JSON](../../data/evidence/23503.json)中。源码记录固定到 PostgreSQL commit `724edf9bde9d356724ad384a2e196edc3c9f80f7`；runtime 记录保留两个目标的 run ID 及结构化观察。

- `src.errcodes.18.6` — [`errcodes.txt`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/utils/errcodes.txt#L235-L241)
- `src.ri-triggers.18.6` — [`ri_triggers.c`](https://github.com/postgres/postgres/blob/724edf9bde9d356724ad384a2e196edc3c9f80f7/src/backend/utils/adt/ri_triggers.c#L2761-L2809)
- `doc.ddl.18.6` — [外键](https://www.postgresql.org/docs/18/ddl-constraints.html#DDL-CONSTRAINTS-FK)
- `doc.protocol.18` — [错误和通知消息字段](https://www.postgresql.org/docs/18/protocol-error-fields.html)
- Runtime：`23503-fk-manual-final-20260909`（latest 与 pg10），结构化观察见[公开证据 JSON](../../data/evidence/23503.json)
