我印象很深的一次经历是帮一个朋友排查线上数据问题。用户表里突然出现了三十多条手机号完全为空的记录还有几条昵称字段直接显示了null字符串前端页面瞬间变得没法看。查到最后原因倒也简单当年建表的时候压根没设置任何约束字段想空就空、想重就重。这种问题如果建表时多花两分钟去设计约束根本走不到线上。这篇文章专门聊 MySQL 表的约束而且是约束系列的上篇。上篇覆盖约束的整体概念、NOT NULL、DEFAULT、COMMENT、UNIQUE最后用一个完整的用户表实战把约束串起来。主键普通用法会简单提一下但主键设计细节、外键、CHECK约束这类内容放到下篇再展开。适合刚看完 MySQL 基础教程、准备动手建表的新手也适合被线上脏数据折磨过、想回来补课的朋友。1. 约束到底是干什么的先搞懂本质再动手在写任何CREATE TABLE语句之前我建议先停下来想一个问题约束的本质到底是什么1.1 没有约束的数据库数据能乱成什么样你可以把数据库表想象成一个快递柜。数据类型规定这个格子能放多大的箱子比如INT放不了字符串这层是规格限制。但约束管的是另一件事这个格子必须放东西吗格子上的编号能不能重复放了东西要不要登记默认信息没有约束的表现实中能出现的情况我见过太多了用户名是NULL前端一渲染就是 null用户一脸懵。同一批订单号插入了两遍统计报表金额直接翻倍。手机号字段空着营销短信群发时一堆报错。性别字段写了个 0后来业务方说 0 是未知老数据全是 0新需求又要区分进退两难。这些问题的共性是应用层没拦住数据库层又不管脏数据就这么进来了。有人可能会想我在代码里判断一下不就行了问题是业务系统往往不是一个程序在写同一个库。后台管理工具、补数据的脚本、排查问题时的临时手动 SQL、公司内部的数据同步任务任何一个入口没走校验脏数据都能溜进去。约束是数据库这一层唯一的兜底谁都绕不过。1.2 一张表讲清 MySQL 六大约束MySQL 里的约束一共六类平时最常用的就这几个。约束类型作用本篇是否覆盖NOT NULL该列不允许存NULL覆盖DEFAULT插入不提供值时给出默认值覆盖UNIQUE该列或列组合的值不允许重复覆盖PRIMARY KEY主键非空且唯一一表一个简单覆盖细节下篇FOREIGN KEY外键保证引用完整性下篇CHECK自定义取值范围检查下篇这里必须先强调一个容易混淆的点数据类型和约束是两回事。VARCHAR(32)规定的是最多 32 个字符这种容量边界但它管不了不能为空不能重复这类业务规则。约束就是把这些业务规则下沉到数据库引擎层让引擎来强制执行。还有一个现实问题约束一旦建好想改会非常痛苦。后期给一个已经存了几百万行的表加唯一约束很可能发现历史数据里早就一堆重复了加都加不上去。所以约束必须建表时就设计清楚而不是数据出问题之后再补救。2. NOT NULL 空约束别让 NULL 悄悄漏进表里2.1 MySQL 默认对 NULL 太宽容新手最常踩的一个坑是以为建表时不写NOT NULL字段就只是可以为空而已没什么大不了。但在 MySQL 里不写NOT NULL就是默认允许NULL。比如下面这个建表语句CREATE TABLE t_user_test ( username VARCHAR(32), nickname VARCHAR(50) );username和nickname两个字段都没有约束意味着插入下面这条记录是完全合法的INSERT INTO t_user_test (username, nickname) VALUES (NULL, NULL);没有报错没有任何警告数据就进去了。等到查询的时候WHERE username admin永远查不到这条NULL记录因为NULL用等号比较的结果是未知且不成立。你得专门写WHERE username IS NULL才能把它捞出来。这还只是查询层的麻烦更麻烦的是前端展示、报表统计、对外接口都会踩到NULL。所以我的习惯是业务上一定得有值的字段全部加NOT NULL。哪些属于这类字段用户名、登录账号、订单号、金额、状态、时间戳基本上只要这个字段没有值就代表数据无效就应该NOT NULL。2.2 非严格模式才是最大的坑这是很多教程不会展开、但实战里非常关键的一个点NOT NULL约束是否真能拦得住脏数据取决于 MySQL 当前运行在严格模式还是非严格模式。MySQL 的sql_mode参数里有一个叫STRICT_TRANS_TABLES的东西决定了对非法值的处理方式。在 MySQL 8.0 的默认配置下它是开启的。你可以用下面这条命令查看SELECT sql_mode;我 8.0 环境实测的结果是这样的ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION在严格模式下往NOT NULL列插入NULL会直接报错CREATE TABLE t_user_notnull ( username VARCHAR(32) NOT NULL ); INSERT INTO t_user_notnull (username) VALUES (NULL);报错信息ERROR 1048 (23000): Column username cannot be null纠错信息很明确这一行直接失败数据库不会给你插进去。但如果你或者运维把sql_mode改掉了把STRICT_TRANS_TABLES去掉情况就完全不一样了。比如在某个老项目的会话里执行SET sql_mode ; INSERT INTO t_user_notnull (username) VALUES (NULL);此时不会报错MySQL 会悄悄把这个NULL转成隐式默认值——字符串类型是空串数值类型是0然后插入成功顺手给你一个 Warning。你可以用SHOW WARNINGS;看到提示--------------------------------------------------------- | Level | Code | Message | --------------------------------------------------------- | Warning | 1048 | Column username cannot be null | ---------------------------------------------------------注意这不是约束失效而是非严格模式下 MySQL 主动帮你做了降级处理。问题在于很多补数据脚本、临时数据修复任务可能在一个非全局的会话里执行sql_mode已经被某些历史代码改成了宽松模式这时候你以为NOT NULL在保护你实际没有。提示生产环境务必保持STRICT_TRANS_TABLES开启并定期检查sql_mode是否被意外改动。就算要改也应该用ALTER ... SET sql_mode的方式显式修改而不是偷偷把整个参数清空。2.3 必须搞懂的 NULL 与空字符串区别NULL和空串经常被混在一起但它们语义上完全不同。NULL表示没有值未知未填写空串表示这是一个明确的值但内容是空的。差别在哪里举三个最实际的场景排序时NULL在升序中默认排在最前面而空串排在自己应有的字符位置。COUNT(字段)会自动忽略NULL但不会忽略空串。WHERE 字段 能匹配空串但永远匹配不了NULL。查询NULL只能用IS NULL或者IS NOT NULL。所以设计字段时要想清楚没值到底代表什么。如果没有值本身就是被允许的业务状态比如用户没填手机号那用NULL没问题如果没有值意味着数据不合格那就该NOT NULL配合DEFAULT给一个兜底值。这个思路会直接影响下一部分说的DEFAULT怎么配。3. DEFAULT 默认值与 COMMENT 字段注释给数据兜底3.1 DEFAULT 的作用远不是省事那么简单DEFAULT是约束里最不起眼、但价值被严重低估的一个。它做的事情是插入记录时如果某个字段没有提供值就用默认值顶上。CREATE TABLE t_user_default ( username VARCHAR(32) NOT NULL, gender TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );往这张表插入一条只带用户名的数据INSERT INTO t_user_default (username) VALUES (alice);然后查出来看--------------------------------------- | username | gender | create_time | --------------------------------------- | alice | 0 | 2025-01-18 14:32:10 | ---------------------------------------gender自动补成了 0create_time自动补成了插入那一刻的时间。你没有写这两个字段MySQL 按约束帮你填了这就是默认值的价值。但我更想强调的是它的另一层作用让代码层能偷懒同时保证数据完整性。业务代码插入数据时不必关心哪些字段要显式传值只要这条记录逻辑上成立数据库都会保证每个字段有合法结果。代码简单了出错的可能性也就少了。3.2 DEFAULT 和 NOT NULL 的最佳搭档关系DEFAULT和NOT NULL是经常一起出现的组合它们解决的是同一个问题字段必须有值但在用户不提供时给它一个合适的值。最经典的组合是用户昵称这类软必填字段。昵称从业务上不能是NULL但不代表用户注册时一定得填。这时候设置成nickname VARCHAR(50) NOT NULL DEFAULT 插入时不传昵称字段自动就是空串不会出现前端展示null的尴尬。同样的思路还有age TINYINT UNSIGNED NOT NULL DEFAULT 18status TINYINT NOT NULL DEFAULT 0update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP这里的ON UPDATE CURRENT_TIMESTAMP是另一个实用的小技巧每次更新记录时MySQL 自动把update_time刷成当前时间。做数据审计和排查这条数据什么时候被改的特别有用。使用这个特性时要注意只有当其他字段发生真实变化时才会触发更新如果更新的值和原值一样MySQL 8.0 不会刷新时间戳。3.3 顺手把 COMMENT 写明白能省一整天排查时间严格来说COMMENT不算约束它是字段的注释。但建表的时候我强烈建议每一个字段都要带注释这跟约束一样属于前期投入、长期回报的事。为什么这里要放一起讲因为有注释的字段在排查约束问题时能少踩很多坑。比如字段status的DEFAULT 0如果你不写注释三个月后你自己都分不清 0 是启用还是停用。加上注释就一行的事status TINYINT NOT NULL DEFAULT 0 COMMENT 账号状态0-正常1-禁用后面无论是自己维护、团队交接还是后端开发对着接口文档写代码都能少问很多问题。查询字段注释也不难SHOW FULL COLUMNS FROM t_user_default;或者从系统库直接查SELECT COLUMN_NAME, COLUMN_COMMENT, COLUMN_DEFAULT, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_NAME t_user_default;实际经验告诉我表结构文档经常没人维护但COMMENT跟着表定义走导出 SQL 时也带着基本不会丢是最不容易骗人的字段说明。3.4 8.0 还能用表达式做默认值MySQL 8.0.13 开始DEFAULT后面可以跟一个表达式用括号包起来。比如CREATE TABLE t_user_expr ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL, nickname VARCHAR(50) NOT NULL DEFAULT (CONCAT(user_, uuid_short())) COMMENT 昵称默认生成 );插入一条数据INSERT INTO t_user_expr (username) VALUES (bob);nickname会自动生成类似user_10484207345946132480这样的值。这个功能在需要无代码默认值的场景很有用但我不建议过度使用。表达式默认值会在每次插入时执行性能开销比字面量默认值大而且可读性取决于表达式的复杂度。日常建表能用字面量默认值解决的优先用字面量。4. UNIQUE 唯一键把重复数据挡在门外4.1 唯一键保证的是业务上的唯一UNIQUE约束保证某一列或某些列的组合在表里不会出现重复值。这是除主键之外最常用、也最容易踩坑的约束。哪些场景需要唯一键几乎所有的业务实体都有唯一标识需求用户账号username必须唯一。手机号绑定账号时一张表里一个手机号只能出现一次。订单表里的商户订单号merchant_order_no必须唯一否则支付回调就乱了。商品编码sku_code必须唯一。建表时声明唯一键有两种写法CREATE TABLE t_user_unique ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT 用户名, email VARCHAR(128) NOT NULL UNIQUE COMMENT 邮箱 );或者在表定义末尾单独声明CREATE TABLE t_user_unique2 ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL, email VARCHAR(128) NOT NULL, UNIQUE KEY uk_username (username), UNIQUE KEY uk_email (email) );第二种写法把约束名显式写出来uk_username、uk_email后面排查索引、删除约束的时候会方便很多。如果像第一种写法那样直接写在字段后面约束名由 MySQL 自动生成通常是字段名本身但不同的 MySQL 版本、不同的写法可能产生不同的名字维护时还得去SHOW CREATE TABLE里确认比较麻烦。4.2 唯一键和主键的区别一句话版本唯一键和主键都能保证不重复但有三点必须区分第一主键有且只有一个唯一键可以有多个。一张表只能有一个PRIMARY KEY但可以有多个UNIQUE。第二主键自动带有NOT NULL属性唯一键不自动带。这是很多人理解错的地方唯一键约束只保证值不重复但它不保证值非空后面专门讲这个坑。第三主键是物理上的聚簇索引入口InnoDB 表的主要数据都挂在主键索引下唯一键是普通的辅助索引。所以主键承担的不只是约束还有存储结构的定位。这也是为什么每张 InnoDB 表我都建议设置一个主键哪怕这个主键只是一个无业务意义的自增id也不要让它空缺。4.3 UNIQUE 与 NULL 共存的诡异行为接着上面说UNIQUE不控制NULL。这意味着什么看例子CREATE TABLE t_user_uniq_null ( id INT PRIMARY KEY AUTO_INCREMENT, phone CHAR(11) UNIQUE COMMENT 手机号 ); INSERT INTO t_user_uniq_null (phone) VALUES (NULL); INSERT INTO t_user_uniq_null (phone) VALUES (NULL);两条phone都是NULL的记录居然都能插入成功。为什么因为 MySQL 的判断逻辑是NULL NULL的结果是未知不是相等所以不认为两条NULL记录在唯一键上冲突了。这个行为在业务上有没有合理性有。有些场景下手机号未绑定的状态确实允许有多条记录手机号一旦填了就不能重复绑定。但要注意如果你想让一个手机号只能出现一次且不能为空那就得UNIQUE加NOT NULL一起上phone CHAR(11) NOT NULL UNIQUE这样设置后插入两条NULL会直接报ERROR 1048插入重复手机号会报ERROR 1062数据才能被真正约束住。注意只要字段没加NOT NULLUNIQUE约束就存在多条 NULL 不冲突的空子。设计表之前要先想好你的业务逻辑是否允许这个空子存在。4.4 复合唯一键与自定义约束名实际问题里单列唯一往往不够经常要组合唯一。比如一个购物平台的收藏表用户和商品的关系CREATE TABLE t_favorite ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT 用户ID, product_id INT NOT NULL COMMENT 商品ID, UNIQUE KEY uk_user_product (user_id, product_id) );uk_user_product联合user_id和product_id两个字段做唯一约束意思就是同一个用户对同一个商品的收藏只能有一条记录。如果你只给user_id加唯一那就变成一个人只能收藏一个商品显然不对如果两个字段各自加唯一那约束的是用户ID不能重复和商品ID不能重复逻辑完全错了。区分单列唯一和复合唯一是设计关联表的基本功。复合唯一键同样允许NULL而且多个NULL的处理规则跟单列一致只约束非NULL值的组合。假如product_id为空user_id相同的多条记录也能插进去设计时要把业务上必填的列加上NOT NULL。如果表已经建好了想补一个唯一约束用ALTER TABLEALTER TABLE t_favorite ADD UNIQUE KEY uk_user_product (user_id, product_id);反过来删除唯一约束因为约束对应的索引删除ALTER TABLE t_favorite DROP INDEX uk_user_product;这里尤其要注意删除命令用的是索引名也就是唯一约束的那个名字不是字段名。写错名字会报ERROR 1091所以动手之前先SHOW CREATE TABLE看清楚约束名。5. 实战从零设计一张能用的用户表讲了这么多理论还是得落到一张真实的表上。下面我用一个最常见的业务场景——用户表把NOT NULL、DEFAULT、COMMENT、UNIQUE四个约束串起来完整走一遍。5.1 梳理需求先把约束想清楚假设要设计一张标准用户表业务给的需求大概是这样用户要有唯一标识作为主键使用。用户名必须存在且全局唯一不能重复。昵称没有也必须能显示前端不能拿到NULL。邮箱可以做登录名必须唯一但不允许为空。手机号允许空着填了就不能重复。年龄、性别这类能不给默认值就不给默认值业务侧尽量少传。每条记录必须有创建时间和更新时间。把这些需求翻译成约束就是下面的字段规划字段类型约束与默认值说明idINT UNSIGNEDPRIMARY KEY AUTO_INCREMENT无业务意义的自增主键usernameVARCHAR(32)NOT NULL UNIQUE登录名非空且唯一nicknameVARCHAR(50)NOT NULL DEFAULT 昵称不给就是空串emailVARCHAR(128)NOT NULL UNIQUE邮箱非空且唯一phoneCHAR(11)UNIQUE DEFAULT NULL手机号允许空填了不可重复ageTINYINT UNSIGNEDNOT NULL DEFAULT 18年龄默认 18genderTINYINTNOT NULL DEFAULT 0性别0 未知 1 男 2 女create_timeDATETIMENOT NULL DEFAULT CURRENT_TIMESTAMP创建时间update_timeDATETIMENOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP更新时间5.2 完整建表语句逐行拆解CREATE TABLE user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 用户ID自增主键, username VARCHAR(32) NOT NULL COMMENT 登录用户名全局唯一, nickname VARCHAR(50) NOT NULL DEFAULT COMMENT 昵称默认空串, email VARCHAR(128) NOT NULL COMMENT 邮箱全局唯一, phone CHAR(11) DEFAULT NULL COMMENT 手机号允许为空唯一, age TINYINT UNSIGNED NOT NULL DEFAULT 18 COMMENT 年龄默认18, gender TINYINT NOT NULL DEFAULT 0 COMMENT 性别0-未知1-男2-女, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username), UNIQUE KEY uk_email (email), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;逐行解释几个容易看漏的点id用了INT UNSIGNED这是把非负约束跟数据类型绑定自增主键不需要负数UNSIGNED能扩大上限一倍。AUTO_INCREMENT是自增列必须声明为某个键这里是主键并且一张表最多只能有一个自增列。插入时不传idMySQL 自动分配。email这里要求非空且唯一。理论上也有另一种设计是邮箱允许为空填了就不能重复跟手机号一样。但实际登录场景里邮箱通常承担登录名职责空邮箱会导致账号体系不完整所以我按非空来设计。业务不同这里可以调整但一定要想清楚。主键和三个唯一键都放在表定义末尾集中声明。相比直接写在字段后面这种写法让每条约束都有明确的名字——uk_username、uk_email、uk_phone——将来维护、删除、排查都有据可查。5.3 用插入测试验证每个约束的效果表建好之后立刻插入几条数据测试各种情况确认约束按预期工作。正常插入一条完整记录INSERT INTO user (username, nickname, email, phone, age, gender) VALUES (zhangsan, 张三, zsexample.com, 13800138001, 25, 1);查询结果----------------------------------------------------------------------------------------------------------- | id | username | nickname | email | phone | age | gender | create_time | update_time | ----------------------------------------------------------------------------------------------------------- | 1 | zhangsan | 张三 | zsexample.com | 13800138001 | 25 | 1 | 2025-01-18 14:40:21 | 2025-01-18 14:40:21 | -----------------------------------------------------------------------------------------------------------再插入一条只提供必填字段的简化数据验证默认值是否生效INSERT INTO user (username, email) VALUES (lisi, lsexample.com);查询结果可以看到nickname是空串age是 18gender是 0phone是NULL两个时间字段自动补上。这些全是DEFAULT和NOT NULL配合的功劳。测试非空约束故意插入username NULLINSERT INTO user (username, email) VALUES (NULL, testexample.com);报错ERROR 1048 (23000): Column username cannot be null测试唯一约束重复插入刚才的usernameINSERT INTO user (username, email) VALUES (zhangsan, otherexample.com);报错ERROR 1062 (23000): Duplicate entry zhangsan for key user.uk_username再测试允许为空的唯一键phone两条NULL记录能不能共存INSERT INTO user (username, email) VALUES (wangwu, wwexample.com); INSERT INTO user (username, email) VALUES (zhaoliu, zlexample.com);这两条都能插入成功phone都是NULL没有触发唯一冲突。这就是前面说的UNIQUE不拦NULL的行为。最后测一个典型的非法取值场景把age改成超出TINYINT UNSIGNED范围的值INSERT INTO user (username, email, age) VALUES (caocao, ccexample.com, 300);报错ERROR 1264 (22003): Out of range value for column age at row 1数据类型也参与了约束工作虽然它不算独立的约束类型但UNSIGNED、长度限制这类特性都在数据边界上发挥着约束作用。6. 常见问题速查与避坑心得最后把平时最容易踩的坑集中整理一下很多问题都是别人问过我、我也亲手犯过的。6.1 我用得不顺的地方和解决方法第一个坑想给已有数据的表加唯一约束但历史数据已经有重复。比如用户表之前没加uk_phone现在想加结果报ERROR 1062: Duplicate entry。处理方式只能是把重复数据处理掉再添加没有捷径。可以先查重复记录SELECT phone, COUNT(*) FROM user WHERE phone IS NOT NULL GROUP BY phone HAVING COUNT(*) 1;然后决定是保留一条、删除多余还是把重复的手机号改成空值处理完再执行ALTER TABLE ADD UNIQUE KEY。这个教训让我养成了建表时就设计完整约束的习惯后面补约束的成本实在太高。第二个坑ALTER TABLE MODIFY改字段类型时把约束和注释弄丢了。比如ALTER TABLE user MODIFY COLUMN email VARCHAR(64);MySQL 的MODIFY是整列替换定义如果没有重新声明NOT NULL、COMMENT等改完之后这些约束会全部消失。正确姿势是把完整定义写全ALTER TABLE user MODIFY COLUMN email VARCHAR(64) NOT NULL COMMENT 邮箱全局唯一;改完立刻SHOW CREATE TABLE user确认一遍是最保险的流程。第三个坑DEFAULT设了什么值业务层完全不知道。比如把新用户的gender默认成 0结果 0 在旧文档里是男新代码里是未知对接接口的同事就会被 0 搞晕。解决方案就一个默认值必须是业务统一口径下的合法值而且一定要通过COMMENT写清楚。约束选型不是数据层面的事它直接关联业务语义踩一次坑就老实了。6.2 约束相关报错速查表场景报错信息原因与处理插入NULL到NOT NULL列ERROR 1048 (23000): Column xxx cannot be null严格模式下违反非空约束检查值是否漏传或上游数据为空插入重复值到唯一键ERROR 1062 (23000): Duplicate entry xxx for key uk_xxx违反唯一约束查重复数据并决定去重方案变更约束时无此键ERROR 1091 (42000): Cant DROP xxx; check that column/key exists约束名或索引名写错用SHOW CREATE TABLE核对ALTER加唯一约束因历史重复失败ERROR 1062 ... Duplicate entry xxx for key uk_xxx表里已有重复值必须清理后才能加约束数值超出字段类型范围ERROR 1264 (22003): Out of range value for column xxx数据类型边界拦截调整类型或检查数据来源字段值不符合功能约束ERROR 3813 (HY000): Column xxx violates check constraint8.0.16 的CHECK约束生效下篇细聊先知道这个码6.3 几点实操建议建表时我一般守着下面三条习惯基本没有再因为约束问题返过工第一每个字段都写COMMENT不管多简单。没有注释的表三个月后就是天书。第二所有业务必填字段一律NOT NULL可能为空的字段用DEFAULT NULL显式表达不要放任默认的NULL悄悄植入。第三所有唯一约束都显式命名uk_前缀加上业务含义规则统一方便排查也方便删改。提示每次CREATE TABLE或者ALTER TABLE执行完都养成用SHOW CREATE TABLE检查最终 DDL 的习惯。你写出来的 SQL 和 MySQL 实际解析后的结构可能有差异一切以SHOW CREATE TABLE的结果为准。我个人这些年折腾数据库的体会是约束这东西前期设计多花五分钟后期能省五个小时。很多人觉得建表就是写字段可真到线上数据一堆NULL、一堆重复记录、前端报错、报表对不上的时候才会后悔当初为什么不把每个字段的边界条件想清楚。别让脏数据有机会进入数据库才是一劳永逸的做法。下篇会继续把主键、自增策略、外键和CHECK约束讲透尤其是外键在实际业务里到底该不该用、什么时候能用那才是真正见功夫的地方。
阅读完成 · 觉得有帮助?