ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MySQL六大约束详解:从非空到外键,构建数据完整性防线

MySQL六大约束详解:从非空到外键,构建数据完整性防线 做为一个写业务代码比写报表多的后端开发者我最早对 MySQL 表的约束是有点不以为然的。直到有一回接手一张用户表十几个字段只有主键手机号、邮箱随便填业务跑了大半年库里攒出三条一模一样的手机号性别字段填出了男、女、MAIL这种鬼东西最后整个团队花了两天做清洗和补丁。那之后我建表的第一件事就是先把约束清单列出来再写 CREATE TABLE。MySQL 的表的约束本质上就是数据库层面替你把守数据入口的一套规则它把脏数据挡在门外而不是等数据进了库再花几倍的力气清洗。这篇文章我把六种约束逐个拆开讲清楚包括定义、建表写法、日常踩坑和排查手段最后用一套订单表带你实操一遍。适合刚接触数据库的同学也适合那些写了好几年 SQL 但从来没认真设计过约束的开发者。1. 为什么说约束是表设计的安全带1.1 约束的本质在数据入口设卡把数据库想成小区的大门约束就是门禁。没有门禁什么人都能进来楼道里贴满小广告车库停着外来车辆最后物业只能挨家挨户清理。数据库也一样没有约束的表现在看起来自由但业务一旦跑起来重复数据、空值、超出范围的数值、关联不上的孤儿记录全都会冒出来。我知道有人会说应用层不是有校验吗表单里不是做了必填判断吗但现实是一个系统不只一个入口。后台管理脚本、数据导入工具、临时修数据的 SQL、新同事写的批量接口任何一个环节漏了校验脏数据就进来了。数据库约束的最大价值就是把这条底线从依赖人人自觉变成数据库硬性拒绝让它成为全系统最后一道、也是最可靠的一道关卡。所以我一直强调约束不是限制你写 SQL 的枷锁而是保护数据质量的契约。约束写得好下游报表、搜索、同步、数据分析全都省心约束写得差后面每一条业务 SQL 都得带上一堆兜底条件那才是真痛苦。1.2 六大约束速览一张表看懂全景MySQL 的约束分为六种按作用可以这样理清非空约束 NOT NULL 管不能为空唯一约束 UNIQUE 管不能重复主键约束 PRIMARY KEY 管每条记录的唯一身份默认值约束 DEFAULT 管不填时给什么检查约束 CHECK 管必须满足某个条件外键约束 FOREIGN KEY 管表与表之间的引用关系。这六种约束可以组合使用比如一个字段同时加上 NOT NULL 和 DEFAULT业务里很常见。还有一个隐藏知识点不是所有约束都会自动建索引但主键、唯一约束、外键都会自动生成索引这直接影响查询性能和数据校验的效率后面章节会逐个展开。约束类型作用列级/表级是否自动建索引NOT NULL禁止 NULL 值列级否UNIQUE值不能重复可多个 NULL列级/表级是PRIMARY KEY唯一标识一行等值 NOT NULL UNIQUE列级/表级是聚簇索引DEFAULT未显式赋值时使用默认值列级否CHECK字段值必须满足表达式条件列级/表级否FOREIGN KEY引用另一张表的列保证引用完整性表级是从表侧2. 六大约束逐个拆解语法、场景与避坑要点2.1 非空约束禁止 NULL 只是第一步非空约束的语法非常简单建表时直接在字段类型后面加 NOT NULLCREATE TABLE user ( id INT NOT NULL, name VARCHAR(50) NOT NULL );加了这个约束后插入 NULL 会被拒绝报错信息是 Error 1048 Column xxx cannot be null。但这里有个特别容易翻车的地方NOT NULL 拦得住 NULL拦不住空字符串。空字符串和 NULL 在 MySQL 里是两回事NULL 表示没有值空字符串表示有一个空字符串。很多人往 NOT NULL 字段里塞空串比如导入数据时把没填写的手机号写成结果 COUNT(phone) 统计时 NULL 被忽略但空字符串会被算进去报表就对不上了。另一个容易踩的坑是 NULL 参与运算。NULL 和任何值比较都不是 TRUENULL NULL 也是 NULL如果查询里写 name NULL永远查不出数据得用 IS NULL 才行。这就是为什么很多表设计干脆把所有业务字段全部 NOT NULL DEFAULT从源头上消灭 NULL 带来的各种诡异行为。我的建议是字段含义是必须有值就加 NOT NULL比如用户名、状态、创建时间备注这种天然允许缺失的字段用 NULL 表达没有反而比空串更有语义。2.2 唯一约束别被多个 NULL 骗了唯一约束用来保证字段值不重复写法很灵活可以列级、也可以表级-- 列级 CREATE TABLE user ( email VARCHAR(100) UNIQUE ); -- 表级复合唯一 CREATE TABLE order_goods ( order_id INT NOT NULL, goods_id INT NOT NULL, UNIQUE KEY uk_order_goods (order_id, goods_id) );建完唯一约束后MySQL 会自动在对应列上创建一个唯一索引所以它既管数据唯一也能加速查询。但很多人不知道唯一约束允许多个 NULL 同时存在。原因很简单唯一索引判断的是某个具体值是否重复而 NULL 彼此之间不算重复值。这就带来一个业务陷阱如果要求手机号要么为空要么全库唯一直接加 UNIQUE 是做不到的因为多个 NULL 都会通过而两行填了同一个手机号的记录会被拦截。这个限制需要知道处理方案通常是应用层兜底或者用生成固定空值的方式替代 NULL。复合唯一约束要特别留意语义它保证的是联合起来不重复不是每个字段单独不重复。上面 order_id 和 goods_id 的组合唯一意思是同一商品在同一个订单里只能出现一次但 order_id 可以出现多次goods_id 也可以出现多次。网上查重复数据的 SQL 经常写成 GROUP BY order_id HAVING COUNT(*) 1那查的是单一字段重复和复合唯一完全不是一回事。还有一个和字符集相关的坑唯一约束去重时会受排序规则影响。如果表的排序规则是 utf8mb4_general_ciA 和 a 会被当成同一个值插入两行会触发唯一冲突如果换成 utf8mb4_bin大小写敏感就能同时存在。做邮箱注册、账号登录功能时这直接决定了用户能不能注册大小写不同的同名邮箱。选哪种规则要在建库时就定下来不要等上线了再改。2.3 主键约束聚簇索引的源头主键约束是六种约束里设计优先级最高的一种。它本质上是 NOT NULL 加 UNIQUE 的合体不允许 NULL、不允许重复、一张表最多一个主键。建表时最常见的是自增主键CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, PRIMARY KEY (id) );为什么主键这么重要因为 InnoDB 存储引擎里主键索引就是数据组织方式。InnoDB 的主键索引也叫聚簇索引索引的叶子节点直接存放整行数据也就是说数据行本身是按主键顺序物理排列的。表里没建主键时InnoDB 会找第一个非空的唯一索引代替要是连这个都没有它会自己生成一个隐藏主键谁都控制不了。所以不给表建主键就是把数据的物理存储方式交给了运气。选主键类型有三个常见方案自增整数、业务编号、UUID。自增整数最省心插入时按顺序追加页分裂少写入性能好。UUID 做主键的问题在于随机性太强插入时数据行要到处找位置会产生大量页分裂和碎片表一大性能差距非常明显。业务编号做主键比如手机号看起来方便但手机号可能被修改、可能有空值、可能超长一旦主键变更所有关联它的外键都要连锁更新牵一发动全身。我通常的建议是无业务含义的代理主键 业务唯一键组合。自增主键还有几个实战细节删掉当前最大 ID 后新插入的数据不会复用那个值MySQL 8.0 下计数器会持久化重启也不会回退想重新编号只能 TRUNCATE 表或者显式 AUTO_INCREMENT 1。高并发、分库分表场景下自增 ID 也没法跨库统一那时候就得上雪花算法这类全局 ID 方案这也是面试爱问的点。2.4 默认值约束设置默认值 0 的场景与陷阱默认值约束在业务表里出现频率极高尤其状态类字段。最常见的需求就是热搜词里那个mysql 设置默认值为 0比如订单状态、审核状态、是否删除标记建表都是这么写的CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-正常, 1-禁用, nickname VARCHAR(50) NOT NULL DEFAULT , create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) );默认值生效的时机是INSERT 时没写这一列或者显式写了 DEFAULT 关键字。很多人误以为插入 NULL 也会触发默认值这是错的如果字段允许 NULL插入 NULL 存的就是 NULL不是默认值如果字段是 NOT NULL插入 NULL 直接报错。想让不传就用默认值生效应用层的 INSERT 语句里就不要带这个字段或者写成 VALUES (..., DEFAULT)。MySQL 8.0.13 之前默认值只能写常量之后支持了表达式比如 DEFAULT (UUID())但表达式默认值有限制不能引用其他列BLOB、TEXT、JSON 这些类型也没法直接设置默认值。还有一个和严格模式绑定的坑MySQL 5.7 开始默认开启 STRICT_TRANS_TABLES如果表里某个字段既没有默认值又标了 NOT NULLINSERT 漏写这一列会直接报 Error 1364 Field doesnt have a default value如果人为把 sql_mode 改成宽松模式MySQL 会悄悄填入类型的隐式默认值整型给 0、字符串给空串这种静默补值很容易掩盖问题不建议在生产环境这样干。2.5 检查约束8.0.16 之后才真正生效检查约束用来限定字段值必须满足某个条件。它是六种约束里大家误解最多的一种因为很长一段时间里MySQL 虽然支持写 CHECK但解析完直接忽略不强制执行只有从 8.0.16 版本开始才真正生效。网上大量旧教程说MySQL 不支持 CHECK说的就是 8.0.16 之前的行为。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, age TINYINT NOT NULL, gender CHAR(1), PRIMARY KEY (id), CONSTRAINT chk_user_age CHECK (age 0 AND age 120), CONSTRAINT chk_user_gender CHECK (gender IN (M, F)) );有了 CHECK性别只能填 M 或 F年龄范围被限定在 0 到 120数据库层直接拒绝了非法数据。同类需求过去很多人用 ENUM 解决比如 gender ENUM(M,F)但 ENUM 有两个问题改枚举值必须 ALTER TABLE代价高而且 ENUM 是按索引值排序的特殊场景下排序结果不符合直觉。CHECK VARCHAR 的组合灵活得多条件表达式随便写。不过 CHECK 在 MySQL 里也有限制不能引用其他表的列不能用子查询也不能用存储函数只能对当前表的当前行做判断。要跨表校验比如订单总额等于明细之和用 CHECK 是做不到的得靠触发器或者应用层。另外注意表达式返回 NULL 时 CHECK 是判定为通过的所以如果字段本身允许 NULL那一行空值不会被 CHECK 拦截该加 NOT NULL 还是要加。2.6 外键约束引用完整性的双刃剑外键约束解决的是表与表之间的引用关系从表插入数据时关联的值必须在主表里存在主表删除或更新数据时从表按设定策略跟着处理。它把不能写入不存在的用户ID这种逻辑下沉到数据库避免产生孤儿数据。CREATE TABLE order_info ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(32) NOT NULL, PRIMARY KEY (id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user(id) );外键定义里最关键的是 ON DELETE 和 ON UPDATE 策略这决定了主表数据变动时从表怎么办。我把四种常用的行为整理成了表策略行为典型场景CASCADE主表删除/更新时从表同步删除/更新删除订单时级联删除订单明细SET NULL主表删除/更新时从表对应字段置为 NULL删除商品分类后商品分类 ID 置空RESTRICT存在引用关系时拒绝删除/更新主表记录用户还有订单记录时禁止删用户NO ACTION与 RESTRICT 行为类似延后到语句结束检查默认行为相当于 RESTRICT我实际经验是CASCADE 要慎用虽然删订单顺带删明细很方便但级联删除在数据量大的场景下可能一次锁很多行而且一旦误删主表数据从表数据全没恢复成本极高。很多团队宁可禁用外键删除逻辑全放应用层事务里控制就是这个原因。外键也不是想建就能建成功。MySQL 对它有硬性要求两张表的存储引擎都得是 InnoDB外键列和被引用列的数据类型必须一致包括 INT 和 BIGINT 这种长度差异被引用列必须有索引主键或唯一索引天然满足两列的字符集和排序规则也要一致。任何一个不满足创建时就报 Error 1215 Cannot add foreign key constraint。后面第四章我会专门列排查思路。性能上也要有预期外键会让每次 DML 都多一步引用检查在高并发插入、批量导入时开销明显而且父表删除数据时子表相关记录会被隐式加锁容易引发锁等待这和mysql 锁表的热搜词是对得上的。我的取舍建议是数据一致性要求高、写并发低的后台系统订单、财务、库存大胆用外键高并发互联网应用外键可以不用但应用层必须有一套严格的事务和校验逻辑兜底。3. 实操设计一套带完整约束的业务表3.1 需求与约束清单先想清楚再建表前面讲了每个约束单独怎么用但实际建表是多个约束组合在一起。我拿一个非常常见的订单系统来完整演示用户表、商品表、订单表、订单明细表四张表之间互相引用。先列需求清单用户名、手机号、邮箱都可以作为登录凭证要求唯一年龄只能 0 到 120用户状态默认 0正常下单时间默认当前时间商品价格必须大于 0订单号唯一订单金额不能为负数同一商品在同一订单里只允许出现一次订单必须属于存在的用户订单明细必须属于存在的订单和商品用户有订单时不允许直接删除。把需求翻译成约束一张表就清楚了表关键约束设计user主键 id用户名/手机号/邮箱唯一年龄 0-120状态默认 0goods主键 id价格 CHECK 0order_info主键 id订单号唯一user_id 外键指向 user(id)总额 CHECK 0状态默认 0order_item主键 idorder_id 外键级联删除goods_id 外键限制删除数量 CHECK 0(order_id, goods_id) 复合唯一3.2 完整建表 SQL 与逐段解读CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, mobile VARCHAR(20) NOT NULL, email VARCHAR(100) DEFAULT NULL, age TINYINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-正常, 1-禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), CONSTRAINT uk_user_username UNIQUE (username), CONSTRAINT uk_user_mobile UNIQUE (mobile), CONSTRAINT uk_user_email UNIQUE (email), CONSTRAINT chk_user_age CHECK (age 0 AND age 120) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods ( id INT NOT NULL AUTO_INCREMENT, goods_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, PRIMARY KEY (id), CONSTRAINT chk_goods_price CHECK (price 0) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_info ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待支付, 1-已支付, 2-已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), CONSTRAINT uk_order_no UNIQUE (order_no), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user(id), CONSTRAINT chk_order_amount CHECK (total_amount 0) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, goods_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, PRIMARY KEY (id), CONSTRAINT uk_order_goods UNIQUE (order_id, goods_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES order_info(id) ON DELETE CASCADE, CONSTRAINT fk_item_goods FOREIGN KEY (goods_id) REFERENCES goods(id) ON DELETE RESTRICT, CONSTRAINT chk_item_quantity CHECK (quantity 0) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 里值得展开讲几个设计决策。第一订单表和商品表的主键都用无业务含义的 id订单号 order_no 虽然唯一但只用唯一约束而不是主键因为订单号是字符串做主键既占空间又影响聚簇索引性能而 id 自增主键插入有序。第二价格字段全部用 DECIMAL 定点数绝不用 FLOAT 或 DOUBLE浮点类型算金额会出现 0.10.2 不等于 0.3 的精度问题。第三订单明细的外键策略分两种order_id 用 CASCADE是为了删订单时明细自动清理goods_id 用 RESTRICT是为了防止商品被删后历史订单明细失去参照。第四下单时明细表也保存了 price 快照商品价格后期改价不影响历史订单的金额计算。这里注意一个细节order_id 和 goods_id 的复合唯一约束 uk_order_goods它的作用就是保证同一个订单里同一个商品只能出现一次。如果没有这个约束业务层一个不小心同一商品就能被插入两行数量还可以叠加订单明细就乱了。这是很多订单系统都踩过的坑。3.3 修改表结构时的约束管理与锁表问题建表之后业务需求变了约束经常需要调整。增删约束的 ALTER TABLE 语法要记熟-- 添加唯一约束 ALTER TABLE user ADD CONSTRAINT uk_user_email UNIQUE (email); -- 添加/删除检查约束 ALTER TABLE user ADD CONSTRAINT chk_user_age CHECK (age 0 AND age 120); ALTER TABLE user DROP CHECK chk_user_age; -- 添加外键 ALTER TABLE order_info ADD CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES user(id); -- 删除外键 ALTER TABLE order_info DROP FOREIGN KEY fk_order_user; -- 设置默认值 ALTER TABLE user ALTER COLUMN status SET DEFAULT 0;具体场景里有两个高频问题。一个是加唯一约束前一定要先查重复数据。直接在有重复数据的列上建唯一索引MySQL 会立刻报 Error 1062 Duplicate entry。正确流程是先跑一遍分组统计SELECT mobile, COUNT(*) FROM user GROUP BY mobile HAVING COUNT(*) 1;有结果就先把重复数据处理掉再回头加约束。数据量大时这个 GROUP BY 也要注意性能最好加个临时索引辅助。另一个是线上大表的约束变更。别以为加约束是瞬间完成的事加唯一索引要扫描全表校验重复加外键也要逐行检查引用关系这对几千万行的表来说就是一场灾难还会在线程里占用锁资源业务查询可能被阻塞。我见过有人白天直接给两千万行的订单表加外键结果数据库负载直接飙升前端超时一片。稳妥做法是评估表的大小和当前负载选择低峰期执行如果表真的很大考虑用 pt-osc 或 gh-ost 这类在线改表工具通过影子表方式平滑完成。加约束之前先看一眼 SHOW CREATE TABLE把现有结构完整过一遍也能避免漏掉字符集或引擎细节导致失败。4. 约束问题排查实录与速查表4.1 高频报错与解决对照表约束相关的报错来来去去就那么几个把错误码认全了排查思路基本就定了。我把实际工作中最常遇到的报错整理成了对照表方便你遇到问题时直接查错误码典型报错内容触发场景处理建议1048Column xxx cannot be null违反 NOT NULL补全字段值或调整表结构允许 NULL1062Duplicate entry xxx for key uk_xxx违反唯一约束/主键去重后重试或改用 INSERT IGNORE / ON DUPLICATE KEY UPDATE1364Field xxx doesnt have a default value严格模式下非空字段缺省INSERT 补上字段、设默认值或调整 sql_mode不推荐1452Cannot add or update a child row外键引用的主表值不存在先保证主表有对应记录再插从表数据1215Cannot add foreign key constraint创建外键失败排查类型、字符集、引擎、索引四个方面3819Check constraint is violated违反 CHECK 约束按业务规则修正字段值1830Cannot change column xxx used in a foreign key constraint修改被外键引用的列先删外键改完再重建其实还有一个处理唯一冲突的实用技巧INSERT ... ON DUPLICATE KEY UPDATE。它的原理就是利用唯一索引或主键冲突触发更新而不是报错比如订单表里要幂等插入写成 INSERT INTO order_info (...) VALUES (...) ON DUPLICATE KEY UPDATE status VALUES(status)这个写法在业务里很常用可以少写一堆先查再插的代码。4.2 用 information_schema 查看约束真面目排查约束问题时光看 SHOW CREATE TABLE 不够有时候外键报错或者不知道约束名我习惯直接查 information_schema。这是 MySQL 自带的元数据数据库表结构信息全在里面。查看某张表有哪些约束SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA your_db AND TABLE_NAME order_info;查看外键具体关联哪列SELECT COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA your_db AND TABLE_NAME order_info AND REFERENCED_TABLE_NAME IS NOT NULL;查看外键的级联策略SELECT CONSTRAINT_NAME, DELETE_RULE, UPDATE_RULE FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE CONSTRAINT_SCHEMA your_db;这个技巧在排查外键到底存不存在策略到底是什么时特别有用。有一次同事说外键删不掉翻 SHOW CREATE TABLE 看不出来一查 KEY_COLUMN_USAGE发现是之前建了个同名但关联不同列的外键命名冲突导致 ALTER TABLE DROP FOREIGN KEY 总是报错。元数据一查问题当场就清楚了。4.3 外键创建失败的典型原因外键创建失败 Error 1215 是最容易让人懵的。我自己总结了一套排查顺序按这个来基本都能找到原因。第一看存储引擎。两张表都得是 InnoDB如果有一张是 MyISAM外键必然建不出来。老库从 MyISAM 迁移过来时经常踩这个。第二看字段类型外键列和被引用列必须完全一致包括类型和长度。INT 对 BIGINT、VARCHAR(32) 对 VARCHAR(64)、DECIMAL(10,2) 对 DECIMAL(10,4)全都不行。还有一个小坑字段是否 UNSIGNED 也必须一致INT 和 INT UNSIGNED 也会报错。第三看字符集和排序规则两张表或两个字段的字符集不一致外键也会失败统一设置 utf8mb4 可以从源头上避免。第四看被引用列有没有索引主键和唯一索引天然没问题如果被引用列只是普通列先给它加个索引再建外键。第五看外键名字MySQL 中外键名在同一个库里必须唯一重名了也会报错所以建外键时我建议显式命名别让 MySQL 自动起名。这四个方向排查完1215 基本都能解决。如果查完还建不上就用第四章第二节的 SQL 查一下 KEY_COLUMN_USAGE对照元数据看列是否真的匹配。5. 约束设计的经验沉淀与面试考点5.1 约束不是越多越好分层校验才是正道看到这里你可能会想那我以后每张表所有约束全加上是不是就高枕无忧了不是约束太多同样有问题。NOT NULL 和 DEFAULT 加得过多批量导入数据时处处碰壁迁移数据还得先清一遍脏数据外键加得太多删除和更新业务被各种 RESTRICT 卡住运维同学半夜处理数据要哭CHECK 条件写太死业务规则一改就要 ALTER TABLE 改约束上线节奏也被拖慢。我的经验是分三层看数据库约束负责底线问题——不能为空、不能重复、必须存在、范围合理应用层校验负责业务体验——给出手机号已注册这样友好的提示前端校验负责交互反馈——还没提交就知道邮箱格式错了。数据库约束不是用来替代另外两层的它只负责兜底把那些绕过应用直接操作数据库的脏数据挡住。核心业务表、数据一致性要求高的表约束可以设计得充分一些非核心、临时性、日志类表保持最小约束即可。5.2 约束命名规范与后期维护约束名看起来只是小事但等表多起来你一定会回来感谢自己当时取了规范的名字。我习惯的命名风格是主键PRIMARYMySQL 默认就叫这个不用改名唯一约束uk_表名_字段名比如 uk_user_mobile外键fk_表名_字段名比如 fk_order_user检查约束chk_表名_字段名比如 chk_user_age普通索引idx_表名_字段名这样做的直接好处是报错信息一眼可读。比如 Duplicate entry xxx for key uk_user_mobile立刻就知道是用户手机号重复如果当时随便起了个 mobile_2排查的时候还得先去表结构里翻半天。团队协作时约束命名规范要写进数据库设计文档让每一个新人都能按统一风格建表不然等某个人顺手写了个约束名后面其他人想改都找不到位置。5.3 高频面试题主键、唯一、外键那些事MySQL 约束是面试高频区问法很多变但核心其实就那几个。我作为面试官常问的第一题是主键和唯一约束有什么区别。答案要点一张表最多一个主键但可以有多个唯一约束主键列不允许 NULL唯一约束允许多个 NULLInnoDB 下主键是聚簇索引唯一约束建出的是二级索引。第二题是外键到底该不该用。最佳回答不是简单说该或不该而是分场景金融、订单、库存这类强一致系统用外键省心高并发、海量数据、分库分表环境下外键的检查和锁开销不可接受改为应用层事务保证。第三题是CHECK 约束在 MySQL 里生效吗。这个问题就是要踩版本坑8.0.16 之前只解析不执行8.0.16 之后真正强制校验回答时提一下版本面试官基本就会放过你。还有一个容易被问到的点唯一约束和 NULL 的关系你知道吗多个 NULL 是互相不冲突的这点和唯一的字面语义有出入面试官很喜欢用这个来测理解深度。另外复合唯一约束的含义也要能讲清楚是组合不重复不是单列不重复。5.4 与事务、存储过程的结合技巧热搜词里有 mysql 事务处理、mysql 存储过程网上搜表约束相关内容时也会看到几个词爬上来这里就说一个实际场景唯一的冲突在事务里怎么处理。InnoDB 事务中如果某条语句违反唯一约束MySQL 会报错并把这条语句回滚但整个事务并不会自动终止。事务里还可以继续执行后面的语句最终到底提交还是回滚由应用层决定。这个行为很容易被忽略很多人以为约束报错后整个事务就废了实际不是。更好用的方案是用存储过程或应用代码捕获错误码比如在存储过程里用 DECLARE CONTINUE HANDLER FOR SQLSTATE 23000 捕获唯一键冲突SQLSTATE 23000 对应完整性约束冲突然后执行幂等更新逻辑。这套组合拳让插入重复数据时不报错、而是自动更新已有行变得非常干净避免了先 SELECT 再 INSERT 的竞态问题。我个人在实际项目里的习惯是把事务里的约束冲突处理做成统一模板所有写入接口都走同一套错误码映射。数据库层面约束负责拦截脏数据事务层负责消化冲突应用层负责给用户友好提示三层配合下来数据质量问题真的能少掉一大半。如果你正在设计新表我的最后一个建议是先写约束清单再写 CREATE TABLE最后用 SHOW CREATE TABLE 核一遍。这个习惯我维持了好几年几乎没再因为数据质量问题返过工。表结构是系统的地基约束就是地基里的钢筋前期多花半小时把约束想清楚后面省下的是无数个数据清洗的加班夜。
返回列表