前言
“别用 SELECT *”——这条几乎是每个程序员入行时都听过的军规。
但如果追问一句"为什么",很多人只能答出"会多查没用的字段"。这个回答对,但太浅。它甚至被《阿里巴巴 Java 开发手册》列为**【强制】条款——能上大厂强制规范的规则,背后一定有比"多传几个字段"更硬的理由。SELECT * 真正的代价,藏在 InnoDB 的回表**机制里。
这篇文章把这条常识讲透:SELECT * 到底多做了哪些事,为什么它能让一个本可以走覆盖索引的查询,退化成回表查询。
环境说明:本文基于 MySQL 8.0,存储引擎 InnoDB。继续复用前几篇的
orders表(100 万行)。
一、先看一个现象:同样的 WHERE,性能差一截
还是那张订单表,关键是这个联合索引:
KEY `idx_user_status` (`user_id`, `status`)
先查两列(都在索引里):
EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 88888;
+----+--------+------+-----------------+-----------------+------+-------------+
| id | table | type | key | key_len | rows | Extra |
+----+--------+------+-----------------+-----------------+------+-------------+
| 1 | orders | ref | idx_user_status | 8 | 10 | Using index |
+----+--------+------+-----------------+-----------------+------+-------------+
再换成 SELECT *:
EXPLAIN SELECT * FROM orders WHERE user_id = 88888;
+----+--------+------+-----------------+-----------------+------+-------+
| id | table | type | key | key_len | rows | Extra |
+----+--------+------+-----------------+-----------------+------+-------+
| 1 | orders | ref | idx_user_status | 8 | 10 | NULL |
+----+--------+------+-----------------+-----------------+------+-------+
WHERE 条件一模一样,走的也是同一个索引,唯一的区别是 Extra:前者是 Using index,后者变成了 NULL。
这一个字段的差别,就是 SELECT * 代价的全部来源。要理解它,得先搞清楚 InnoDB 的数据到底是怎么存的。
二、底层:InnoDB 的两种索引长什么样
InnoDB 里,数据和索引是存在 B+ 树里的,而且分两种树。
聚簇索引(主键索引):叶子节点存的是整行完整数据。一张表的所有字段,都挂在主键这棵树的叶子上。
二级索引(普通索引):叶子节点只存索引字段 + 主键值,不存其他列。
拿我们的表举例,idx_user_status(user_id, status) 这棵二级索引的叶子节点,只有三样东西:user_id、status、还有主键 id。order_no、amount、created_at 这些列,它一概没有。
这个设计是理解一切的钥匙:二级索引里没有的列,只能去聚簇索引里取。

三、核心:什么是回表
现在回头看第一节的两个查询。
SELECT user_id, status 时,要的两列刚好都在 idx_user_status 索引里。MySQL 扫描这棵二级索引,直接就能拿到全部要的数据,不用再去别处找。这叫覆盖索引——索引"覆盖"了查询需要的所有列,Extra 显示 Using index。
SELECT * 时,要的是全部 6 列。二级索引里只有 user_id、status、id,剩下的 order_no、amount、created_at 不在里面。于是 MySQL 只能:
- 先在二级索引里找到匹配的行,拿到它们的主键
id - 再拿着
id回到聚簇索引,把完整的行查出来
这个"拿主键回聚簇索引再查一次"的过程,就叫回表(back to table)。
回表的代价是:命中多少行,就要回表多少次。上面的例子命中 10 行,就要额外做 10 次聚簇索引查找。如果命中的是 10 万行,那就是 10 万次回表——每一次都是一趟 B+ 树的查找。
这才是 SELECT * 真正的成本:它几乎必然导致回表,而回表次数随命中行数线性增长。
四、SELECT * 的其他代价
回表是最核心的,但不是唯一的。SELECT * 还有几笔账:
1. 无法利用覆盖索引。 这是上面讲的重点。哪怕你精心建了覆盖索引,一个 SELECT * 就让它白费——因为 * 里总有索引覆盖不到的列。
2. 传输和内存开销。 把用不到的列(尤其是大的 TEXT、BLOB 字段)从磁盘读出来、装进内存、通过网络传给应用,全程都在为你根本不看的数据买单。
3. 加重 buffer pool 压力。 读出来的整行数据会占用 InnoDB 的缓冲池,挤占了本可以缓存热点数据的空间,间接降低缓存命中率。
4. 破坏代码稳定性。 表加了一列,SELECT * 的返回结果就悄悄变了,依赖列顺序的代码、ORM 映射都可能出问题;线上还容易查出预期外的大字段。
五、那应该怎么写
原则很简单:用到哪些列,就写哪些列。
-- 不要
SELECT * FROM orders WHERE user_id = 88888;
-- 要
SELECT id, order_no, amount, created_at FROM orders WHERE user_id = 88888;
明确写出字段,好处是连锁的:
- 需要的列少时,有机会命中覆盖索引,直接免掉回表
- 不传输、不加载用不到的列,省 IO、省内存、省带宽
- 表结构变化时,查询结果稳定可控
如果查询的列刚好能被某个索引覆盖,还可以主动设计覆盖索引。比如经常 SELECT user_id, status, created_at WHERE user_id = ?,就可以把索引建成 (user_id, status, created_at),让这类查询全程走索引不回表。
六、大厂为什么把它写进开发规范
这条军规不是民间口口相传,而是被白纸黑字写进了大厂规范。《阿里巴巴 Java 开发手册》在 MySQL 数据库部分就有一条**【强制】**级的规定:
【强制】在表查询中,一律不要使用
*作为查询的字段列表,需要哪些字段必须明确写明。
说明:
1)增加查询分析器解析成本。
2)增减字段容易与resultMap配置不一致。
3)无用字段增加网络消耗,尤其是 text 类型的字段。
注意它标的是**【强制】**——在阿里的规约里,【强制】意味着违反就是 bug,代码评审直接打回,不是"建议你最好别这么写"。这三条说明和我们前面分析的完全对得上:解析成本、代码稳定性(resultMap 不一致)、网络与大字段消耗。
同一份手册里还有一条**【推荐】**级的规定,正是本文的核心:
【推荐】利用覆盖索引来进行查询操作,避免回表。
说明:如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?浏览一下目录就好,这个目录就起到了覆盖索引的作用。
这个"翻目录还是翻正文"的比喻很精妙:二级索引就是目录,聚簇索引就是正文。 能在目录里查到的信息(覆盖索引),就没必要翻到正文那一页(回表)。而 SELECT * 相当于每次都要翻到正文——因为目录里不可能印着整页内容。
为什么大厂对这种"小事"这么较真? 因为在大厂的数据规模下,小事会被放大成事故:
- 单表动辄千万、上亿行,一次
SELECT *的批量查询多出来的回表和网络传输,是实打实的资源浪费 - 一个服务的慢查询会拖垮整个数据库实例,影响的是所有依赖它的业务
- 几百上千号人协作,只有把"用哪些列写哪些列"变成强制规范,才能避免某个人的随手
SELECT *拖垮全局
规范的本质,是把少数专家踩过的坑,变成所有人都能绕开的红线。理解了回表原理,你就明白这条红线为什么值得画。
七、常见误区与面试高频问答
Q:SELECT * 和 SELECT 具体字段,扫描的行数一样吗?
一样。WHERE 条件相同,命中的行数就相同(EXPLAIN 的 rows 一致)。区别在每命中一行要不要回表——SELECT * 要,覆盖索引不要。差距在这里,不在扫描行数。
Q:那查所有列时,SELECT * 和列全写出来性能一样吗?
如果确实要用到全部列,两者性能基本一样(都要回表取完整行)。SELECT * 的问题主要出在"其实用不了那么多列,却顺手写了 *"的场景。但即便要全部列,显式写出来在代码稳定性上仍然更好。
Q:COUNT(*) 里的 * 也是这个问题吗?
不是。COUNT(*) 里的 * 是"统计行数"的固定语法,不读取任何列的值,和 SELECT * 完全是两回事,可以放心用(详见上一篇《COUNT 到底哪个快》)。
Q:怎么判断一个查询走没走覆盖索引?
看 EXPLAIN 的 Extra:出现 Using index 就是覆盖索引、没回表;如果是 NULL 或 Using where,通常就发生了回表。
总结
"别用 SELECT *"不是一句口号,它背后是 InnoDB 的存储结构:
- 二级索引只存索引列 + 主键,其他列得去聚簇索引取——这一步叫回表。
SELECT *几乎必然回表,且回表次数随命中行数线性增长;只查索引覆盖的列,则可以走覆盖索引,一次都不用回表。- 回表之外,
SELECT *还有传输、内存、缓存、代码稳定性等多笔隐性成本。
一句话记忆: SELECT * 最大的代价不是"多几个字段",而是让你永远用不上覆盖索引、次次回表。用到哪些列就写哪些列,能省下的可能是成千上万次 B+ 树查找。

545

被折叠的 条评论
为什么被折叠?



