SELECT * 让覆盖索引失效,因其必然引入索引未包含的字段,导致必须回表;即使WHERE命中索引,Extra也不再显示Using index,而触发随机IO。

SELECT * 为什么让覆盖索引彻底失效
覆盖索引(Covering Index)是指查询所需的所有字段都包含在某个索引中,MySQL 可直接从索引页读取全部数据,无需回表访问聚簇索引。一旦写 SELECT *,哪怕 WHERE 条件能走索引,只要表里有任意一个字段不在该索引中,优化器就无法使用覆盖索引。
常见错误现象:EXPLAIN 显示 type=ref 但 Extra 列没有 Using index,反而出现 Using where; Using index condition 或干脆空着——说明正在回表。
- 联合索引
(status, user_id, username)下,SELECT user_id, username FROM users WHERE status = 1是覆盖查询;SELECT *就不是 - 哪怕只多一个
created_at字段没包含在索引里,整个索引对SELECT *就失去覆盖价值 - ORM 自动生成的
SELECT *(如 Django 的User.objects.all())会悄无声息地绕过你精心设计的覆盖索引
回表引发的 IO 次数翻倍甚至三倍
InnoDB 的辅助索引只存索引列 + 主键值,查 SELECT * 时,必须先走辅助索引定位主键,再根据主键去聚簇索引查整行。这就是“回表”。而大字段会让事情更糟。
关键细节:
- TEXT、BLOB、超过 728 字节的
VARCHAR会被 InnoDB 存到溢出页(off-page),回表时需额外一次磁盘 IO 读取溢出页 - buffer pool 命中率低于 80% 时,每次回表大概率触发真实磁盘读,随机 IO 成为瓶颈
-
LIMIT 10000, 20配合SELECT *:MySQL 仍要构造前 10000 行完整记录,内存和 CPU 白白浪费
网络与客户端内存被悄悄拖垮
很多人以为 DB 和应用同机部署就不用操心网络开销,其实不然——本地 loopback 仍走 TCP 协议栈,要序列化、拷贝、反序列化。
实测对比(用户表):
-
SELECT *:含icon_md5 CHAR(32)、log TEXT、profile JSON,单行平均 >12KB -
SELECT id, nick_name, avatar_url:单行压到 200B 以内,体积缩小 60 倍 - Java 应用用
ResultSet加载 1 万行SELECT *,堆内存占用可能飙升数百 MB;显式指定字段后,通常不到 10MB
表结构变更时的隐性故障最难排查
开发阶段表小、字段少、只有你一个服务连,SELECT * 看似无害。但只要 DBA 加了个 backup_data LONGBLOB 字段,所有依赖该表的旧查询就会集体变慢——监控只显示“慢 SQL 数上升”,日志里找不到具体哪条语句惹的祸。
更隐蔽的问题:
- ORM 的
resultMap或实体类未同步更新字段,导致运行时报错或字段值为空 - 下游服务缓存了
SELECT *结果,字段删了却还在读,引发 NPE 或解析异常 - 备份/归档脚本也用
SELECT *,突然因新增大字段导致导出超时或磁盘爆满
真正危险的不是性能下降本身,而是它不报错、不告警、只在流量高峰或数据增长后缓慢显现。











