1. SQL 约束核心能力速览
很多初学者写 SQL 建表时,只关注字段类型和长度,却在数据正确性上反复踩坑:重复数据混进来了、必填字段为空、关联记录被误删、分数列出现了负数。这些问题单靠应用程序判断并不可靠,多一个入口就多一处漏写校验的逻辑。真正的兜底方案,是在数据库层面把规则定死,让数据库自己拒绝非法数据。这套规则,就是 SQL 中的约束。
| 能力项 | 说明 |
|---|---|
| 约束本质 | 数据库主动校验数据合法性的规则,向表中插入、更新、删除数据时自动检查 |
| 常用约束 | NOT NULL、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK、DEFAULT 六大类 |
| 主要作用 | 保证数据完整性、一致性、准确性,减少脏数据 |
| 作用时机 | 插入(INSERT)时检查、更新(UPDATE)时检查、删除(DELETE)时按规则联动 |
| 操作方式 | 建表时定义、建表后用 ALTER TABLE 添加或删除 |
| 应用场景 | 订单、用户、库存等所有需要强一致性的业务表 |
| 适用平台 | MySQL、SQL Server、PostgreSQL、Oracle、SQLite 等主流关系型数据库 |
| 学习成本 | 低,掌握 CREATE TABLE 与 ALTER TABLE 即可 |
先给结论:约束不是可学可不学的知识点,数据完整性、数据库设计、慢 SQL 排查、后端接口报错,到最后都会绕回到约束上。这篇文章完整拆解六大约束的概念、SQL 语法、工程实践和常见坑位,并且给出可直接复制的建表和修改语句。
适合读者:正在学数据库原理的学生,刚接触 MySQL/SQL Server 的后端开发,以及被脏数据折磨、想从源头控制数据质量的工程师。
2. 约束到底是什么
约束(Constraint)从字面理解就是限制条件,是关系型数据库管理系统(RDBMS)提供的一套数据校验机制。它定义在表结构上,对所有写入数据的操作生效,只要违反规则,数据库直接拒绝执行并返回错误。
举个实际例子,用户表通常要求邮箱不能重复:
CREATE TABLE users ( id INT PRIMARY KEY, email VARCHAR(100) UNIQUE NOT NULL );这条语句中出现了三个约束:
PRIMARY KEY约束:id是主键,不能为空且必须唯一。UNIQUE约束:email不能重复。NOT NULL约束:email不能为空。
之后任何写入重复邮箱或空邮箱的操作都会被数据库拦截。
约束与数据类型、索引、触发器有本质区别:
- 数据类型约束的是存储格式,比如
INT列不能写入字符串。 - 约束约束的是业务语义,比如“邮箱不能重复”“分数不能为负”。
- 索引是加速查询的结构,但
UNIQUE约束会隐式创建唯一索引。 - 触发器是主动执行额外逻辑,约束是被动挡住非法操作,性能开销远低于触发器。
建立约束体系的意义可以总结为三句话:数据正确性由数据库保证,而不是依赖开发者自觉;多应用入口写同一套校验的成本远高于在数据库定义一次;线上数据的质量直接影响查询速度、统计报表和系统稳定性。
这里需要特别区分约束与搜索引擎的热词之间的关系。在 FPGA、时序设计等硬件领域也大量出现“约束”一词,例如“时钟约束”“IO 约束”“XDC 约束”,它指的是时序和引脚分配规则。本文讨论的是数据库管理系统中的 SQL 约束,两者完全不是一个技术领域,不要混淆。
3. 六大 SQL 约束逐一拆解
3.1 NOT NULL 非空约束
非空约束要求字段必须有值,插入或更新为NULL时被拒绝。建表时直接写在字段类型之后:
CREATE TABLE student ( id INT NOT NULL, name VARCHAR(50) NOT NULL, gender CHAR(1) );上面的语句表示:id和name都必须填值,gender可以不填。这里注意一个细节,NULL不是空字符串,也不是数字 0,它表示“未知值”。在数据库语义中,空字符串是有效值,NULL才是没有值。
在修改已有表时使用:
ALTER TABLE student MODIFY COLUMN name VARCHAR(50) NOT NULL;如果表中已有NULL记录,执行上面语句会失败。必须先处理脏数据,再添加非空约束:
UPDATE student SET name = 'unknown' WHERE name IS NULL; ALTER TABLE student MODIFY COLUMN name VARCHAR(50) NOT NULL;对于 SQL Server,语法略有不同:
ALTER TABLE student ALTER COLUMN name VARCHAR(50) NOT NULL;工程建议:新建表时不要把约束留到联调阶段再补,写建表语句就同步设计。后期给大表加 NOT NULL 约束,在千万级数据量上会锁表较长时间,影响线上业务。
3.2 UNIQUE 唯一约束
唯一约束保证一个字段或一组字段的所有值互不重复。和主键不同,一张表可以有多个唯一约束,且唯一约束允许存在 NULL 值(不同数据库行为略有差异,MySQL 中多个 NULL 不视为重复)。
CREATE TABLE customer ( id INT PRIMARY KEY, phone VARCHAR(20) UNIQUE, email VARCHAR(100) UNIQUE );唯一约束会自动创建唯一索引,所以它同时也是查询优化的一个重要手段。按手机号或邮箱查用户时,命中唯一索引效率很高。
它解决的实际问题很典型:用户注册时防止手机号被重复注册、订单表中防止订单号重复、库存系统中防止同一商品同一批次重复入库。
需要组合多个字段唯一时,写法是表级约束:
CREATE TABLE order_item ( order_id INT, product_id INT, quantity INT, UNIQUE (order_id, product_id) );这里表示同一个订单中不允许出现两行相同商品的记录。这种“联合唯一”是数据建模中非常常见的需求,务必掌握。
3.3 PRIMARY KEY 主键约束
主键是表内数据的唯一身份标识,它同时具备UNIQUE和NOT NULL两种特性,并且一张表只能有一个主键。主键可以是单列,也可以是多列组成的联合主键。
最基础的单列主键写法:
CREATE TABLE dept ( dept_id INT PRIMARY KEY, dept_name VARCHAR(100) );联合主键用法:
CREATE TABLE course_selection ( student_id INT, course_id INT, semester VARCHAR(20), PRIMARY KEY (student_id, course_id, semester) );联合主键表示一个学生在一个学期内选同一门课只能有一条选课记录。使用联合主键时要注意:
- 复合主键字段越多,索引体积越大,写入速度越慢。
- 避免使用过长字符串字段做主键,占用空间大,也会拖慢关联查询。
- 生产环境中更常见的做法是使用自增整型代理主键(
AUTO_INCREMENT或IDENTITY),把业务唯一性交给唯一约束解决,这样表结构更清爽,外键引用也更简单。 - 不要用身份证号、手机号等敏感业务字段做主键,业务值一旦变更会牵连所有外键引用。
对于 SQL Server 的自增主键写法:
CREATE TABLE employee ( emp_id INT IDENTITY(1,1) PRIMARY KEY, emp_name VARCHAR(50) );MySQL 对应:
CREATE TABLE employee ( emp_id INT AUTO_INCREMENT PRIMARY KEY, emp_name VARCHAR(50) );3.4 FOREIGN KEY 外键约束
外键约束建立表与表之间的关联,保证从表的某个字段值必须存在于主表的被引用字段中。这是关系型数据库实现“引用完整性”的核心机制。
订单表和用户表的经典场景:
CREATE TABLE customers ( id INT PRIMARY KEY, name VARCHAR(50) ); CREATE TABLE orders ( order_id INT PRIMARY KEY, customer_id INT, total_amount DECIMAL(10, 2), FOREIGN KEY (customer_id) REFERENCES customers(id) );这样设计后,插入订单时customer_id必须对应customers表中真实存在的id,否则插入失败。删除用户时,如果该用户下还有订单,删除会被阻止或触发级联操作,取决于外键的联动规则。
外键的 ON DELETE 和 ON UPDATE 规则:
| 规则 | 行为 | 实际场景 |
|---|---|---|
| RESTRICT | 存在关联记录时拒绝删除,默认行为 | 订单表有关联,禁止删用户 |
| CASCADE | 删除主表记录时自动删除从表记录 | 删除帖子时自动删除评论 |
| SET NULL | 主表删除后将从表外键置为 NULL | 删除商品后订单中商品 ID 置空 |
| NO ACTION | 与 RESTRICT 类似,检查时机略有差异 | 需要立即报错,不隐式修改 |
示例:
CREATE TABLE comments ( id INT PRIMARY KEY, post_id INT, content TEXT, FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE );工程中对外键的使用存在争议。互联网高并发系统中,很多团队为了写入性能选择在应用层维护引用关系,不使用数据库外键。但在内部管理系统、金融系统、ERP 等强一致性要求的系统中,外键依然是数据质量的保障。
从性能角度看,外键约束会带来额外检查开销。插入从表数据时要查主表索引验证存在性,批量导入时每一行都要验证,导入速度会明显变慢。如果项目是低并发后台系统,完整外键约束利大于弊;如果用户量很大、数据量在千万级以上,需要谨慎评估是否使用外键,必要时用应用程序事务来保证一致性。
3.5 CHECK 检查约束
CHECK 约束最直接地体现“业务规则下沉到数据库”的思维,它通过一个条件表达式来限制字段的取值范围。条件为真时数据才能写入。
CREATE TABLE product ( id INT PRIMARY KEY, name VARCHAR(100), price DECIMAL(10, 2), quantity INT, CHECK (price >= 0), CHECK (quantity >= 0) );这条规则保证商品价格和数量不能为负数。年龄限制也是典型场景:
CREATE TABLE person ( id INT PRIMARY KEY, name VARCHAR(50), age INT, CHECK (age >= 0 AND age <= 150) );在 MySQL 8.0.16 之前,CHECK 约束会被 MySQL 解析但不会真正生效,8.0.16 之后才正式强制实施。这是一个很重要的兼容性问题,如果你的团队使用老版本 MySQL,检查约束可能并没有真正拦截非法数据。而 SQL Server、PostgreSQL、Oracle 则长期完整支持 CHECK 约束。
列出 CHECK 约束:
ALTER TABLE person ADD CONSTRAINT chk_age CHECK (age > 0 AND age < 150);删除 CHECK 约束:
ALTER TABLE person DROP CONSTRAINT chk_age;CHECK 约束的局限是表达式必须能在数据库内部求值,不能调用存储函数。涉及多表联合判断的复杂规则应该在应用层或触发器中处理。
3.6 DEFAULT 默认值约束
DEFAULT 不在严格意义的数据校验范围内,它定义字段在未显式赋值时的默认值,防止遗漏字段导致写入失败或产生 NULL。
CREATE TABLE employee ( id INT PRIMARY KEY, name VARCHAR(50), hired_date DATE DEFAULT (CURRENT_DATE), status VARCHAR(20) DEFAULT 'active' );插入时不指定status字段,数据库自动填入'active';不指定hired_date,自动使用当前日期。
关键细节:
- 默认值可以是常量,也可以是系统函数,如
NOW()、CURRENT_TIMESTAMP。 DEFAULT不会在 UPDATE 时自动重置,只在 INSERT 没有指定值时生效。- 如果 Default 设置后表已有大量数据,新增字段时数据库会扫描全表填充默认值,大表上有锁表风险。
- 修改默认值用
ALTER TABLE ... ALTER COLUMN ... SET DEFAULT,具体语法在不同数据库中差异较大。
-- SQL Server / PostgreSQL ALTER TABLE employee ALTER COLUMN status SET DEFAULT 'inactive';-- MySQL ALTER TABLE employee ALTER COLUMN status SET DEFAULT 'inactive';4. 约束的命名规范与查看方式
写约束时如果不指定名称,数据库会自动命名,比如PRIMARY、symbol、constraint_1。这种随机命名让后续删除和排查变得困难。工程实践中应该显式命名。
CREATE TABLE payment ( id INT, order_id INT, amount DECIMAL(10, 2), CONSTRAINT pk_payment PRIMARY KEY (id), CONSTRAINT fk_payment_order FOREIGN KEY (order_id) REFERENCES orders(id), CONSTRAINT chk_payment_amount CHECK (amount >= 0), CONSTRAINT uq_payment_order UNIQUE (order_id) );命名建议统一前缀:主键pk_,外键fk_,唯一约束uq_,检查约束chk_,默认值约束df_。
查看表的约束信息,各数据库语法不同。
MySQL:
SHOW CREATE TABLE payment;SQL Server:
SELECT name, type_desc FROM sys.key_constraints WHERE parent_object_id = OBJECT_ID('payment');PostgreSQL:
SELECT conname, contype FROM pg_constraint WHERE conrelid = 'payment'::regclass;5. 约束的管理:添加、删除与修改
建表后要维护约束,通常使用ALTER TABLE语句。不同操作的目标不同,语法也有差异。
给已有表添加主键:
ALTER TABLE customer ADD PRIMARY KEY (id);给已有表添加外键:
ALTER TABLE orders ADD CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers(id);给已有表添加唯一约束:
ALTER TABLE customer ADD CONSTRAINT uq_customer_email UNIQUE (email);给已有表添加检查约束:
ALTER TABLE product ADD CONSTRAINT chk_product_price CHECK (price > 0);删除约束时,MySQL 不支持DROP CONSTRAINT统一写法,必须分类型处理:
-- MySQL 删除主键 ALTER TABLE customer DROP PRIMARY KEY; -- MySQL 删除唯一约束 ALTER TABLE customer DROP INDEX uq_customer_email; -- MySQL 删除外键 ALTER TABLE orders DROP FOREIGN KEY fk_orders_customer;SQL Server 和 PostgreSQL 可以用统一语法:
ALTER TABLE customer DROP CONSTRAINT uq_customer_email;约束修改流程是:先删除旧约束,再添加新约束。没有直接修改约束的便捷语法。比如把检查条件从price > 0改为price >= 0,必须先删除再重新添加。
约束添加失败的主要原因:
- 表中已有数据不满足新约束条件,必须先清理数据。
- 外键关联的字段在主表中没有对应索引,部分数据库要求外键列必须建索引。
- 主键字段中包含 NULL 或重复值。
- 唯一约束字段中已有重复值。
在线大表添加约束的风险前面提到过,MySQL 5.6 之前ALTER TABLE会锁表,5.6 之后使用ALGORITHM=INPLACE可以降低影响,但仍有风险。生产环境中对超大表加约束,建议采用在线 DDL 工具,并选择业务低峰期执行。
6. 约束与索引的关系
约束和索引是两个不同概念,但关系密切。
- 主键约束会自动创建唯一索引。
- UNIQUE 约束会自动创建唯一索引。
- 外键约束不会自动创建索引,但 MySQL 官方建议给外键列创建索引。没有索引时,父表删除记录需要扫描整个子表去检查外键引用,性能很差。
- CHECK 和 NOT NULL 不会创建索引,也不能直接通过约束加速查询。
理解了这一点就能明白:约束的主要职责是保证数据正确性,但它捎带影响了数据访问效率。主键和唯一索引对等值查询和关联查询有非常直接的加速效果。慢 SQL 排查时,缺主键的表往往被首先怀疑。
有一种说法是索引越多写入越慢,因为每次插入都要维护所有索引。在添加唯一约束时,要明确这个字段是不是真的需要全局唯一。运行中的大表,加一个不必要的唯一约束,等于新增一个索引,会直接影响写入性能。
7. 不同数据库中的约束差异
主流数据库对 SQL 约束的支持程度和写法有明显区别,迁移时经常踩坑。下表是核心差异汇总:
| 功能点 | MySQL 8.0+ | SQL Server | PostgreSQL | Oracle |
|---|---|---|---|---|
| CHECK 约束强制执行 | 8.0.16 起强制 | 支持 | 支持 | 支持 |
| 级联删除 | 支持 | 支持 | 支持 | 支持 |
| 延迟约束 | 不支持 | 部分支持 | 支持 | 支持 |
| 自增语法 | AUTO_INCREMENT | IDENTITY | SERIAL / IDENTITY | IDENTITY / SEQUENCE |
| 修改字段名语法 | CHANGE COLUMN | RENAME COLUMN | RENAME COLUMN | RENAME COLUMN |
| 约束默认命名 | 自动按表名生成 | 自动生成随机名 | 自动生成随机名 | 自动生成 SYS_C 开头名 |
CHECK 约束的兼容性是历史大坑。早版本 MySQL 中写入的 CHECK 约束会被解析但完全不生效,做表结构迁移时要检查老表的约束是否真实在拦数据。建议用如下方式验证:
INSERT INTO product (id, name, price, quantity) VALUES (1, 'test', -100, -100);如果插入成功且没有报错,说明 CHECK 约束没有生效。
SQL Server 中还有一个特有概念叫过滤约束(Filtered Constraint),PostgreSQL 同样支持部分唯一索引。比如只约束未删除记录的手机号唯一,已删除的软删除记录可以允许重复。
PostgreSQL 和 SQL Server 的部分唯一索引写法:
-- PostgreSQL CREATE UNIQUE INDEX uq_users_email_active ON users (email) WHERE is_deleted = false; -- SQL Server CREATE UNIQUE INDEX uq_users_email_active ON users (email) WHERE is_deleted = 0;这个特性对于软删除场景非常实用。标准 SQL 本身没有这种写法,属于数据库扩展功能,能处理常规 UNIQUE 约束做不到的“条件唯一”需求。
8. 约束在批量任务与数据清洗中的应用
约束不只是建表时用一次,在批量数据导入、数据清洗、慢 SQL 优化中同样举足轻重。
8.1 批量导入约束检查
大批量导入数据,通常先用临时表或无约束表接收数据,清洗完成后一次性添加约束。这套流程比逐行校验快得多。
推荐流程:
- 创建与目标表结构相同但无约束的暂存表。
- 用
LOAD DATA或BULK INSERT或程序批量写入。 - 按业务规则清洗,删除重复记录、修复空值、修正非法数据。
- 执行 ALTER TABLE 添加约束。
- 若约束添加失败,根据错误定位脏数据并重新处理。
SQL Server 的批量导入写法:
BULK INSERT staging_users FROM 'C:\\data\\users.csv' WITH ( FIELDTERMINATOR = ',', ROWTERMINATOR = '\\n', FIRSTROW = 2 );MySQL 对应:
LOAD DATA INFILE '/data/users.csv' INTO TABLE staging_users FIELDS TERMINATED BY ',' LINES TERMINATED BY '\\n' IGNORE 1 ROWS;批量写入外键约束时,逐行验证引用关系会明显拖慢导入速度。建议流程是:先暂存表,后校验,再启用约束。这里最优做法是导入前先关闭约束,导入完成后再重建。SQL Server 和 Oracle 支持NOCHECK或DISABLE CONSTRAINT指令。MySQL 没有直接关闭外键约束的语法,但可以用SET FOREIGN_KEY_CHECKS = 0临时跳过检查,之后恢复:
SET FOREIGN_KEY_CHECKS = 0; -- 执行批量导入 SET FOREIGN_KEY_CHECKS = 1;8.2 约束对慢 SQL 的影响
查询性能与数据分布密切相关。没有约束的表,数据质量差,COUNT(DISTINCT)、GROUP BY、JOIN的结果都可能令人怀疑。唯一约束和主键约束生成的索引,对常用等值查询是明显的加速器。外键约束强制数据一致后,多表 JOIN 不会因为缺失匹配数据产生大量意料外的空行。
8.3 约束冲突数据定位
添加约束失败时,数据库往往只报“Duplicate entry”或者“Check constraint violated”,不会直接告诉你哪些行有问题。常见定位 SQL 如下。
查重复值:
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;查违反非空约束的记录:
SELECT * FROM users WHERE name IS NULL;查未匹配外键的记录:
SELECT * FROM orders o LEFT JOIN customers c ON o.customer_id = c.id WHERE c.id IS NULL;9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 添加唯一约束失败 | 目标列已有重复值 | 用 GROUP BY ... HAVING COUNT(*) > 1 查重复 | 去重或处理重复数据后再添加 |
| 插入外键列报错 | 外键值在主表不存在 | 单独 SELECT 主表对应值 | 修正引用值或先插入主表数据 |
| CHECK 约束不生效 | MySQL 版本低于 8.0.16 | SHOW CREATE TABLE 查看 DDL | 升级数据库,或改用应用层校验加触发器 |
| 删除主表数据被拒绝 | 从表有外键引用 | 查看关联表子记录 | 先删从表或使用 ON DELETE CASCADE |
| ALTER TABLE 一直阻塞 | 大表加约束,DML 频繁 | 查看 SHOW PROCESSLIST / sys.dm_exec_requests | 分批次加约束或使用在线 DDL 工具 |
| 批量导入速度极慢 | 每条数据逐行校验外键和唯一索引 | 观察导入耗时 | 临时关闭外键检查,导入后重建索引 |
| 约束命名冲突 | 不同表约束名重复 | 查看数据库约束信息 | 约束名在 schema 级唯一,重命名 |
| 数据库迁移数据类型失败 | 源库有违反目标库约束的数据 | 导出时做数据校验 | 用暂存表清洗后再导入 |
补充一个重要注意点:MySQL 中删除约束的方式因约束类型不同而不同,最容易忘的是外键要使用DROP FOREIGN KEY而不是DROP INDEX。虽然 MySQL 的文档中DROP FOREIGN KEY会自动删除关联索引,但如果操作写错,会一直报语法错误。
10. 最佳实践与使用建议
10.1 建表阶段就设计约束
建表是约束成本最低的时间点。表已经上线运行、灌入大量数据后再加约束,会产生脏数据冲突、锁表风险、迁移脚本编写等大量额外工作。数据库建模评审阶段就要明确每个字段的非空性、唯一性、取值范围和引用关系。
10.2 约束命名规范化
所有约束显式命名,统一pk_、fk_、uq_、chk_前缀。方便后续维护、排错和数据库迁移。自动命名的约束在多人协作和大规模表结构中完全不可维护。
10.3 区分业务约束与数据库约束
业务规则复杂多变的部分,比如“订单金额不能超过账户余额”“一个用户最多创建 10 个店铺”,不适合直接用 CHECK 或外键表达,建议在应用层或存储过程中实现。数据库约束适合承载本质性的、稳定的数据规则,比如主键唯一性、必填字段、金额非负。把易变业务规则放进数据库会导致频繁修改表结构,维护成本极大。
10.4 批量任务注意解耦
批量初始化数据、清洗脏数据、导入历史数据时,遵循先写入再校验的思路,利用暂存表和分批添加约束,避免反复触发逐行校验导致性能问题。
10.5 关注大表操作窗口
给一个大表加主键、唯一约束或者非空约束,都可能产生长时间锁。操作前查看表大小和业务低峰期,优先使用在线 DDL 工具,涉及外键约束更需谨慎。
10.6 数据导出迁移前先做合规校验
涉及身份证、手机号、邮箱等敏感信息时,导出前先做脱敏和合规评估。唯一约束的建立与敏感字段的业务属性相关时,要同步考虑授权范围和数据使用边界。
10.7 不要依赖数据库纠错
约束是最后一道闸门,不是开发的借口。开发接口时,仍然要在应用层做参数校验,给用户友好的错误提示。否则数据库报的“Duplicate entry”可能直接泄漏表结构信息,在存档级安全要求较高的系统中也建议对数据库报错信息做统一包装。
11. 总结与下一步
SQL 约束的核心价值在于把数据规则沉淀到数据库层,让每个写入数据的入口都遵守同一套规则。最值得优先掌握的是主键约束、非空约束和唯一约束,这三个是绝大多数业务系统的标配,也是最常出现在数据库设计和 SQL 面试中的内容。外键与 CHECK 约束次之,使用时要结合业务场景和团队对数据库性能的要求来决定。
下一步的建议很明确:把你负责的核心业务表打开,检查三条约束是否到位——每个表都有主键吗?必填字段都有非空约束吗?业务上不可重复的字段都建了唯一约束吗?这三条检查完,再考虑外键和 CHECK 的细化设计。训练时可以挑一个订单表结构,自己写一套包含六大约束的建表语句,再用ALTER TABLE完成增删改操作,很快就能把 SQL 约束的使用手感建立起来。
最容易踩的坑也再强调一次:MySQL 8.0.16 之前的 CHECK 约束不生效,批量导入时逐行校验拖慢速度,大表加约束前务必先评估锁表风险。把这三个点记在脑子里,再复杂的约束问题也能快速定位。