check约束最先执行,发生在语句解析和写入缓冲区阶段,早于触发器;触发器次之,可修改new值但无法绕过check;应用层校验仅预筛选,无强制力。

CHECK 约束在 INSERT/UPDATE 时最先执行
数据库收到一条 INSERT 或 UPDATE 语句后,CHECK 约束是第一道校验关卡——它发生在语句解析和行数据写入缓冲区阶段,早于任何触发器运行。只要违反 CHECK 条件(比如 age ),语句直接报错,根本不会走到后续逻辑。
常见错误现象:Error: new row for relation "users" violates check constraint "users_age_check"。这种错误通常出现在批量导入或应用层未做前置判断时。
- CHECK 是声明式约束,由 postgresql / SQL Server / oracle 等引擎原生支持(mysql 5.7 及以前会忽略 CHECK,8.0+ 才真正生效)
- 不能引用其他行、其他表,也不能调用非确定性函数(如
NOW()、RANDOM()),否则建表失败 - 不锁表,但每次写入都需计算表达式,高并发下简单 CHECK 影响极小,复杂表达式(含子查询)会显著拖慢性能
触发器在校验链中排第二,且可覆盖 CHECK 的“意图”
触发器(BEFORE INSERT OR UPDATE)在 CHECK 之后、实际写入前执行。它能访问 NEW 行数据,也能做跨表查询、调用函数、甚至修改 NEW 字段值——这意味它可能“绕过”CHECK 的原始限制。
使用场景:需要动态校验(如“用户所属部门必须存在”,而部门表在另一库)、或自动补全字段(如把空 slug 自动生成)。但注意:如果触发器改了 NEW.age 导致它又违反 CHECK,语句仍会失败。
- 触发器里抛出异常(
raise EXCEPTION)和 CHECK 失败效果一致,但错误信息更可控 - 一个表可有多个触发器,执行顺序按创建时间升序;若依赖顺序,建议命名体现优先级(如
trg_validate_01_business) - 触发器无法阻止同一事务中其他语句对同一行的并发修改,CHECK 也无法,这点上两者都不提供隔离保障
应用层校验本质是“预筛选”,不具强制力
应用代码里的 if user.age 或 ORM 的 <code>validates('age'),只是在数据发给数据库前做一次快速拦截。它不参与数据库的执行计划,也不影响事务原子性。
容易踩的坑:本地开发时一切正常,上线后因网络延迟、多服务共用 DB、或直连 SQL 工具操作,导致校验被绕过。尤其当业务方提供 API 接口,又允许后台脚本直连 DB 时,应用层校验形同虚设。
- 适合做用户体验优化(如前端实时提示、减少无效请求),而非数据一致性保障
- 与数据库约束重复校验不是 bug,而是分层防御:应用层快、反馈好;数据库层稳、兜底强
- ORM 如 SQLAlchemy 的
@validates默认只在session.add()时触发,bulk_insert_mappings会跳过,务必测试边界路径
优先级排序不是静态的,而是执行时序 + 作用域决定的
所谓“优先级”,其实是数据库引擎内部处理语句的固定流水线:语法解析 → CHECK 校验 → 触发器执行 → 写入 WAL → 提交。应用层压根不在这个链路上,它只是上游的“喊话人”。
真正容易被忽略的是:CHECK 和触发器都依赖当前事务的可见状态,而应用层看到的可能是旧快照(如 READ COMMITTED 隔离级别下),导致三者对“合法数据”的判断出现偏差。比如应用查到部门存在,CHECK 允许插入,但触发器执行时该部门已被另一个事务删掉——这时触发器报错,而 CHECK 早已通过。
别迷信某一层的“最强校验”,关键看它是否覆盖你的数据变更路径。dba 通常只信 CHECK + 触发器;后端工程师得盯紧 ORM 配置和批量操作漏网点;前端校验……就当它不存在好了。