生产环境禁用SELECT *,因其导致覆盖索引失效、强制回表或全表扫描,引发I/O激增、应用层字段错位、网络与内存开销剧增,且掩盖真实性能与维护风险。

生产环境里用 SELECT * 不是语法错误,而是把性能、稳定性和可维护性全押在“表结构永远不变”这个几乎每天都在被打破的假设上。
覆盖索引失效导致回表或全表扫描
MySQL/PostgreSQL 的覆盖索引(Using index)只在查询字段全部落在索引叶子节点时生效。SELECT * 一出现,只要表里有任何一个字段没被该索引包含(比如新增的 config_json 或原有的 avatar_blob),优化器就会放弃覆盖索引,强制回表——先查二级索引拿主键,再回聚簇索引捞整行。
-
EXPLAIN中Extra字段从Using index变成Using where; Using index condition或Using temporary; Using filesort,就是明确信号 - InnoDB 对溢出页(off-page)的额外读取会让 I/O 次数翻倍,buffer pool 命中率低时延迟飙升
- 哪怕你只想要
id和name,而表上有(id, name)覆盖索引,SELECT *也会让它作废
表结构变更直接击穿应用层
字段增删改不是“开发期小事”,而是线上故障高频根因。SELECT * 让应用隐式依赖列顺序、数量和类型,一旦底层变动,错位就发生得悄无声息。
- JDBC 中用
ResultSet.getObject(2)按位置取值?加一列后全部下标越界,抛Column index out of range - MyBatis 的
resultMap若未显式映射,新增一个NOT NULL字段且无默认值,直接触发SQLException - 视图用
SELECT * FROM t创建,后续ALTER TABLE t ADD COLUMN不会自动同步,查出来永远少一列 - Django 的
Model.objects.all()默认生成SELECT *,必须配合.values('a', 'b')或.only('a', 'b')收口
网络、内存与执行计划三重开销被严重低估
一行数据体积不由业务需要决定,而由表里最宽字段决定。你只用 id 和 nickname,SELECT * 却可能拖着几 MB 的 profile_json 和 avatar_blob 一起走。
- 单行从 200B 膨胀到 5KB,1000 行就是 5MB 网络传输——即使 DB 和应用同机部署,也要走 loopback TCP 栈
- ORM 如 Hibernate 为每个字段创建对象属性,哪怕你只访问其中两个,GC 压力也翻倍
- 分页场景如
LIMIT 10000, 20,MySQL 仍要构造并丢弃前 10000 行完整记录,CPU 和内存纯浪费 - 优化器无法预判实际需要哪些列,执行计划波动大;
pg_stat_statements把不同字段组合视为不同语句,但SELECT *看似统一,实则每次解析都要重新推导列集合,缓存命中率低
子查询中用 SELECT * 是性能退化的加速器
子查询里的 SELECT * 不只是“多传点数据”,它让优化器彻底失去列推导能力,谓词下推、索引下推、覆盖索引全部失效,常见表现是临时表 + 全表扫描。
-
SELECT a FROM (SELECT * FROM t) s WHERE s.b > 10:优化器不知道s.b是否存在、是否有索引,大概率放弃下推,先物化整个子查询结果再过滤 -
SELECT a FROM (SELECT a, b FROM t) s WHERE s.b > 10:优化器立刻识别b可用于过滤,若t(b)有索引,很可能直接走索引范围扫描 - 三层嵌套
SELECT *子查询,在 MySQL 8.0+ 中可能触发 Derived table materialization 强制落盘,哪怕结果只有几行 - 假设表
orders(id, user_id, amount, status, created_at)有联合索引(user_id, status, amount),外层只取amount,但子查询用SELECT *就会让这个索引完全失效
真正难的不是多敲几个字段名,而是得想清楚:这一查到底要什么数据、谁在消费、字段生命周期多长、下次加列时会不会崩——SELECT * 让人跳过所有这些判断,只留下一个看似省事、实则高危的惯性动作。最常被忽略的是子查询和视图场景,那里没有报错,只有缓慢退化和难以复现的错位。

















