上篇把主键、非空、默认值、唯一约束这四类“单表内部”的基础约束都聊透了这篇往下接着讲表的约束里最容易让人翻车的两个大头外键约束和检查约束CHECK再加上“约束已经建好了后面怎么改、怎么删、怎么查”这些日常维护操作。只要你的项目涉及多表关联外键和CHECK就决定了数据能不能在库层面被拦住而不是等到业务代码跑出一堆脏数据后才追悔莫及。这篇适合两类人一类是刚把MySQL语法学完、准备动手设计真实表的初学者另一类是写了不少SQL但在生产环境里对约束又爱又怕的开发者。我尽量把版本差异、踩坑现场和可直接抄的建表方案都写清楚。1. 表的约束下这篇到底在补什么1.1 为什么要把这两类约束单独拿出来讲先理清一个概念上篇讲的主键、非空、默认值、唯一约束本质上都是在管理“这一张表自身的数据质量”。比如某列不能为空、某列的值不能重复、某列不填时给个默认值这些规则即便没有外部参照也能成立。但外键约束它是跨表的。它表达的是表与表之间的一种“依赖关系”——子表里的某条记录必须指向父表里真实存在的一条记录。这就不是单表能决定的事了它要求我们先想清楚表之间的关系再去设计约束。CHECK约束也不太一样它属于更细粒度、更贴近业务规则的校验。比如成绩不能超过100、性别只能是男或女、结束时间必须晚于开始时间。这类规则如果放在应用层写每个入口都要重复校验漏一处就出问题如果放在数据库层只要一条INSERT或UPDATE触犯了规则直接报错谁都绕不过去。从学习路线上说这两类约束是“进阶”部分但它们在实际建表里出现的频率一点都不低。很多人建表只记得加主键结果多表联查时发现数据一对不上或者删除父表记录时留下一堆孤儿数据这就是没理解外键的作用。所以这篇与其说是讲语法不如说是在帮你建立“表格关系设计”的直觉。1.2 用生活场景理解约束分层我习惯把数据库约束想成一个公司里的多道审批流程。数据类型和长度是最基础的“格式审查”字段是数字还是字符串最多写多少位。主键和唯一约束是“一人一ID工号不重复”的规则保证每行能被唯一找到。非空和默认值是“必填项不能空着没填就按默认方案处理”。外键约束则是“员工归属部门而部门必须真实存在”杜绝把一个员工分到不存在部门的情况。CHECK约束更像财务审核的底线比如金额不允许为负数超出标准直接打回。在这个体系里外键和CHECK的定位是“业务安全网”。它们不负责提升查询性能也不负责优化存储只负责在你对业务考虑不周时数据库帮你守住最后一道关口。理解这一点之后再去看后面的语法和注意事项每一步都有目的。2. 外键约束表和表之间的“户口登记”2.1 外键到底在防什么事先看一个特别常见的场景。有一张学生表一张选课成绩表成绩表里存了student_id。如果某天业务要删除某个学生程序先删了学生表的记录却没有同步删除成绩表里的数据那成绩表里就出现了“查无此人”的记录。这类记录就叫孤儿数据。更麻烦的是如果成绩表里还没有任何索引去限制这个student_id你甚至无法快速查出来哪些成绩是孤儿数据。时间一长报表统计全是脏数据对账对不上。外键就是为此设计的。当你给成绩表的student_id字段加上外键指向学生表的id字段之后数据库会强制保证成绩表里出现的student_id必须已经在学生表里存在。任何违反这个规则的INSERT、UPDATE、DELETE都会被阻止。我在生产环境里见过最典型的场景是订单表和用户表。订单表里的user_id如果没做外键一旦用户注销时程序出错订单就成了“无主订单”客服查起来非常痛苦。加了外键之后用户想删都删不掉必须先把订单处理完虽然要多写几步逻辑但数据安全很多。2.2 外键的创建语法与硬性限制外键可以在建表时直接声明也可以建表后用ALTER TABLE追加。基础语法长这样CREATE TABLE 子表 ( id INT PRIMARY KEY, parent_id INT, CONSTRAINT fk_name FOREIGN KEY (parent_id) REFERENCES 父表(id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINEInnoDB;如果表已经存在用ALTER TABLE追加ALTER TABLE 子表 ADD CONSTRAINT fk_name FOREIGN KEY (parent_id) REFERENCES 父表(id);这里有一个非常关键的坑外键不是你想加就能加的。MySQL对外键有一堆硬性限制任何一条不满足创建就会报ERROR 1215。我把最常踩的限制列一下父表被引用的列必须是主键或唯一索引列。如果父表id列既不是主键也没有唯一索引外键直接失败。子表外键字段和父表被引用字段的数据类型必须匹配。比如都是INT不能一边INT一边BIGINT这在实际项目里非常容易被忽视。两个表都必须使用InnoDB存储引擎。MyISAM不支持外键。字符集和排序规则必须一致。如果一张表是utf8mb4另一张是latin1外键创建会失败这种情况在从旧库迁移或导入数据时特别常见。子表里不能已经有违反外键规则的脏数据。比如子表里已经存在一个parent_id999而父表根本没有999那么外键也建立不起来。我曾经在帮同事排查问题时发现他在MySQL 8.0上创建外键一直失败最后用SHOW CREATE TABLE对比两个表的字符集才发现父表是utf8子表是utf8mb4统一字符集之后立刻成功。这种问题从报错信息上第一眼很难看出来但如果把上面这些限制逐项比对一遍基本都能定位。2.3 删除和更新时的四种策略怎么选外键不仅管插入还管父表发生DELETE或UPDATE时子表数据怎么办。这四种策略初学者最容易混淆整理成表策略父表动作时子表行为适用场景CASCADE父表删除/更新子表联动删除/更新强关联关系如订单明细随主订单一起清理SET NULL父表删除/更新子表对应字段置NULL外键列允许为空且需要保留子表记录时RESTRICT存在子表记录时父表操作直接拒绝安全要求高宁可报错也不清理NO ACTION同RESTRICT在InnoDB中等效同上SET DEFAULTInnoDB不支持基本不用CASCADE适合什么场景比如一个订单主表和一个订单明细表订单删了明细留着没有任何意义直接用CASCADE让数据库自动清理。但有一个必须注意的问题CASCADE是数据库隐式执行的不是应用代码主动删的。如果项目里每个表都大量使用CASCADE一旦误删父表数据关联的子表数据会在你完全没意识到的情况下被大批量清空这个后果非常难挽回。SET NULL适合的是“子表记录应该保留但关联关系可以断开”的场景。比如员工表删除员工后排班表的employee_id置为NULL这样排班历史还在只是不再关联具体员工。使用SET NULL有一个前提子表外键列必须允许NULL如果该列设置了NOT NULL这策略就没法生效。RESTRICT和NO ACTION是我个人在生产环境里最推荐默认使用的策略。它们的行为是只要子表还有记录引用父表父表就删不掉、也改不掉。开发人员必须在应用层先处理子表数据再操作父表。虽然流程繁琐一点但每一步都是显式的不容易造成级联误伤。2.4 物理外键和逻辑外键怎么选说完策略说一个很多教程不会讲但实际项目里必然会遇到的问题物理外键虽然安全但在高并发、大流量的业务系统里未必是首选。物理外键的好处是完整性由数据库保证逻辑简单。但它的缺点是每次插入、更新子表数据库都要额外去父表校验批量删除或更新时还会产生额外的锁竞争影响写入性能。在分库分表架构下外键更是直接没法用因为关联的表可能根本不在同一个库。所以在真实生产环境里很多团队会选择“逻辑外键”表结构设计时保留关联字段比如order表的user_id但不在数据库层加FOREIGN KEY而是通过应用层事务和查询来保证数据一致性。那是不是说物理外键就没用了不是。在内部管理系统、后台系统、数据一致性要求极高的财务系统里物理外键依然是很好的选择。我的建议是新项目如果并发量不高优先加物理外键让数据安全兜底如果明确知道将来会分库分表或者写入量非常大就采用逻辑外键同时把关联校验逻辑做得足够严密并辅以定时任务扫描异常数据。3. CHECK约束从“解析但不执行”到真正生效3.1 版本差异是最大的坑如果你在网上搜CHECK约束的用法会发现大量资料说“MySQL的CHECK约束不生效只是摆设”。这话在MySQL 5.7以及8.0.16之前确实成立——MySQL会解析CHECK语法但不会真的去检查数据。很多老开发者因此养成了“CHECK没用就不写”的习惯。但MySQL 8.0.16开始CHECK约束才真正实现了强制执行。我用8.0.46版本实测过插入不符合CHECK规则的数据会直接报错说明这个功能已经可以放心用于生产环境。如果你还在MySQL 5.7上那你只能通过触发器或应用层校验来替代CHECK。如果你已经升级到8.0.16以上请忘掉“CHECK不生效”的旧观念它是你保证数据规范的一把好手。这对老项目的迁移也是一个隐藏风险旧库里的CHECK约束以前不生效里面可能已经存了违规数据升到8.0.16之后这些约束突然开始起作用原有的INSERT和UPDATE语句可能批量失败。升级之前一定要先把存量数据筛查一遍。3.2 列级约束和表级约束的写法CHECK约束有两种写法一种是紧跟在字段后面的列级约束适合检查单字段另一种是独立的表级约束适合检查多字段之间的关系。CREATE TABLE student_score ( id INT PRIMARY KEY, student_name VARCHAR(50) NOT NULL, score INT, -- 列级CHECK只检查score这一列 CHECK (score BETWEEN 0 AND 100), -- 表级CHECK可以跨字段检查 CONSTRAINT chk_score_valid CHECK (score 0 AND score 100) ) ENGINEInnoDB;表级约束其实可以搞定任何列级约束能做的事情但列级约束写起来更简洁阅读时也更容易对照到具体字段。我个人的习惯是简单单字段规则用列级复杂规则或者多字段关系用表级同时给表级约束起一个有意义的名字方便后续定位。3.3 5个能直接落地的CHECK场景CHECK约束最实用的场景我总结成五类在真实业务里都能直接用第一类是数值范围。比如成绩、年龄、数量、价格加一个区间限制。score BETWEEN 0 AND 100或者price 0。这类约束能很自然地防止应用层漏校验时写入极端的脏数据。第二类是枚举集合。用IN语法限制取值必须是集合内成员。比如性别字段sex IN (男, 女, 保密)或者订单状态status IN (0, 1, 2, 3)。在旧系统里状态字段经常因为代码枚举没同步而出现未知值加上CHECK之后新值入库直接报错逼着你先改约束再发代码反而能提前发现部署问题。第三类是字符串格式或长度。比如手机号必须11位可以写CHAR_LENGTH(phone) 11身份证号可以做更复杂的前缀校验。虽然正则的写法比较有限但常用的长度和前缀判断已经能覆盖很多场景。第四类是字段间的大小关系。比如合同表的结束日期必须晚于开始日期可以写成end_date start_date。这个校验在应用层写很容易漏但用CHECK一行搞定。第五类是多个字段组成的联合规则。比如折扣价必须低于原价同时折扣比例不得超过50%discount_price original_price AND discount_rate 0.5。这五类没有覆盖全部但已经能应对日常90%的简单规则。更复杂的校验比如跨表查询判断、调用存储过程CHECK就做不到了那属于触发器和应用层的范畴。3.4 CHECK约束对NULL值的一个隐藏行为第一次用CHECK的人几乎都会踩一个坑当字段值是NULL时CHECK约束是直接通过的。这是什么意思比如你给score字段写了CHECK (score 0 AND score 100)然后插入一条score为NULL的数据这条数据不会触发CHECK错误。因为SQL标准里对NULL做比较运算的结果是“未知”而CHECK只有结果为FALSE时才会拒绝UNKNOWN反而算通过。所以如果你希望“成绩必须填”必须同时给该字段加NOT NULL不能只靠CHECK。正确写法是score INT NOT NULL CHECK (score BETWEEN 0 AND 100)这一点在我们项目里出过一次真实事故。开发以为CHECK已经强制了必填和范围结果线上库里出现了大量score为NULL的记录统计平均分时把NULL直接当成0处理导致报表严重失真。后来排查才发现就是漏了NOT NULL。记住CHECK只管“值是不是合法”不管“值是不是存在”是两个维度的问题。4. 约束的查询、添加、删除与命名规范4.1 查看约束的三种方法建好的约束想确认一下是否生效、叫什么名字、覆盖哪些字段最常用的方法有三种第一种是SHOW CREATE TABLE它能完整展示表的建表语句包括所有约束定义SHOW CREATE TABLE student_score;这种方式最直观适合人工排查。缺点是输出内容比较长如果表字段很多一眼扫过去容易看花眼。第二种是查询information_schema的元数据表。比如TABLE_CONSTRAINTS表能列出所有约束KEY_COLUMN_USAGE表能查看外键和唯一约束的字段明细CHECK_CONSTRAINTS表能查看CHECK的具体条件。SELECT * FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_NAME student_score; SELECT * FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_NAME student_score; SELECT * FROM information_schema.CHECK_CONSTRAINTS;这三种方式适合写自动化脚本去判断约束是否存在。比如你在发布脚本里先检查约束是否已经添加避免重复执行报错。第三种是直接看可视化客户端。Navicat、DataGrip、MySQL Workbench的表的“设计”面板里都能直接看到外键和检查约束的配置。日常开发用图形化工具很快但排查线上问题时还是命令行的方式更靠谱。4.2 添加和删除约束的完整操作约束建完之后想调整加一个约束用ALTER TABLE删一个约束也要ALTER TABLE但具体写法不同。添加外键ALTER TABLE student_score ADD CONSTRAINT fk_student FOREIGN KEY (student_id) REFERENCES student(id);删除外键ALTER TABLE student_score DROP FOREIGN KEY fk_student;添加CHECKALTER TABLE student_score ADD CONSTRAINT chk_score_range CHECK (score BETWEEN 0 AND 100);删除CHECKALTER TABLE student_score DROP CONSTRAINT chk_score_range;删除主键ALTER TABLE student_score DROP PRIMARY KEY;删除唯一约束本质是删索引ALTER TABLE student_score DROP INDEX uk_student_no;还有一点需要注意删除被外键引用的父表列或表时必须先删外键否则会报错。很多人在清理旧表时习惯直接DROP TABLE父表结果发现删不掉系统提示有外键依赖就是这个原因。正确顺序一定是先删子表或先删外键。修改约束没有单独的“修改”语法只能先删除再添加。如果你要改一个CHECK的规则操作顺序应该是ALTER TABLE student_score DROP CONSTRAINT chk_score_range; ALTER TABLE student_score ADD CONSTRAINT chk_score_range CHECK (score BETWEEN 0 AND 100);这中间存在一个短暂的时间窗口约束是不存在的。在操作大数据量的表时这个窗口期可能会有并发写入绕过约束。稳妥的做法是在业务低峰期操作或者用事务把所有ALTER语句包起来。4.3 约束命名的约定约束命名看似小事实际上影响后面维护效率。一个设计良好的约束名能让运维人员从报错信息里直接看出问题在哪张表、哪个字段。我推荐一套比较简单实用的命名规则主键约束pk_表名唯一约束uk_表名_字段名非空约束一般不用起名数据库默认机制即可外键约束fk_子表名_父表名再加字段也可以比如fk_order_user检查约束chk_表名_字段名或chk_表名_校验规则比如chk_student_score_score_range给每个约束取一个可读的名字最大的好处是报错时能够快速定位。比如MySQL报错说CONSTRAINT chk_student_score_score_range被违反你一眼就知道是成绩表的score字段范围问题不用再去翻建表语句。相比之下那些不命名、由系统自动生成的约束名实际排查起来非常痛苦。5. 常见问题与排查技巧实录5.1 一张速查表解决大多数异常最近做项目时有一个同事问我“同样一句INSERT为什么有时候报1048有时候报1062还有时候报1452”这三个错误码分别对应非空约束、唯一约束、外键约束的违反。我干脆给他整理了一张排查表自己也一直在用错误码典型场景常见原因处理思路ERROR 1048插入或更新时某列为空字段设置了NOT NULL补全必填字段检查应用层传参ERROR 1062插入重复数据主键或唯一约束冲突查询已存在数据做幂等处理ERROR 1215创建外键失败类型、字符集、引擎、父列索引或脏数据问题逐项比对SHOW CREATE TABLEERROR 1452插入子表数据时父表无对应记录外键约束阻止孤儿数据先查父表数据确认引用正确CHECK约束未生效插入了明显违规数据却成功版本低于8.0.16或约束未正确添加检查版本号确认约束定义存在这张表虽然没有覆盖每个细节但对于日常开发和运维来说已经能解决大部分问题。遇到不认识的错误码先看错误信息里提到的约束名再返回来查这张表排查速度会快很多。5.2 外键创建失败的三个真实现场实际运维中外键创建失败是比较常见的问题而且原因往往比较隐蔽。我复盘过三个典型的现场。第一个现场是“字段类型不匹配”。子表的字段是BIGINT父表被引用字段是INT创建外键时报ERROR 1215。很多人第一反应是查看语法但其实问题出在类型完全不一致。解决办法是把其中一个表的字段类型改成一致或者先变更字段类型再加外键。第二个现场是“父表字段没有索引”。如果父表被引用的列不是主键也不是唯一索引MySQL在创建外键时会直接拒绝。解决办法是给父表字段先加唯一索引或主键再创建外键。第三个现场是“脏数据阻塞”。子表里已经有大量数据其中某些外键值在父表里根本找不到。这种情况在旧系统加约束时特别常见。数据库为了保证约束成立会拒绝在脏数据存在的情况下创建外键。解决办法是先跑一遍SQL查出不匹配的数据清理或补齐后再建约束SELECT DISTINCT sc.student_id FROM student_score sc LEFT JOIN student s ON sc.student_id s.id WHERE s.id IS NULL;把这条查询的结果处理好外键创建基本就能通过了。这一步也是任何生产库加约束前必须做的数据体检。5.3 排查约束问题时我常用的几个SQL遇到约束问题我习惯先跑几个固定的查询来定位。这里分享出来可以直接存着备用。第一个查看一个库下所有外键SELECT TABLE_NAME, CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME IS NOT NULL;第二个查看某个表的CHECK约束SELECT CONSTRAINT_NAME, CHECK_CLAUSE FROM information_schema.CHECK_CONSTRAINTS WHERE CONSTRAINT_SCHEMA your_database_name;第三个查看约束字段明细SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA your_database_name AND TABLE_NAME your_table;这几个查询在写自动化巡检脚本时特别有用。比如上线前检查所有表是否都带上了必要约束或者找某张表的外键关系是否遗漏都能用它们快速跑出来。与其一个个表去翻建表语句不如直接从元数据表里聚合查询。还有一个排查技巧创建约束报错之后紧跟着执行SHOW WARNINGS很多情况下能看到比错误码更具体的提示信息。比如有些版本的MySQL会把“Foreign key constraint is incorrectly formed”这种提示输出出来能直接帮你缩小范围。6. 实操案例学生课程成绩信息实体表设计6.1 业务需求与实体关系拆解这一节我们做一个相对完整的案例正好把前面所有约束都用上。业务需求是设计一个学生课程成绩信息系统的核心表结构包含学生、课程、成绩三个实体。先理清关系一个学生可以选修多门课程一个课程可以被多个学生选修所以学生和课程是多对多关系。多对多关系在关系型数据库里需要通过一张中间表来表达。这张中间表就是成绩表同时承载“选修”和“成绩”两个概念。成绩表里student_id指向学生表course_id指向课程表这两个字段都是外键。成绩本身有业务规则限制范围0到100。实体确认后再设计三张表的字段和约束。学生表id主键、学号唯一、姓名非空、性别用CHECK限制枚举、电话唯一可空。课程表id主键、课程编号唯一、课程名称非空、学分可以用CHECK限制为大于0。成绩表id主键、student_id外键、course_id外键、score字段用CHECK限制0到100、选课学期字段非空、还要加一个联合唯一约束防止同一学生同一课程重复出现。6.2 完整建表SQL与约束说明直接用SQL把三张表建出来我用的是MySQL 8.0.46版本所有约束都能实际生效CREATE DATABASE IF NOT EXISTS school DEFAULT CHARSET utf8mb4; USE school; -- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, sex VARCHAR(10) DEFAULT 保密, age INT, phone VARCHAR(20), UNIQUE KEY uk_student_no (student_no), UNIQUE KEY uk_phone (phone), CONSTRAINT chk_student_sex CHECK (sex IN (男, 女, 保密)), CONSTRAINT chk_student_age CHECK (age 0 AND age 120) ) ENGINEInnoDB; -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL, course_name VARCHAR(100) NOT NULL, credit INT NOT NULL, UNIQUE KEY uk_course_no (course_no), CONSTRAINT chk_course_credit CHECK (credit 0) ) ENGINEInnoDB; -- 成绩表 CREATE TABLE student_score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, semester VARCHAR(20) NOT NULL, score INT, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE, CONSTRAINT chk_score_range CHECK (score 0 AND score 100) ) ENGINEInnoDB;把这张表拆开看每一处约束都是有目的的。学生表的student_no设了唯一约束保证学号不能重复sex字段加CHECK把性别取值限制死age字段加CHECK防止多写一个零导致年龄上千。course表的course_no唯一credit大于0也是同理。成绩表是核心。student_id和course_id两个外键分别指向两张父表保证每次成绩记录都对应真实存在的学生和课程。联合唯一约束uk_student_course保证同一学生同一课程只能有一条记录防止并发重复选课。score加了CHECK直接拒绝负数和超过100的分数。我在设计时还给student_id加了NOT NULL因为成绩记录如果没有学生就没有任何意义score没有加NOT NULL因为有些场景下先选课未出分也是合理状态这里按业务需求来。6.3 插入、删除时的约束验证表建好之后可以通过两条SQL来实际感受约束的作用。先插入正常数据INSERT INTO student (student_no, name, sex, age, phone) VALUES (A001, 张三, 男, 20, 13800000001); INSERT INTO course (course_no, course_name, credit) VALUES (C001, 数据库原理, 4); INSERT INTO student_score (student_id, course_id, semester, score) VALUES (1, 1, 2024-01, 95);这几条都能正常插入。但接下来这些操作会被约束挡住-- 性别不在枚举里报CHECK错误 INSERT INTO student (student_no, name, sex) VALUES (A002, 李四, 未知); -- 重复学号报唯一约束错误 INSERT INTO student (student_no, name) VALUES (A001, 王五); -- 成绩超过100报CHECK错误 INSERT INTO student_score (student_id, course_id, semester, score) VALUES (1, 1, 2024-02, 150); -- 学生不存在报外键错误 INSERT INTO student_score (student_id, course_id, semester, score) VALUES (999, 1, 2024-02, 80);这些错误信息刚好对应第5节速查表里的错误码可以对照着感受一下。约束的所有价值就在这些“拒绝”里看似限制了灵活性实际上保护了整张表的数据可信度。6.4 如果不想用物理外键可以怎么替代最后说一种常见调整思路。有些项目的成绩表数据量极大或者已经规划了分库分表这时候物理外键可能不是最优解。这种情况下我建议至少保留下面几层保障第一保留外键字段的索引。在student_id和course_id上各建一个普通索引保证查询成绩时走索引能快速定位。第二在应用层维护事务。写入成绩时先查学生表和课程表确认存在再插入成绩表整个流程放在一个事务里。第三加定时任务做“孤儿数据扫描”。定期查询左连接后父表为NULL的记录把问题数据暴露出来SELECT sid.id, sid.student_id, sid.course_id FROM student_score sid LEFT JOIN student s ON sid.student_id s.id WHERE s.id IS NULL;说白了物理外键是“防患于未然”逻辑外键是“出事之后快速发现”。如果你选择逻辑外键就必须接受数据库自身不再强制这一点靠制度和脚本去补足。我个人这几年建表下来最深的体会是约束是用来兜底的不是用来代替业务校验的。基础约束管好单表格式外键管好表间关系CHECK管好业务规则的常量化。这套组合下来一般业务系统的数据质量都能守得住。如果你所在的项目还在用老版本MySQLCHECK的坑尤其要留意千万别把“建了约束”当成“约束生效”。先在测试环境把每一条约束的拒绝场景跑一遍再上线到生产这个习惯能帮你省下大量查脏数据的时间。