MySQL 8.0.16才真正支持CHECK约束?这点还真不是空xue来风。低于这个版本,哪怕你在表结构里写得再像模像样,插入age = -5或score = 0照样能成功——约束定义完全是一纸空文。所以,搞清楚你手头的MySQL版本,是使用这个功能的第一步。

确认你的 MySQL 版本是否真正支持 CHECK
直接执行SELECT @@version;就行了。结果是8.0.16或更高版本(比如8.0.33、8.4.0)才算数。这里有个坑:很多人看到版本是8.0就放心了,但很多线上环境其实是8.0.11或8.0.28,如果没打过关键补丁或升级过,所谓的CHECK支持还是不靠谱的。
一个更稳妥的办法是直接做个实测:
- 建个表:
CREATE TABLE t (x INT, CONSTRAINT chk_x CHECK (x > 0)); - 插入一条违规数据:
INSERT INTO t VALUES (-1); - 如果服务器返回
Check constraint 'chk_x' is violated.,那恭喜你,约束真正生效了。否则,就是空有虚名。
别太迷信文档里那句“8.0 支持”,自己动手跑一遍,比什么都踏实。
列级 CHECK 和表级 CHECK 的实际差异
列级CHECK的语法很紧凑,但能力也受限制——它只能引用当前列。比如给订单状态加个约束:CHECK (status IN ('pending', 'shipped', 'cancelled')),这就挺合适。
但一旦需要跨列做业务逻辑校验,就必须用表级CHECK了。比如“发货时间不能早于下单时间”这个场景:
CREATE TABLE orders (order_time DATETIME, ship_time DATETIME, CONSTRAINT chk_time_order CHECK (ship_time >= order_time OR ship_time IS NULL));
这里面有几个关键点需要注意:
- 表达式中间出现
NULL时,比较结果为UNKNOWN,约束不会触发报错,这是SQL标准行为,也是不少新手踩坑的地方。 - 绝对不能在里面调用函数,比如
NOW()、UUID(),甚至子查询也不允许。MySQL会直接甩出一个错误:Function 'now' is not allowed in check constraint。 - 约束名最好显式指定,比如写成
chk_status_valid。如果偷懒不写,MySQL会生成一个类似t_chk_1的自动命名,后续排查或删除的时候,绝对够你找半天。
ENFORCED vs NOT ENFORCED:可控的约束开关
MySQL提供了一个挺实用的选项:你可以创建一个CHECK约束,但暂时不强制执行。只要加上NOT ENFORCED关键字就行:
ALTER TABLE users ADD CONSTRAINT chk_email_format CHECK (email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,}$') NOT ENFORCED;
这种写法特别适合灰度场景:
- 先加上约束的定义,上线后观察日志或监控,看看存量数据里是不是有很多违反规则的。
- 把脏数据修复干净之后,再执行
ALTER TABLE users ALTER CHECK chk_email_format ENFORCED;激活约束。 - 注意,
NOT ENFORCED的约束在information_schema.check_constraints表中依然可见,ENFORCED字段会显示为NO。
不过也得提醒一句,别把它当成开发环境里的开关随便关。生产库一旦设为NOT ENFORCED,就等于主动放弃了一道数据防线。
CHECK 失效时的兜底方案:触发器不是备选,是必选
如果你所在的MySQL版本无法确认是不是真心支持CHECK,或者需要兼容老版本(比如还在跑MySQL 5.7),又或者要实现CHECK不支持的逻辑(比如查另一张表、调用函数、甚至发告警),那触发器就是唯一的选择。
举个例子,模拟“余额不能为负”这种校验:
DELIMITER $$ CREATE TRIGGER tr_check_balance_before_insert BEFORE INSERT ON accounts FOR EACH ROW BEGIN IF NEW.balance < 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不能为负'; END IF; END$$ DELIMITER ;
关键的点在于:
- 触发器在
BEFORE INSERT/UPDATE阶段执行,比应用层的校验更加底层、更靠谱。 - 要报错就必须用
SIGNAL主动抛异常。千万别图省事写个SET NEW.balance = 0这种静默修正——业务语义会被彻底埋没。 - 当然,触发器的逻辑越重,写入性能的下降越明显。高频交易表里,谨慎使用复杂的校验逻辑。
真实的生产环境里,CHECK和触发器往往共存:前者管的是简单的取值范围(比如状态枚举、金额正数),后者管的是跨表一致性或动态规则。别指望一个机制就能包打天下。
