23503 — foreign_key_violation:外键约束冲突
速览
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中。
| 字段 | 值 |
|---|---|
| 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 |
| 别名 | — |
含义与触发路径
子表上的外键指向父表的主键或合适的唯一键。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 只说明关系检查失败,不直接给出业务修复。缺少父行可能是写入顺序错误、标识符错误、另一个事务尚未提交,或需要级联策略的有意删除。选择修复前应检查语句和约束定义。
报文与诊断
代表性操作创建父表和子表,不插入键 99 对应的父行,然后插入子行。下面的可执行摘录与 runner 使用同一触发和恢复顺序;实际运行时由隔离 harness 为名称加上模式限定。
PostgreSQL 18.6 的自然错误为:
PostgreSQL 10.21 的主报文和 detail 相同;对应源码行为位于第 3266 行。detail 取决于服务器是否有权限描述键值,可能不存在。不要把本地化的英文报文当作协议契约:应按 23503 分支,再读取结构化字段和操作上下文。
诊断
先记录失败语句、SQLSTATE、严重级别、主报文、detail、hint、服务器版本和事务状态。显式事务要分别记录错误后(INERROR)以及恢复后(IDLE 或 INTRANS)的状态。后续的 25P02 表示客户端在事务已经失败时发送了命令;它是后续状态,不能替代 23503。
使用 pg_constraint 和 pg_get_constraintdef() 检查命名约束及其引用关系。结合适用的隔离级别检查尝试写入的键和父表。如果父行由另一个事务创建,应确认写入和提交顺序是否符合设计;固定等待并不能证明父行已经可见。
对于父键删除或更新,检查引用行以及 ON DELETE、ON UPDATE 声明的动作。延迟外键可能允许违规语句暂时成功,而在 COMMIT 时才报告 23503。日志和重试逻辑应保留这一时点。
处理与修复
选择符合关系语义的修复:
- 像代表性案例一样,先创建或选择目标父行,再重试子行写入。
- 如果子标识符过期或格式错误,修正子行标识符;不要关闭约束来掩盖数据错误。
- 删除父行时,只有业务规则允许时才采用声明的级联、置空或限制动作;否则应先更新或归档引用行。
- 如果父行由另一个事务写入,应采用能建立预期顺序和隔离级别的事务设计。回滚后重新读取,再决定是否重放旧的子行请求。
显式事务失败后,ROLLBACK 会丢弃其中的待提交工作并让连接回到 IDLE。当子行操作可选时,可以使用 savepoint 保留之前的工作。真正的修复应包括父行的实际读取、提交后的子行以及提交后查询;只执行 ROLLBACK 或打开新连接不能证明关系已经修好。
版本与边界
目录在 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、约束身份和外键诊断形状作为兼容边界。延迟时点、权限、级联动作以及并发创建父行是独立维度,本次立即约束案例不覆盖它们。
相关
23505 — unique_violation 处理唯一性不变量中的重复值。23502 — not_null_violation 处理必填列收到 NULL。40001 — serialization_failure 和 40P01 — deadlock_detected 描述可能围绕关系修复出现的并发结果。25P02 — in_failed_sql_transaction 是未处理错误之后的后续事务状态。
来源
本页结构化证据记录在公开证据 JSON中。源码记录固定到 PostgreSQL commit 724edf9bde9d356724ad384a2e196edc3c9f80f7;runtime 记录保留两个目标的 run ID 及结构化观察。
src.errcodes.18.6—errcodes.txtsrc.ri-triggers.18.6—ri_triggers.cdoc.ddl.18.6— 外键doc.protocol.18— 错误和通知消息字段- Runtime:
23503-fk-manual-final-20260909(latest 与 pg10),结构化观察见公开证据 JSON