MySQL 1215 无法添加外键约束:8.4 实测 20 种外键失败,只有 3 种还报 1215
搜「mysql 1215 无法添加外键约束」,几乎每篇都让你查字段类型、字符集和索引。我们在 MySQL 8.4.7 测试库(自造表)上逐条复现了 20 种外键失败:类型/字符集不一致报 3780,父列缺唯一键报 6125,父表不存在报 1824,真正还报 1215 的只有 3 种——临时表带外键、存储生成列基列上 ON UPDATE CASCADE、FOREIGN_KEY_CHECKS=0 先建子表后父表类型不符。更危险的是 2 种根本不报错:MyISAM 子表的外键被静默丢掉、checks=0 下给 100 万行带孤儿数据的表加外键直接成功。附码对照表、LATEST FOREIGN KEY ERROR 读法和只读巡检 SQL。
测试库实测(MySQL 8.4.7,默认 sql_mode,utf8mb4,表和数据全部自造,8 vCPU,2026-11-11):常见说法是「1215 就查类型、字符集、索引是否一致」,我不同意——这些情况在 8.4 上已经分别报 3780、6125、1822,你在 8.4 上照这份清单排查,看到的根本不是 1215。我的判断:在 8.4 上看到 1215,先排查临时表、存储生成列和 FOREIGN_KEY_CHECKS=0 这 3 种冷门情况;比 1215 更该防的是两种不报错的情况——MyISAM 子表的外键被静默丢弃,以及 FOREIGN_KEY_CHECKS=0 下加外键不校验已有数据。20 种失败的码分布:3780×6、6125×2、1824×2、1215×3,1822 / 1830 / 1506 / 3733 / 1553 / 1452 / 1170 各 1。100 万行子表:正常加外键 4.79 秒;有 1 行孤儿时 5.38 秒后报 1452;LEFT JOIN 找孤儿 1.20 秒。
适用对象
- 我的判断:在 MySQL 8.4 上,「1215 先查类型、字符集、索引」这份老清单已经对不上了。测试库 20 种外键失败里,这三类问题分别报 3780(不兼容列,6 种)、6125(缺唯一键,2 种)、1822(缺索引,restrict 关闭时),只有 3 种还报 1215。
- 还报 1215 的 3 种:① CREATE TEMPORARY TABLE 带外键;② 存储生成列的基列上用 ON UPDATE CASCADE;③ FOREIGN_KEY_CHECKS=0 时先建了引用它的子表(INT),之后 checks=1 建父表 BIGINT 主键——报错落在一条不含任何外键语法的 CREATE 父表语句上。
- SHOW WARNINGS 对 1215 没用,只重复一遍 Error 1215;第 ③ 种可以在 SHOW ENGINE INNODB STATUS 的 LATEST FOREIGN KEY ERROR 里看到子表名和原因,①② 在 SQL 层就被拒,那一段不会更新。
- 比报错更危险的是不报错:MyISAM 子表写外键语法执行成功,SHOW CREATE TABLE 里外键没了;FOREIGN_KEY_CHECKS=0 时给 100 万行、带 1 行孤儿的表加外键直接成功,孤儿还在。
- 8.4 默认 restrict_fk_on_non_standard_key=ON:引用非唯一键的外键直接报 6125。关掉后能建,但会给 Warning 6124,官方标为已弃用、将来会移除——从 5.7 带过来的这类外键,升级前就该改成引用唯一键。
- 加外键的耗时主要花在校验已有数据:100 万行子表成功加外键 4.79 秒,有 1 行孤儿时跑满 5.38 秒才报 1452。先用 LEFT JOIN 找孤儿(1.20 秒)再加外键,比反复试 ALTER 省时间。
「1215 就查类型、字符集、索引」——这份清单在 8.4 上已经对不上了
写这篇前我列了 5 个角度:给一年前的自己的建议、常见说法哪里不对、换个定义(1215 是外键校验失败的旧总称)、对企业为什么重要(升级脚本漏捕获新码)、用医院分诊类比。最后选了第二个,因为它最能直接改变排查顺序。
网上 1215 文章的排查清单本身没错,错在它默认这些问题会报 1215。我在 8.4.7 上逐条复现,结果是:类型不一致、有无符号不一致、字符集或排序规则不一致、ALTER 加外键类型不符、改父列类型,全部报 3780;父列没有唯一键报 6125;父表不存在或是 MyISAM 报 1824。也就是说,如果你在 8.4 上看到的确实是 1215,这份清单上的每一项大概率都不是原因。
换个定义更好理解:1215 更像是外键校验失败的旧总称,8.4 把大部分具体原因拆成了独立的错误码。就像医院把「发烧」拆成了具体诊断,你还拿「发烧」去搜,就找不到对症的药。
| 原因 | 触发语句(节选) | 报错码 |
|---|---|---|
| 有无符号不一致 | 子 pid INT → 父 id INT UNSIGNED | 3780 |
| 整数宽度不一致 | 子 BIGINT UNSIGNED → 父 INT UNSIGNED | 3780 |
| 字符集不一致 | 子 utf8mb4 → 父 latin1 唯一列 | 3780 |
| 排序规则不一致 | 子 utf8mb4_general_ci → 父 utf8mb4_bin | 3780 |
| ALTER TABLE 加外键类型不符 | ALTER … ADD CONSTRAINT fk10 … | 3780 |
| 改被引用的父列类型 | ALTER TABLE p2 MODIFY u BIGINT | 3780 |
| 父列没有索引 | REFERENCES parent(code) | 6125 |
| 父列只有非唯一索引 | REFERENCES parent(k) | 6125 |
| 同上无索引,restrict_fk_on_non_standard_key=OFF | — | 1822 |
| 父表不存在 | REFERENCES no_such_parent(id) | 1824 |
| 父表是 MyISAM | REFERENCES pm(id) | 1824 |
| ON DELETE SET NULL 但子列 NOT NULL | pid … NOT NULL … SET NULL | 1830 |
| 父表分区 | PARTITION BY HASH(id) | 1506 |
| 虚拟生成列做外键 | g … AS (x) VIRTUAL | 3733 |
| 删除被外键使用的父表唯一索引 | ALTER TABLE p2 DROP INDEX uk | 1553 |
| 子表已有孤儿数据 | ALTER … ADD CONSTRAINT fk11 | 1452 |
| TEXT 列做外键 | FOREIGN KEY(t) REFERENCES pt(t) | 1170 |
| 临时表带外键 | CREATE TEMPORARY TABLE d7 … | 1215 |
| 存储生成列基列 ON UPDATE CASCADE | g … STORED; FOREIGN KEY(x) … ON UPDATE CASCADE | 1215 |
| checks=0 建子表 INT,后建父表 BIGINT | CREATE TABLE later_parent(id BIGINT PRIMARY KEY) | 1215 |
临时表、存储生成列、FOREIGN_KEY_CHECKS=0 之后建父表
① 临时表:CREATE TEMPORARY TABLE 带外键直接报 1215。官方文档写得很清楚:父表和子表必须使用同一存储引擎,并且都不能是临时表。这种情况多出现在存储过程或报表脚本里,开发者把正式表的 DDL 复制成临时表时顺手带上了外键。
② 存储生成列的基列:表里有 g INT AS (x) STORED,又在 x 上建外键并写 ON UPDATE CASCADE,报 1215。官方原文:存储生成列的基列上的外键,ON UPDATE 不能用 CASCADE、SET NULL 或 SET DEFAULT。改成 ON UPDATE RESTRICT / NO ACTION 即可。
③ FOREIGN_KEY_CHECKS=0 之后建父表:这是我最初踩的坑。session 里 SET FOREIGN_KEY_CHECKS=0,建子表 d10(pid INT) 引用一张还不存在的 later_parent——成功,没有任何警告。之后 SET FOREIGN_KEY_CHECKS=1,CREATE TABLE later_parent(id BIGINT PRIMARY KEY),报 1215。你执行的这句里根本没有 FOREIGN KEY 关键字,只能从 InnoDB 状态里反查是谁引用了它。导 dump 时表的创建顺序和类型改动叠在一起,就是这么来的。
本节判断
- 排查第 ③ 种:SHOW ENGINE INNODB STATUS\G 里 LATEST FOREIGN KEY ERROR 段,测试库原文为 “Error in foreign key constraint of table labdb/x10: there is no index in referenced table which would contain the columns as the first columns, or the data types in the referenced table do not match the ones in table.”,直接点名子表 x10。
- SHOW WARNINGS 在 3 种情况下都只返回一行 Error 1215 Cannot add foreign key constraint,没有任何额外原因。
- ①② 被 SQL 层直接拒绝,不会写入 LATEST FOREIGN KEY ERROR,只能对照语句本身判断:有没有 TEMPORARY、外键列是不是某个 STORED 生成列的基列。
2 种不报错的情况:外键被静默丢掉、孤儿数据被直接放过
MyISAM 子表:CREATE TABLE c8(… FOREIGN KEY(pid) REFERENCES parent(id)) ENGINE=MyISAM 执行成功,没有错误也没有警告。SHOW CREATE TABLE c8 一看,只剩 KEY `pid` (`pid`),外键语法被吞掉了。如果你的建表脚本没有显式写 ENGINE、而某个环境的默认引擎被改过,测试环境和生产环境可能一个有外键一个没有。
FOREIGN_KEY_CHECKS=0 加外键:我在测试库建了父表 bp 10 万行、子表 bc 100 万行(另插 1 行 pid 不存在的孤儿)。checks=1 时 ALTER TABLE bc ADD CONSTRAINT … 跑了 5.38 秒后报 1452;先 LEFT JOIN 找孤儿 1.20 秒、DELETE 1.36 秒,再加外键 4.79 秒成功。但如果在 checks=0 下加外键,ALTER 直接成功,事后 LEFT JOIN 还能查出 1 行孤儿——外键在,数据却不满足它。
| 操作 | 结果 | 耗时 |
|---|---|---|
| 造数:父表 10 万行 + 子表 100 万行 + 1 行孤儿 | — | 3.98 秒 |
| checks=1 加外键(有 1 行孤儿) | ERROR 1452 | 5.38 秒 |
| LEFT JOIN 找孤儿 | 1 行(pid=999999) | 1.20 秒 |
| DELETE 孤儿 | 删除 1 行 | 1.36 秒 |
| checks=1 再加外键 | 成功 | 4.79 秒 |
| checks=0 下删外键、插 1 行孤儿、加外键、再查孤儿 | 加外键成功,孤儿 1 行仍在 | 1.41 秒(四步合计) |
从 5.7 / 8.0 升到 8.4:按 1215 写的错误处理会漏掉新码
迁移和发布脚本里常见这样的逻辑:捕获 1215 就打印「外键创建失败,检查类型」然后继续。在 8.4 上,同样的类型问题返回的是 3780,父列非唯一键返回 6125——如果脚本只认 1215,要么直接中断在一个没人认识的错误码上,要么被通用的 catch 吞掉。建议把外键相关的码一起列进去:1215、1452、1553、1822、1824、1830、3780、3733、6125。
第二个风险是 restrict_fk_on_non_standard_key。测试库 SELECT @@restrict_fk_on_non_standard_key 返回 1(ON)。这时引用非唯一键的外键直接报 6125;把会话变量关掉后能建,但会给 Warning 6124:Foreign key … refers to non-unique key or partial key. This is deprecated and will be removed in a future release. 如果你的老库里有这种外键,升到 8.4 后重建表或导入 dump 时会失败,正确做法是给父列补唯一键,而不是全局关掉这个开关。
另一件值得一起核对的事是 8.4 的 InnoDB 默认值变化,见我们之前的 [MySQL 8.4 升级后变慢实测](/resources/mysql-84-upgrade-slow-innodb-defaults-change-buffer-ahi)。
升级或导入前,先跑这 3 条
下面 3 条只读 SQL 在测试库上都能跑通,把库名换成你自己的即可。
本节判断
- 非 InnoDB 表(外键会被静默丢掉):SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND ENGINE <> 'InnoDB';
- 库里实际存在的外键(和建表脚本对照,看有没有丢):SELECT CONSTRAINT_NAME, TABLE_NAME, REFERENCED_TABLE_NAME FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE();
- 孤儿数据(加外键前先查,表名列名换成你的):SELECT c.id, c.pid FROM child c LEFT JOIN parent p ON p.id = c.pid WHERE c.pid IS NOT NULL AND p.id IS NULL LIMIT 10;
我的判断
在 MySQL 8.4 上,1215 已经不是「外键类型不对」的代名词了:看到 1215,先查临时表、存储生成列和 FOREIGN_KEY_CHECKS=0 这 3 种;看到 3780 / 6125 / 1822 才是老清单该派上用场的时候。理由很简单——20 种外键失败里 17 种都换了更具体的码,按旧码排查只会越查越偏。真正需要花时间防的,是 MyISAM 和 FOREIGN_KEY_CHECKS=0 这两种根本不报错的情况。
错误码速查:[1215 速查页](/tools/mysql-error/1215)、[1452 子表孤儿数据](/tools/mysql-error/1452)、[1054 列名找不到](/tools/mysql-error/1054)、[1406 数据过长](/tools/mysql-error/1406)、[1146 表不存在](/tools/mysql-error/1146)。更多 MySQL / PostgreSQL 实测汇总见 [数据库实测资料](/resources/database)。
觉得有用?转给正在做这件事的同事或朋友
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
mysql 1215 无法添加外键约束怎么解决?
先 SELECT VERSION() 确认版本。在 MySQL 8.4.7 测试库里只剩 3 种情况报 1215:临时表带外键、存储生成列的基列上用 ON UPDATE CASCADE、FOREIGN_KEY_CHECKS=0 先建子表后建的父表列类型不符。分别改用普通表、把 ON UPDATE 改成 RESTRICT/NO ACTION、把父表列类型改成和子表一致。
外键字段类型不一致为什么报 3780 不报 1215?
MySQL 8.4 用 3780(ER_FK_INCOMPATIBLE_COLUMNS)专门报引用列与被引用列不兼容,测试库里有无符号、INT/BIGINT、字符集、排序规则不一致都报 3780。VARCHAR(50) 引用同字符集排序规则的 VARCHAR(20) 可以建成功。
SHOW WARNINGS 看不到 1215 的原因怎么办?
看 SHOW ENGINE INNODB STATUS\G 的 LATEST FOREIGN KEY ERROR。FOREIGN_KEY_CHECKS=0 后建父表那种情况会在这里写出子表名;临时表和存储生成列两种在 SQL 层被拒,只能对照语句判断。
6125 Missing unique key 是什么?要关掉 restrict_fk_on_non_standard_key 吗?
8.4 默认 restrict_fk_on_non_standard_key=ON,外键引用的父列必须有唯一键。关掉能建,但会给已弃用警告 6124,将来版本会移除。正确做法是给父列补唯一键或主键。
SET FOREIGN_KEY_CHECKS=0 加外键安全吗?
不安全。测试库里 checks=0 时给带 1 行孤儿的 100 万行表加外键直接成功,孤儿数据仍在。加外键前先用 LEFT JOIN 查孤儿并清理,再在 checks=1 下加。
MyISAM 表写了外键为什么不报错?
MyISAM 不支持外键,测试库里 MyISAM 子表的外键语法被静默丢弃,SHOW CREATE TABLE 里只剩普通索引;父表是 MyISAM 时报 1824。先查出非 InnoDB 表再决定是否转换。
下一步
推荐动作
你提供 information_schema 里的表引擎、外键清单和孤儿数据检查结果(可脱敏),我们逐项判断哪些外键在升级或导入时会失败、哪些已经被静默丢掉,给出处理顺序;生产变更由你方授权执行。也可以先买 ¥49 MySQL 学习包自查,或用免费的 MySQL 错误码速查先对一遍码。