大家好,我是晚安code。
假设一个场景一条慢查询,单表两千万行,WHERE status = ? 一条简单过滤跑了 8 秒。EXPLAIN 一看 type 是 ALL,全表扫描。原因说出来你可能不信:status 字段是 varchar,代码里传了个 int,MySQL 一隐式转换,索引直接作废。这种坑,MySQL 开发规范里几乎每条都在提醒你。

这种坑,阿里那本《Java 开发手册(黄山版)》的 MySQL 规约里其实都写了。我把建表、索引、SQL、ORM 四块最要命的强制项整理成这份清单(2026 年 8 月整理,以 MySQL 8.0 为基线)。MySQL 开发规范里最值钱的一条经验是:表一旦上线,字段名和类型基本就改不动了,所以每一行 DDL 都值得你认真写。
先点个收藏,咱们一条条过。
一、建表规约:命名的坑,从第一行 DDL 就开始了
MySQL 建表规范里,命名是最容易被忽略、又最难改的部分。 字段名一旦上线被业务引用,改起来牵一发动全身,还没法预发布,所以规范第一条就是"想清楚再建"。
几个强制项先记下来:
1)布尔字段一律用 is_xxx 命名,类型是 unsigned tinyint,1 表示是、0 表示否。别用 status 猜来猜去,is_deleted 一眼就知道是逻辑删除标记。
2)表名、字段名全小写,禁止大写、禁止数字开头、禁止两个下划线夹数字。MySQL 在 Windows 下不区分大小写,Linux 下区分——一旦部署到 Linux,AliyunAdmin 和 aliyun_admin 就是两张表。我同事当年就是在这里摔过一跤。

3)表名单数形式,user 而不是 users,跟 DO 类名保持一致。
4)禁用保留字,desc、range、match 这些都不能当列名。
5)索引命名统一:主键 pk_字段名,唯一索引 uk_字段名,普通索引 idx_字段名。看名字就知道索引类型,排查问题时省一半力气。

接着是类型。小数一律用 decimal,禁止 float 和 double——float/double 是二进制近似存储,对账经常对出几厘钱的误差。金额、汇率这种一存 float,线上对不平只是时间问题。
字符串类型看长度:长度几乎相等用 char 定长,长度不确定用 varchar,但 varchar 别超过 5000。超长文本(如商品详情)拆出来单独一张表用主键关联,别硬塞在主表里拖垮索引率。
每个表必备三字段:id(bigint unsigned 主键自增)、create_time、update_time(datetime 类型,要记时区就用 timestamp)。没有 update_time 的表,出了数据问题你连"什么时候改的"都查不到。
然后是争议最大的一条——逻辑删除。
逻辑删除:不物理删除记录,而是用标记字段(如 is_deleted=1)表示已删除,相当于给数据加了"回收站"。好处是数据可追溯,坏处是原本唯一的键可能不唯一,需要根据业务场景另行处理。
这里常有人纠结"删了就是删了,留个标记干嘛"。我把两条路的取舍画成一张表:
| 维度 | 物理删除 | 逻辑删除 |
|---|---|---|
| 数据可追溯 | 不可恢复 | 可追溯操作记录 |
| 唯一性约束 | 无冲突 | 需处理唯一键复用 |
| 存储占用 | 省空间 | 多占一行标记 |
| 查询复杂度 | 简单 | 每处 WHERE 都带 is_deleted |
| 适用场景 | 临时表、可重建数据 | 核心业务数据、需审计 |
我的态度很明确:核心业务表一律逻辑删除,临时表随便物理删。别省那点空间,丢了用户订单数据你哭都来不及。

最后一条建表规矩:单表超过 500 万行或 2GB,才推荐分库分表。如果预计三年后数据量根本到不了这个级别,千万别在建表时提前分——分布式事务、跨库查询、主键方案这些复杂度,不是你现在背得动的。
可能有人会问:表才 100 万行,要不要现在就分库分表?
不用。规范写的是"超过 500 万行或 2GB 才推荐",按你三年后的增长预估来算。提前分库分表,等于用未来的复杂度惩罚现在的自己,多数项目根本撑不到那个量级。
二、索引规约:索引不是越多越好,也不是越少越好
MySQL 索引优化最常见的两个误区:一是索引宁滥勿缺,二是吝啬到该建的索引不建。 我的经验是:索引的关键不是数量,而是它能不能被查询真正用到——建了用不上的索引,纯属浪费空间还拖慢写入。
先记死一条硬规:业务上具备唯一性的字段,哪怕是组合字段,也必须建唯一索引。 别觉得应用层校验够了就省,墨菲定律保证:只要没有唯一索引,并发场景下脏数据一定会来。至于"唯一索引影响插入速度",那点损耗可以忽略不计。
几条索引铁律一起给:
1)超过三个表禁止 join;join 的字段类型必须绝对一致,且关联字段必须有索引。
2)varchar 字段建索引必须指定长度,没必要对全字段建。一般长度 20 的索引,区分度能到 90% 以上,可以用 count(distinct left(列名, 长度)) / count(*) 算区分度。
3)页面搜索严禁左模糊或全模糊(%关键字、%关键字%)。索引文件是 B-Tree,最左前缀匹配,左边不确定就用不上索引——这种查询直接走搜索引擎。
4)建组合索引,区分度最高的放最左边;但如果同时有等号和范围条件,等号条件的列要前置。比如 WHERE c > ? AND d = ?,即使 c 区分度更高,也要建 idx_d_c 而不是 idx_c_d。
接下来是我摔得最惨的坑——隐式转换。
隐式转换:SQL 里字段类型和传入值类型不一致时,MySQL 自动把类型转换后再比较。这个转换过程会导致索引失效,让查询从索引查找退化成全表扫描。
开篇那条 8 秒慢查询就是这么来的:status 是 varchar,代码里传 int,隐式转换一发,索引就废了。EXPLAIN 的 type 直接变成 ALL,全表扫描。

排查慢查询先看 EXPLAIN,type 列从好到差有明确的层级,我画成了这张图——type 至少要 range,ref 更好,const 最理想,看到 ALL 基本就是索引没用上:

如果看到 ALL,优先怀疑是不是字段类型不一致、是不是没建索引。
然后说覆盖索引——这是 MySQL 索引优化里性价比最高的一招。
覆盖索引(Covering Index):查询所需字段全部包含在索引里的情况,直接读索引就够,不用再回主表取整行。相当于查书的目录就能回答"第 11 章是什么标题",没必要翻到那页。
判断方法:EXPLAIN 的 Extra 列出现 Using index,就是覆盖索引生效了。看这张对比图,同样是命中索引,走不走覆盖索引差一次回表:

如果查询的字段在索引里查不全,会先通过索引定位主键,再回主表取整行——这就是回表。回表多一次 IO,数据量大时性能差距非常明显。所以写 SELECT 别上来就 *,只取需要的列,往往就能让查询变成覆盖索引。
三、SQL 语句:count、sum、NULL 三座山
SQL 规范里最容易翻车的不是复杂语法,是 count、sum、NULL 这三个看着简单的东西。 每一条都对应过我线上见过的真实事故。
第一座山是 count。统计行数必须用 count(*),不要用 count(列名) 或 count(常量)。 count(*) 统计所有行,包括 NULL;count(列名) 会跳过该列为 NULL 的行,语义就不对了。count(distinct 列) 则是去重统计非 NULL 值。
第二座山是 sum 的 NPE。当某一列全是 NULL 时,sum(col) 返回 NULL 而不是 0。 拿这个结果直接做运算,Java 侧空指针报错是轻的,业务数据算错才是要命的。规范写法是加一层兜底:
-- 反例:列全为 NULL 时,SUM 返回 NULL
SELECT SUM(amount) FROM orders WHERE status = 'PAID';
-- 正例:用 IFNULL 兜底
SELECT IFNULL(SUM(amount), 0) FROM orders WHERE status = 'PAID';
第三座山是 NULL 判断。NULL 与任何值直接比较结果都是 NULL,NULL = NULL 不返回 true,NULL <> 1 也不返回 false——它们都返回 NULL,而 NULL 在 WHERE 里会被当成假。判断空值请用 ISNULL() 或 IS NULL,别用 =。
此外两个"禁用"要记牢:外键与级联必须禁用,一切外键概念在应用层解决——外键影响插入速度,级联更新是强阻塞,分布式高并发场景更是更新风暴的温床;存储过程必须禁用,难以调试、难以扩展、没有移植性。
可能有人会问:禁用外键,那数据一致性谁来保证?
应用层事务来保证。
student表删了学生,删除score表关联记录的逻辑放在同一个事务里,由代码控制顺序。单机低并发时外键省事,但上了分布式,外键的强阻塞就是性能杀手,所以规范宁可让应用层多写两行。
还有一条运维铁律:数据订正(尤其删改)之前,必须先 SELECT 确认,再执行更新。 我见过不止一次"UPDATE 忘了带 WHERE,一张表数据全没了"的事故,先查一遍能救你一命。
四、ORM 映射:MyBatis 里藏着的隐性炸弹
ORM 层最大的隐患不是性能,是 SQL 注入和字段乱映射。 前两章把 SQL 写对,这一章把映射写对,才算真正闭环。
几条强制项:
1)一律不用 * 查字段,需要哪些列就明确写哪些。* 增加解析成本、增删字段容易和 resultMap 对不上、text 大字段白白增加网络消耗——三宗罪,一个都别碰。
2)参数一律用 #{},禁用 ${}。#{} 走预编译防注入,${} 是字符串拼接,用户输入直接拼进 SQL,等于给攻击者递刀。
3)更新记录必须同步更新 update_time 字段,否则数据变更时间和记录对不上,排障无从下手。
4)不要写大而全的更新接口。传个 POJO 进来,不管有没有变化都 update set 所有字段,三个问题:容易误改、效率低、白白增加 binlog 存储。只更新真正变化的字段。
5)多表查询的列,必须加表别名限定(t1.name)。我同事就栽过:某表新增了一个 name 字段,预发布变更一跑,线上直接抛 Column 'name' in field list is ambiguous,列名歧义,排查了半天。

把四块串起来看,其实就一句话:MySQL 开发规范里几乎没有一条是锦上添花,每一条背后都对应过一次线上事故。 命名乱起、类型乱用、索引乱建、NULL 裸奔——这些"小问题"在数据量上来之后,都会变成你凌晨三点爬起来救火的理由。
建表(MySQL 建表规范)、索引(MySQL 索引优化)、SQL、ORM 四块都能在这篇里找到对应的坑。后续我打算再写一篇 EXPLAIN 实战解读,把慢查询定位讲透,感兴趣的可以在评论区告诉我。
我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你在 MySQL 开发规范上踩过最疼的一个坑是什么?