CHECK约束(检查约束)是SQL中一种非常实用的数据完整性约束,它的作用很简单:给某一列或几列的取值加上一个条件,凡是插入或更新的数据不满足这个条件,数据库就直接报错拒绝。很多团队习惯把数据校验全部放在应用层做,但应用层校验有一个天然漏洞——只要有一条路径绕过了业务代码(比如运维直接执行SQL脚本、其他系统直连数据库写入),脏数据就进来了。CHECK约束相当于在数据库层面再加一道防线,无论数据从哪个入口进来,都必须过这一关。

CHECK约束到底能做什么
从原理上讲,CHECK约束就是一个返回布尔值的表达式。数据库在每次INSERT或UPDATE时都会对这个表达式求值,结果为TRUE就放行,结果为FALSE就拒绝整个操作。它的典型应用场景包括:限制数值范围(年龄必须在0到150之间)、限制枚举值(状态只能是几个固定值)、保证字段间的关系(结束日期必须晚于开始日期)等。
下面通过建表语句来看最基础的用法:
CREATE TABLE employee (
id INT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
age INT,
salary DECIMAL(10,2),
-- 单列上的简单检查
age INT CHECK (age >= 18 AND age <= 60),
status VARCHAR(20) CHECK (status IN ('active', 'inactive', 'locked')),
-- 保证月薪不超过年薪的十二分之一
CONSTRAINT chk_salary CHECK (salary >= 0)
);上面演示了两种写法:一种是直接跟在列定义后面,适合简单的单列条件;另一种是用CONSTRAINT 约束名 CHECK(条件)的独立写法,可以引用多个列。强烈建议使用后者并显式命名,因为命名之后的管理会方便很多,比如删除约束时可以直接按名字删,而不需要去查系统表找到数据库自动生成的那个随机名字。
还有一种场景是在表已经存在之后追加约束,这时要用ALTER TABLE语句:
-- 给已有表添加命名的CHECK约束 ALTER TABLE employee ADD CONSTRAINT chk_age_range CHECK (age >= 18 AND age <= 60); -- MySQL 8.0.16之前的版本不支持CHECK,会静默忽略这个语法 -- 删除约束的方式 ALTER TABLE employee DROP CONSTRAINT chk_age_range; -- SQL Server的写法 ALTER TABLE employee DROP CONSTRAINT chk_age_range; -- MySQL 8.0.16+ 的写法 ALTER TABLE employee DROP CHECK chk_age_range;
需要注意,如果表里已经存在不满足条件的数据,执行ALTER TABLE添加约束会直接失败。这是一个很常见的坑:想给线上老表补约束,结果发现历史数据早就越界了。正确的做法是先用查询语句找出不合规的数据清洗掉,再添加约束;或者先添加约束再看报错提示,逐批修复。
跨列条件和表达式的高级用法
CHECK约束真正灵活的地方在于它可以同时引用多列,实现字段之间的逻辑校验。举个例子,订单表里通常有折扣金额和订单金额两个字段,业务上要求折扣不能超过订单金额本身,这类关系用CHECK约束表达非常自然:
CREATE TABLE orders (
order_id INT PRIMARY KEY,
amount DECIMAL(10,2) NOT NULL,
discount DECIMAL(10,2) DEFAULT 0,
start_date DATE,
end_date DATE,
CONSTRAINT chk_discount CHECK (discount <= amount),
CONSTRAINT chk_date_range CHECK (end_date >= start_date)
);很多数据库还允许在CHECK约束中使用函数。比如在PostgreSQL中可以对字符串做正则匹配,保证邮箱格式、手机号格式大体正确:
-- PostgreSQL 支持正则表达式的CHECK约束
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
phone VARCHAR(20),
email VARCHAR(100),
CONSTRAINT chk_phone CHECK (phone ~ '^1[3-9][0-9]{9}$'),
CONSTRAINT chk_email CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$')
);不过函数的使用要谨慎。首先不是所有数据库都支持,SQL Server就不允许在CHECK约束里使用子查询;其次约束会在每一行写入时求值,如果函数计算开销大,会拖慢写入速度。原则上CHECK约束里的表达式应该尽量简单、确定,不要依赖其他表的数据,也不要使用诸如CURRENT_TIMESTAMP这类每次求值结果都不同的函数,否则约束的行为会变得难以预测。
NULL值与各数据库的支持差异
CHECK约束有一个容易被忽略的特性:当列的值为NULL时,约束表达式求值结果为UNKNOWN,而UNKNOWN并不会阻止写入。也就是说,CHECK (age >= 18)约束不了NULL值。如果业务上要求该列必须有值,正确做法是配合NOT NULL一起使用,CHECK负责范围校验,NOT NULL负责非空校验,两者各司其职。
各数据库对CHECK约束的支持程度也不一致,整理如下:
| 数据库 | 支持情况 | 说明 |
|---|---|---|
| MySQL | 8.0.16起支持 | 之前的版本解析语法但直接忽略,约束完全不生效 |
| PostgreSQL | 完整支持 | 支持正则、函数等丰富表达式 |
| SQL Server | 完整支持 | 不允许使用子查询 |
| Oracle | 完整支持 | 约束名不填会自动生成SYS_C开头名称 |
MySQL这个差异点要特别留意。如果你的系统要在多个MySQL版本间迁移,或者开发环境用的是旧版本,CHECK约束写了等于没写,数据校验形同虚设。稳妥的方式是在建表后手动执行几条违规数据验证约束是否真的生效,比如往age列插一个5,看数据库是否报错,确认无误再依赖它。
使用建议与总结
综合来看,CHECK约束适合承接那些规则明确、稳定不变、只涉及当前行数据的校验逻辑。范围校验、枚举值校验、字段关系校验是它的强项;而涉及跨表查证、需要外部接口调用的复杂校验,仍然应该放在应用层完成。两者并不冲突,数据库层的约束是兜底,应用层的校验负责给出友好的错误提示,各干各的活。
最后几点实践建议:一是所有CHECK约束都显式命名,命名规范统一,方便日后维护和排查;二是添加约束前先检查存量数据,避免线上操作失败;三是上线前用违规数据实测一遍,确认数据库版本真的执行了约束;四是别把过于复杂的业务规则塞进CHECK,约束一旦创建,修改的成本比改代码高得多。把这些细节做到位,CHECK约束就能成为保障数据质量的一件趁手工具。
SQL CHECK约束检查约束数据库约束修改时间:2026-09-10 22:02:45