MySQL为何不推荐用SELECT *,尤其在覆盖索引和IO优化场景下?
- 内容介绍
- 文章标签
- 相关推荐
本文共计975个文字,预计阅读时间需要4分钟。
直接使用SELECT *会破坏覆盖索引,强制回表查询,这并非可能慢,而是必然慢,因为它是完全随机+I/O的过程。
为什么 SELECT * 必然让覆盖索引失效
覆盖索引生效的前提是:查询所需所有字段都完整存在于某个二级索引的叶子节点中。一旦用了 SELECT *,MySQL 就必须把整行数据捞出来——哪怕你只差一个 TEXT 或 BLOB 字段没在索引里。
而 TEXT、BLOB、超长 VARCHAR(超过 728 字节)根本不会被任何索引存储,MySQL 会静默忽略它们是否出现在联合索引定义中。
- 执行计划里
type还是ref,但Extra从Using index变成Using where,就是最直接的信号:覆盖已失效 - 哪怕你给所有“小字段”建了联合索引,只要表里存在
TEXT,这个索引就永远无法覆盖整行 - 优化器不是“放弃索引”,而是被迫走“索引扫描 + 回表”,成本评估后可能干脆选全表扫描(顺序 I/O 比大量随机 I/O 更快)
大字段带来的额外 IO 开销
InnoDB 对大于 728 字节的字段会做行溢出处理:主记录只存 20 字节指针,真实内容存到单独的溢出页。这意味着读一行,要先读聚簇索引页,再根据指针去读溢出页——至少两次随机 I/O。
-
SELECT *会把所有溢出字段都拉一遍,哪怕业务完全用不到log、content、avatar - Buffer Pool 被大量无用的大字段占满,挤走真正热的数据页
- 网络传输体积激增,尤其当应用和 DB 不在同一机房时,延迟和带宽压力明显
显式列名还能规避哪些实际风险
不只是性能问题,SELECT * 在工程落地时容易引发隐性故障:
- 表结构变更(如新增/删除字段)后,应用层按固定下标解析结果集会错位,比如原
users.id, users.name变成users.id, users.email, users.name,name就被读成email - 多表
JOIN时同名列(如id)会互相覆盖,不加别名直接报错或返回错误值 - ORM 映射失败:Hibernate、MyBatis 等依赖列名绑定属性,
SELECT *返回的字段顺序/数量不稳定,容易触发SQLException或空指针 - 敏感字段泄露:表里若有
password_hash、token,SELECT *会一并查出,后端没过滤就透出给前端
EXPLAIN 是唯一能验证覆盖是否生效的手段
别猜,直接看执行计划。重点盯两个字段:
-
type:确认走了哪个索引(比如ref或range) -
Extra:出现Using index才算真正覆盖;只要带Using where或Using filesort,就说明有回表或临时表开销 - 如果
SELECT id, name FROM users WHERE name = ?走了idx_name且Extra = Using index,那换成SELECT *后Extra一定变
真正难的不是写对 SQL,而是每次改查询前,都愿意花 10 秒跑一遍 EXPLAIN —— 很多人跳过这步,直到线上慢查报警才回头补。
本文共计975个文字,预计阅读时间需要4分钟。
直接使用SELECT *会破坏覆盖索引,强制回表查询,这并非可能慢,而是必然慢,因为它是完全随机+I/O的过程。
为什么 SELECT * 必然让覆盖索引失效
覆盖索引生效的前提是:查询所需所有字段都完整存在于某个二级索引的叶子节点中。一旦用了 SELECT *,MySQL 就必须把整行数据捞出来——哪怕你只差一个 TEXT 或 BLOB 字段没在索引里。
而 TEXT、BLOB、超长 VARCHAR(超过 728 字节)根本不会被任何索引存储,MySQL 会静默忽略它们是否出现在联合索引定义中。
- 执行计划里
type还是ref,但Extra从Using index变成Using where,就是最直接的信号:覆盖已失效 - 哪怕你给所有“小字段”建了联合索引,只要表里存在
TEXT,这个索引就永远无法覆盖整行 - 优化器不是“放弃索引”,而是被迫走“索引扫描 + 回表”,成本评估后可能干脆选全表扫描(顺序 I/O 比大量随机 I/O 更快)
大字段带来的额外 IO 开销
InnoDB 对大于 728 字节的字段会做行溢出处理:主记录只存 20 字节指针,真实内容存到单独的溢出页。这意味着读一行,要先读聚簇索引页,再根据指针去读溢出页——至少两次随机 I/O。
-
SELECT *会把所有溢出字段都拉一遍,哪怕业务完全用不到log、content、avatar - Buffer Pool 被大量无用的大字段占满,挤走真正热的数据页
- 网络传输体积激增,尤其当应用和 DB 不在同一机房时,延迟和带宽压力明显
显式列名还能规避哪些实际风险
不只是性能问题,SELECT * 在工程落地时容易引发隐性故障:
- 表结构变更(如新增/删除字段)后,应用层按固定下标解析结果集会错位,比如原
users.id, users.name变成users.id, users.email, users.name,name就被读成email - 多表
JOIN时同名列(如id)会互相覆盖,不加别名直接报错或返回错误值 - ORM 映射失败:Hibernate、MyBatis 等依赖列名绑定属性,
SELECT *返回的字段顺序/数量不稳定,容易触发SQLException或空指针 - 敏感字段泄露:表里若有
password_hash、token,SELECT *会一并查出,后端没过滤就透出给前端
EXPLAIN 是唯一能验证覆盖是否生效的手段
别猜,直接看执行计划。重点盯两个字段:
-
type:确认走了哪个索引(比如ref或range) -
Extra:出现Using index才算真正覆盖;只要带Using where或Using filesort,就说明有回表或临时表开销 - 如果
SELECT id, name FROM users WHERE name = ?走了idx_name且Extra = Using index,那换成SELECT *后Extra一定变
真正难的不是写对 SQL,而是每次改查询前,都愿意花 10 秒跑一遍 EXPLAIN —— 很多人跳过这步,直到线上慢查报警才回头补。

