SQL CHECK 约束 vs 触发器 vs 应用层校验的约束优先级排序

1次阅读

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

SQL CHECK 约束 vs 触发器 vs 应用层校验的约束优先级排序

CHECK 约束在 INSERT/UPDATE 时最先执行

数据库收到一条 INSERTUPDATE 语句后,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 配置和批量操作漏网点;前端校验……就当它不存在好了。

text=ZqhQzanResources