根本原因是select *强制触发回表和溢出页读取:先查辅助索引得主键,再回聚簇索引取整行,两次随机io;若含text等大字段,还需第三次读溢出页,io成倍增加。

SELECT * 为什么直接拉高磁盘 IO 次数
根本原因不是“多读了几列”,而是它强制触发回表(refetch)——只要查询不能被索引完全覆盖,InnoDB 就得从辅助索引跳回聚集索引拿整行。比如在 name 字段上有索引,但写的是 SELECT * FROM user WHERE name = 'Alice',MySQL 必须:先查 name 索引拿到主键 id,再用 id 去聚簇索引里捞全部字段。两次 B+ 树随机查找,IO 成倍增加。
更糟的是大字段:当某列是 TEXT 或超长 VARCHAR(>728 字节),InnoDB 会把超出部分存到溢出页(overflow page),SELECT * 就要额外再读一次磁盘。哪怕你只想要 id 和 name,也逃不掉。
- 辅助索引页里只存
name + id,不存avatar_url、bio这类宽字段 - 显式写
SELECT id, name FROM user WHERE name = ?,可全程在索引页完成,Extra显示Using index - buffer pool 命中率低于 80% 时,
SELECT *的磁盘刷页行为会立刻暴露为慢查询
网络和内存开销不是“感觉不到”,而是静默吃资源
单行数据包体积膨胀,不是靠“同机部署就能忽略”。Linux loopback 接口仍走完整 TCP 栈:序列化 → 内核 socket buffer → 反序列化。一个带 log TEXT 和 profile JSON 的用户表,SELECT * 单行常超 10KB;而只取 id, username, avatar_url,压到 200B 内很常见。
ORM 更危险:cursor.fetchall() 或 ResultSet 是按行分配内存的。1000 行 × 10KB = 10MB 堆内存白占;而 1000 行 × 200B = 200KB,GC 压力差两个数量级。
-
net_buffer_length默认 16KB,只是单次发送缓冲上限,不改变总传输量 - 分页场景下
LIMIT 10000, 20配合SELECT *,MySQL 仍要构造并丢弃前 10000 行完整记录 - 云环境跨可用区调用时,千行级
SELECT *可能多耗 50ms+ 网络往返
表结构一变,SELECT * 就成定时炸弹
加个 backup_data LONGBLOB 字段?所有旧查询瞬间变慢,监控只报“慢 SQL 数上升”,没人想到是字段膨胀。更隐蔽的是列序问题:ALTER TABLE ADD COLUMN created_at TIMESTAMP FIRST 后,原本靠 rs.getObject(2) 取用户名的 JDBC 代码直接错位——getObject(2) 突然拿到的是时间戳,不是名字。
- 视图用
SELECT *创建,后续ALTER TABLE加字段不会自动更新视图定义 - MyCat / ShardingSphere 等中间件解析元数据时,
*返回列序依赖底层表物理顺序,极易错位 - Django 的
Model.objects.all()默认生成SELECT *,但 serializer 层若只用 3 个字段,剩下 47 个全浪费
哪些情况真能用 SELECT *?别信“小表没事”
唯一安全的场景只有两类:一是 DBA 临时连上去手敲 SELECT * 看两眼样本数据;二是极小字典表(如 status_code,固定 2–3 列且永不变)。其他所谓“安全”全是错觉——只要表未来可能加字段、只要应用代码存在按位置取值、只要 ORM 没做字段裁剪,SELECT * 就埋着性能和稳定性双雷。
最易被忽略的一点:覆盖索引失效不是“性能稍差”,而是让优化器彻底放弃使用索引扫描。一旦回表成本被预估过高,它宁可全表扫描(顺序 IO)也不愿走索引+大量随机 IO——这时 EXPLAIN 里 type 直接变成 ALL,连索引都懒得看了。











