news 2026/10/7 3:39:17

MySQL六种约束详解:从NOT NULL到CHECK,打造可靠表设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL六种约束详解:从NOT NULL到CHECK,打造可靠表设计

约束这词听起来像限制,实际是给数据库表结构“定规矩”。我在做 MySQL 表设计时,见过太多因为约束缺失导致的脏数据问题:重复的订单号、为空的外键、超出范围的数值。数据库不是 Excel,它应该替你挡住非法数据,而不是事后用UPDATE去“擦屁股”。这篇把 MySQL 里最常见的六种约束从头捋一遍,讲清它们各自解决什么问题、怎么用、会踩哪些坑,适合刚学完增删改查、准备认真做表设计的朋友。

1. 约束到底在解决什么问题?

1.1 约束的本质:数据库的“关卡”

约束(Constraint)是 MySQL 在数据写入层面设置的规则,你可以把它想象成游乐场入口的安检闸机——只有符合要求的乘客才能上车。

如果没有约束,一张用户表可能存进age = -5、重复的邮箱、没有所属部门的员工记录。这些脏数据在写入时看起来没什么,但一到了统计报表、联表查询甚至后续迁移阶段,就会变成灾难。我在实际项目里就遇到过一张没有任何约束的历史表,里面有十几条user_id = NULL的订单记录,导致每次汇总销售额都得额外写排除逻辑。

约束不是一个可有可无的“附加题”,它是表设计的第一道防线。MySQL 支持NOT NULL、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK、DEFAULT六种常见约束,它们分别是:字段能不能为空、值能否重复、如何唯一标识一行、如何维护表间关系、值能否超出范围、不传该字段时用什么兜底。理解了这六个问题,你基本就理解了关系型数据库的表结构设计。

1.2 约束选型的优先级:先主键,再非空,再看关系和范围

新手最容易犯的错误是“每个字段都加约束,把表堆成铁桶”。实际上,约束是需要按场景权衡的。我一般遵循这个顺序:

  1. 先确定主键:任何表都要有一个能被稳定识别的唯一标识,否则后续更新、删除、关联都无从谈起。
  2. 再处理必填字段:业务上必须存在的字段,比如订单金额、用户名,必须加NOT NULL。
  3. 然后处理唯一性:业务上天然唯一的字段,如邮箱、身份证号、订单号,用UNIQUE或唯一索引兜底。
  4. 最后考虑跨表关系和取值范围:需要关联父表时加外键;取值范围受限时加CHECK。

这个顺序不是绝对的,但它能避免你在一开始就陷入“这个字段该不该加外键”的纠结。约束不是越多越好,加多了会降低写入性能、增加维护成本,加对了才是对数据质量负责。

2. 六种约束逐个拆解

2.1 NOT NULL:让字段必须“有货”

NOT NULL是最朴素也最容易被人忽略的约束。它保证字段在插入和更新时不能为空值NULL。

很多人分不清NULL和空字符串'':NULL表示“值不存在”,''表示“值为一个长度为0的字符串”。这是两个完全不同的东西。比如电话号码字段,用户没填写,应该存NULL;用户填了一个空字符串,那反而不合理。所以NOT NULL的语义是“业务上必须存在的值”,而不是“不能为空字符串”。

建表时的写法很简单:

CREATE TABLE user ( id INT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NULL );

这里phone允许为空,因为不是所有人都愿意留电话。而name不允许为空,因为一条用户记录如果没有名字,后续维护和识别都会出问题。

实操中要注意:ALTER TABLE给已有数据表加NOT NULL约束时,如果表里已经存在NULL数据,MySQL 会直接报错。你得先处理旧数据,把NULL更新成合法值,再执行修改语句。

2.2 UNIQUE:给字段加“防重锁”

UNIQUE约束保证字段或字段组合的值在整张表中不重复。它和索引是伴生的——加了UNIQUE约束,MySQL 会自动创建一个唯一索引。

常见应用场景是业务上的唯一标识,比如:

  • 用户表的邮箱
  • 订单表的订单编号
  • 商品表的商品编码
CREATE TABLE user ( id INT PRIMARY KEY, email VARCHAR(100) UNIQUE, name VARCHAR(50) NOT NULL );

这里如果试图插入两条相同email的记录,第二条会报Duplicate entry 'xxx@example.com' for key 'user.email'。

UNIQUE约束还可以组合使用,比如一个“用户收藏商品”的表,要求同一个用户不能重复收藏同一个商品:

CREATE TABLE user_favorite ( user_id INT NOT NULL, product_id INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_product (user_id, product_id) );

组合唯一索引允许单个字段出现重复,但字段组合不能重复。这里也顺带说一个容易踩的坑:UNIQUE约束对NULL是网开一面的,多个NULL不会被视为重复。也就是说,如果某字段允许为NULL,那么你可以放心地插入多条NULL记录,UNIQUE不会拦你。这在语义上说得通:既然“值不存在”,那就不存在“重复”的问题。

2.3 PRIMARY KEY:主键约束的隐藏逻辑

主键是NOT NULL和UNIQUE的结合体,它规定字段既不能为空、也不能重复,并且一张表只能有一个主键。主键有两个关键作用:

  1. 唯一标识一行记录,方便通过主键快速定位和更新数据。
  2. 作为其他表外键关联的目标,表结构设计里主键就是“身份证号”。

实际开发中,我几乎总是用自增整数或雪花ID作为主键,而不是用业务字段。因为业务字段(比如身份证号)虽然唯一,但可能会变更,一旦变更,所有关联这个字段的外键表都要跟着改。用无业务含义的id列做主键,能隔离业务变化对关联关系的影响。

主键的定义方式有两种常见的写法:

-- 方式一:列级约束 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ); -- 方式二:表级约束,适合复合主键 CREATE TABLE order_item ( order_id INT NOT NULL, product_id INT NOT NULL, quantity INT, PRIMARY KEY (order_id, product_id) );

复合主键表示两条记录只有在所有主键字段都相同时才算重复。但它会带来一个问题:后续其他表想引用这张表时,外键也必须带上全部主键字段,维护成本较高。所以设计时优先考虑单列主键,除非场景真的需要复合唯一性。

2.4 FOREIGN KEY:外键约束的爱与痛

外键用于维护表与表之间的引用完整性。比如订单表里的user_id必须来自用户表的id,否则就成“孤儿订单”了。

CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, amount DECIMAL(10, 2) NOT NULL, CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES user(id) );

外键约束会自动检查插入、更新、删除操作是否符合引用关系。比如你想删除用户表中一个仍有订单的用户,MySQL 会默认拒绝删除,直到你先把该用户的订单处理掉。

外键还支持ON DELETE和ON UPDATE规则,常见的有:

  • CASCADE:级联更新或删除。
  • SET NULL:父记录删除时,子表外键字段置为NULL。
  • RESTRICT:拒绝操作,这也是 MySQL 默认行为。

我个人的建议是:中小型项目里外键能用则用,但它有代价。外键会在每次写操作时额外检查父表,影响写入性能;在数据迁移、批量导入时也会造成各种束缚。很多互联网大厂干脆禁用外键,把引用关系交给应用层去保证。这并不是说外键不好,而是不同场景取舍不同。如果你是学习阶段,建议亲手建一次外键、体验一下约束行为;如果是团队项目,要先和同事约定好外键策略。

另外,外键还有一个硬性前提:关联的两张表必须都是 InnoDB 引擎,而且关联字段的类型必须完全一致。比如父表是INT UNSIGNED,子表是INT,外键会创建失败。

2.5 CHECK:MySQL 8.0.16 之前之后的两个世界

CHECK约束用来限定字段的取值范围,比如年龄必须大于0、分数必须在0到100之间。但 MySQL 对 CHECK 的支持有个重要的分水岭:8.0.16 之前,CHECK 约束会被解析但不会强制执行。

什么叫“解析但不执行”?就是你写age INT CHECK (age > 0),建表不会报错,但你可以插入age = -5,MySQL 也会乖乖接受。这是早期 MySQL 的一个历史坑,很多老书和旧文章都因为这个说“MySQL 不支持 CHECK”,实际上不是不支持,是它偷懒没干活。

从 8.0.16 开始,MySQL 终于开始强制执行 CHECK 约束了。比如:

CREATE TABLE student ( id INT PRIMARY KEY, name VARCHAR(50) NOT NULL, age INT CHECK (age >= 6 AND age <= 100) );

插入age = 5会直接报错:

ERROR 3819 (HY000): Check constraint 'student_chk_1' is violated.

如果你用的还是 MySQL 5.7 或更早版本,请记住:别指望 CHECK 帮你挡非法数据,要么通过BEFORE INSERT触发器去校验,要么在应用层做判断。

2.6 DEFAULT:默认值不是约束?其实是“隐性约束”

严格说,DEFAULT定义的是字段的默认值,它属于列属性而不算传统意义上的约束。但在实际表设计中,它和约束经常一起出现,目的也是减少脏数据。

最常见的默认值是时间戳:

CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );

这样插入数据时不写created_at,MySQL 会自动填当前时间。ON UPDATE CURRENT_TIMESTAMP还能在记录被更新时自动刷新updated_at,省去应用层手动维护的时间字段。

注意一个细节:MySQL 的DEFAULT不支持函数表达式,只支持固定值或少数内置函数(如CURRENT_TIMESTAMP)。如果你希望默认值是UUID(),在 8.0.13 之前是不行的,之后才允许部分内置函数作为默认值。

3. 实操:建表时如何组合约束

3.1 一个用户订单系统的约束设计

纸上谈兵不如直接实战。假设我们要做一个简单的用户订单系统,包含用户表、商品表、订单表、订单明细表,看看约束怎么组合。

第一步,建用户表。用户必须有用户名,手机号唯一但允许为空(用户可能不填),创建时间有默认值:

CREATE TABLE `user` ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, phone VARCHAR(20) UNIQUE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第二步,建商品表。商品价格必须大于0,库存不能为负数:

CREATE TABLE product ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL CHECK (price > 0), stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0), PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第三步,建订单表。订单金额必须大于0,用户外键关联用户表,删除用户时不允许直接删(RESTRICT):

CREATE TABLE orders ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL, total_amount DECIMAL(10,2) NOT NULL CHECK (total_amount > 0), status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES user (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

第四步,建订单明细表。明细里的数量和价格不能为负,同时一个订单里不能有重复商品(用复合唯一键):

CREATE TABLE order_item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, quantity INT NOT NULL CHECK (quantity > 0), price DECIMAL(10,2) NOT NULL CHECK (price >= 0), PRIMARY KEY (id), UNIQUE KEY uk_order_product (order_id, product_id), CONSTRAINT fk_order_item_order FOREIGN KEY (order_id) REFERENCES orders (id), CONSTRAINT fk_order_item_product FOREIGN KEY (product_id) REFERENCES product (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这套设计下来,任何一张表都不会被写入“负数价格”“空用户ID”“重复商品行”这类基本脏数据。你可以亲手执行一遍,再试着插入几条违规数据,感受一下约束的拦截效果。这比单纯背概念有用得多。

3.2 ALTER TABLE 动态添加和删除约束

表已经建好之后,用ALTER TABLE也可以随时增删约束。常用语法如下:

-- 添加非空约束 ALTER TABLE user MODIFY username VARCHAR(50) NOT NULL; -- 添加唯一约束 ALTER TABLE user ADD UNIQUE KEY uk_phone (phone); -- 添加主键 ALTER TABLE order_item ADD PRIMARY KEY (id); -- 添加外键 ALTER TABLE order_item ADD CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders (id); -- 添加检查约束(MySQL 8.0.16+) ALTER TABLE product ADD CONSTRAINT chk_price CHECK (price > 0); -- 删除约束 ALTER TABLE order_item DROP FOREIGN KEY fk_item_order; ALTER TABLE order_item DROP INDEX uk_order_product;

这里有个重要提醒:删除外键用的是DROP FOREIGN KEY,但删除唯一约束用的是DROP INDEX。主键删除则是ALTER TABLE table_name DROP PRIMARY KEY。很多新手把外键约束名的语法套到唯一约束上,结果报ERROR 1091 (42000): Can't DROP 'xxx'; check that column/key exists,其实就是用的语法不对。

还有一个经验:在ALTER TABLE之前,先执行SHOW CREATE TABLE table_name\G查看当前表结构和约束名。约束名如果没显式指定,MySQL 会自动生成类似表名_chk_1、表名_ibfk_1这样的名字,删的时候需要用到它。

3.3 约束命名规范与查看方式

约束名看似不起眼,但在排错时特别重要。MySQL 里每个约束都有自己的名字,规则如下:

约束类型默认命名建议命名
PRIMARY KEYPRIMARYPRIMARY
UNIQUE字段名uk_表名_字段名
FOREIGN KEY表名_ibfk_序号fk_子表_父表
CHECK表名_chk_序号chk_表名_含义

比如fk_orders_user,一看就知道是订单表关联用户表的外键。在团队协作时,约束名统一规范能少很多沟通成本。

查看一张表的全部约束,最快的方法:

SHOW CREATE TABLE orders\G

或者在information_schema表里查:

SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = '你的数据库名' AND TABLE_NAME = 'orders';

每次排错前先看约束定义,比瞎猜报错原因要高效得多。

4. 常见问题与排查技巧实录

4.1 约束冲突报错:Duplicate entry 和 Check constraint violated

写入数据时最常见的约束报错有两个。

第一个是Duplicate entry 'xxx' for key 'uk_xxx',说明触发了唯一约束。常见原因不是真的业务重复,而是字段字符集或排序规则不一致,导致 MySQL 认为两条本应不同的数据是重复的。比如utf8mb4_general_ci是不区分大小写的排序规则,Abc@example.com和abc@example.com会被判定为重复。解决方法是换用区分大小写的排序规则,比如utf8mb4_bin。

第二个是Check constraint 'xxx_chk_1' is violated,这是 MySQL 8.0.16 以后才会出现的报错。排错时先查看 CHECK 约束定义,再回看插入的数据,确认是值超出范围还是逻辑判断写错。注意 CHECK 表达式里不能用子查询,也不能引用其他表的字段,只能在当前行值上做判断。

4.2 外键创建失败的几类原因

外键报错通常比唯一约束更复杂。我的经验是依次排查下面四点:

  1. 两张表必须都是 InnoDB 引擎。
  2. 两个关联字段的数据类型必须一致,包括长度和UNSIGNED属性。比如父表id INT UNSIGNED,子表user_id INT,就会报ERROR 3780 (HY000): Referencing column 'user_id' and referenced column 'id' ...。
  3. 关联字段必须有索引,父表的关联字段必须是主键或唯一键。
  4. 字符集和排序规则要一致。父表是utf8mb4,子表是utf8,也会失败。

遇到外键失败,先用SHOW CREATE TABLE检查引擎和字符集,再比较两列定义,大多数问题都能定位。

4.3 约束与性能:别把表设计成“铁桶”

约束能保证数据质量,但不是免费午餐。UNIQUE和PRIMARY KEY会创建索引,每次写操作都要维护索引;FOREIGN KEY写操作时要检查父表;CHECK在 8.0.16+ 每次写入时都要执行表达式判断。

我见过的糟糕设计是:一张流水记录表上,给十几个字段都加了唯一约束,结果并发写入时频繁撞约束,性能一塌糊涂。约束应该放在业务上必须唯一的字段上,而不是所有你觉得“以后可能用得上”的字段。

还有一个常见坑:对含有大字段(如TEXT、VARCHAR(255))的列加唯一索引时,如果字符集是utf8mb4,索引长度可能超过 MySQL 的 768 字节限制。解决方法是给前缀加索引,比如UNIQUE KEY uk_content (content(100)),或者用HASH字段存内容指纹再做唯一约束。

4.4 约束与数据迁移的冲突

数据导入场景下,约束经常变成“拦路虎”。比如用mysqldump备份还原时,如果目标库已有部分数据,还原过程会因为主键或唯一约束冲突而中断。

我的做法是:批量导入大量数据前,先临时关闭外键检查(只在当前会话生效):

SET FOREIGN_KEY_CHECKS = 0; -- 执行导入 SET FOREIGN_KEY_CHECKS = 1;

但注意,关闭约束检查不等于数据合法。导入后最好再做一次校验,确认没有产生孤儿数据。日常开发中,约束是“防君子不防小人”的规则,它减少的是人工失误,而不是替代业务逻辑校验。


我个人在实操中最大的感悟是:约束不是写代码时随便加上去的装饰,而是表设计阶段和业务方反复确认后的“硬规则”。你在建表时花十分钟想清楚哪个字段必须非空、哪个字段必须唯一,后面省下的可能是几个通宵排查脏数据的时间。设计约束时,多问问自己:这个字段如果为空会有什么后果?如果重复会出现什么风险?如果违反范围会带来什么损失?想清楚这三个问题,再动手写 SQL,表结构才是真正能扛事的表结构。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 3:39:03

DIY超声波阵列定向声波发射器:从压电换能器到参量阵实战

前一阵子动手做了一个超声波阵列定向声波发射器&#xff0c;算是我玩电子DIY以来最烧脑也最有成就感的一件事。外形上它只是一块亚克力板&#xff0c;上面密密码码焊了十几个银色的圆形探头&#xff0c;可通电之后你和它正常说话的距离&#xff0c;站在正前方两三米能听清声音&…

作者头像 李华
网站建设 2026/10/7 3:38:39

page-break-inside与break-inside:彻底解决CSS打印分页截断问题

1. 打印页面被拦腰截断&#xff1a;page-break-inside 到底在解决什么问题1.1 一次报价单打印翻车&#xff0c;让我重新审视这个属性我最早在 page-break-inside 上翻车&#xff0c;是在给客户做报价单打印的时候。客户把商品明细拉得很长&#xff0c;页面上看排版也还行&#…

作者头像 李华
网站建设 2026/10/7 3:37:51

tcpreplay 依赖链全解析:从 libpcap 到 libnl 的编译避坑指南

简介&#xff1a;这份资源面向需要在Linux服务器上离线部署tcpreplay的网络运维与测试人员&#xff0c;解决内网环境无法直接联网安装依赖的问题。压缩包共4个文件&#xff0c;以gz、tar源码包和sh安装脚本为主&#xff0c;整体约93.4MB&#xff0c;涵盖gcc、Bison、flex、libp…

作者头像 李华
网站建设 2026/10/7 3:37:41

贝塞尔曲线驱动RecyclerView滚动到位波纹动效的工程实践

做列表滚动结束后的波纹效果&#xff0c;这件事起初不是我自己想出来的。当时有个产品需求&#xff1a;在分类列表里滚动到指定位置&#xff0c;也就是自动吸附到某个分组的锚点&#xff0c;希望在停下来的那一瞬间&#xff0c;锚点位置冒出一圈像水面波纹一样扩散的光圈&#…

作者头像 李华
网站建设 2026/10/7 3:36:42

小波变换MIMO-OFDM系统Matlab仿真:误码率分析与频谱效率提升

先把结论放在前面&#xff1a;这套基于小波变换的MIMO OFDM通信仿真实测下来&#xff0c;在高信噪比区间能把误码率压到传统FFT-OFDM的一个数量级以下&#xff0c;而且去掉循环前缀之后频谱效率还能再提一截。如果你是正在做毕业设计、通信课程项目&#xff0c;或者想验证一下“…

作者头像 李华