MySQL 8.0.16+ 的 CHECK 约束仅在 InnoDB 引擎、严格模式启用、表达式确定性且无非确定函数时才生效;低于版本、MyISAM、含 NOW()/UUID() 等或未显式声明 DETERMINISTIC 均导致失效。

MySQL 8.0 的 CHECK 约束只有在版本 ≥ 8.0.16 且配置正确时才真正拦截非法数据;低于该版本或未启用严格模式、用了 MyISAM 引擎、或表达式含非确定性函数,都会导致校验失效——你写的约束可能根本没在跑。
确认 CHECK 是否真生效:先查版本和引擎
别跳过这步。建表后插入非法值却没报错?大概率是约束压根没注册成功。
- 运行
SELECT VERSION();—— 必须返回8.0.16或更高,8.0.15和5.7.x都不行 - 执行
SHOW CREATE TABLE your_table;—— 输出里必须明确出现CHECK子句,否则说明被 MySQL 忽略(常见于旧版或 MyISAM) - 确认引擎:
ENGINE=InnoDB是硬性要求,MyISAM完全不支持CHECK,哪怕版本够新也白搭 - 检查行格式:
ROW_FORMAT=DYNAMIC更稳妥,REDUNDANT在 8.0+ 已弃用且部分CHECK行为异常
写合法 CHECK 表达式的三条铁律
CHECK 不是任意 SQL 表达式都能塞进去,MySQL 对“确定性”有严格限制。
- 禁止子查询:
CHECK (status IN (SELECT code FROM valid_status))直接报错ERROR 3816 - 禁止非确定性函数:
NOW()、UUID()、RAND()、USER()全部不允许;CURRENT_DATE也不行 - 禁止用户变量和存储函数(除非显式声明为
DETERMINISTIC) - 安全可用的包括:
>、BETWEEN、IN ('a','b')、IS NOT NULL、CHAR_LENGTH()、REGEXP、简单CASE分支
字段级 vs 表级 CHECK:什么时候能跨列?
列级 CHECK 只能引用当前列;跨列逻辑校验必须用表级写法,且位置很关键。
- 字段级(只能单列):
age TINYINT CHECK (age BETWEEN 0 AND 150)✅;age TINYINT CHECK (age < salary)❌(salary还没定义,语法错误) - 表级(可跨列):必须写在所有列定义之后,例如
CONSTRAINT chk_date_order CHECK (start_date <= end_date)✅ - 命名建议显式写:
CONSTRAINT chk_phone_format CHECK (CHAR_LENGTH(phone) = 11 AND phone REGEXP '^1[3-9][0-9]{9}$'),避免 MySQL 自动生成难追踪的t1_chk_1 -
NULL不触发失败:表达式结果为UNKNOWN(比如score > 60遇到score IS NULL)时,约束通过。真要拦空值,得额外加NOT NULL或写成CHECK (score IS NOT NULL AND score > 60)
给已有表加 CHECK 的真实流程
不是执行一条 ALTER TABLE ... ADD CHECK 就完事——MySQL 默认会校验存量数据,不合规就直接失败。
- 先定位脏数据:
SELECT * FROM users WHERE NOT (email LIKE '%@%'); - 修复或删除违规行(不能靠
UPDATE ... SET email = NULL混过去,NULL可能仍不满足业务语义) - 再执行
ALTER TABLE users ADD CONSTRAINT chk_email_valid CHECK (email LIKE '%@%'); - 如果想先加约束但暂不校验历史数据,可用
NOT ENFORCED:ADD CONSTRAINT chk_age CHECK (age >= 18) NOT ENFORCED,后续再DROP+ADD ENFORCED激活(MySQL 不支持直接改 enforced 状态)
最容易被忽略的是:CHECK 只做静态判断,它不查其他表、不依赖会话上下文、不调用外部逻辑。如果你需要验证“客户余额是否足够”,或者“当前操作人是否有权限”,这类校验必须交给触发器或应用层——别硬塞进 CHECK 里,只会卡在语法或运行时报错。


















