子查询中用 select * 会破坏列推导能力,导致优化器无法静态确认列集合,进而放弃谓词下推、索引下推和物化优化,甚至拒绝走覆盖索引;显式指定列名才能让数据库在编译阶段确定数据流向与访问路径。

子查询里用 SELECT * 不是“慢一点”,而是让优化器直接放弃推导列依赖,进而可能选错执行计划、无法下推条件、甚至拒绝走索引。指定列名不是为了“提高解析速度”,而是为了让数据库在编译阶段就能确定数据流向和访问路径。
子查询中 SELECT * 会破坏列推导能力
MySQL/PostgreSQL 在解析子查询时,需要明确知道外层引用了子查询的哪些列,才能决定是否能下推 WHERE、能否复用索引、是否支持物化(materialization)等优化。而 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)有索引,很可能直接走索引下推 - Oracle 和 SQL Server 对
SELECT *子查询更敏感,部分版本会直接禁用谓词下推(Predicate Pushdown)
SELECT * 子查询让覆盖索引失效
覆盖索引依赖“查询所需所有字段都在索引中”。子查询若用 SELECT *,即使外层只取一列,优化器也认为整行都可能被需要,从而拒绝使用只含部分字段的索引。
- 假设表
orders(id, user_id, amount, status, created_at),有联合索引(user_id, status, amount) -
SELECT amount FROM (SELECT * FROM orders WHERE user_id = 123) t WHERE status = 'paid'→ 无法利用该索引,因SELECT *暗示可能需要id或created_at,必须回表 -
SELECT amount FROM (SELECT user_id, status, amount FROM orders WHERE user_id = 123) t WHERE status = 'paid'→ 可完全走索引,零回表
嵌套层级加深时,SELECT * 的代价指数级放大
每多一层子查询,SELECT * 就多一次元数据膨胀:列名重复解析、权限校验重跑、隐式类型转换风险叠加。尤其当子查询含 JOIN 或 UNION 时,列推导复杂度陡增。
- 三层嵌套
SELECT *子查询,在 MySQL 8.0+ 中可能触发Derived table materialization强制落盘,哪怕结果只有几行 - PostgreSQL 的
subquery_scan节点在EXPLAIN中显示width=xxx(估算宽度),*会让这个值严重高估,误导成本模型选错连接方式 - ORM 如 MyBatis 动态生成子查询时,若未显式限制字段,
<select></select>套<include></include>容易无意中引入SELECT *,线上查慢日志里常见这类“隐形膨胀”
真正容易被忽略的不是语法本身,而是子查询上下文里字段的“可见性契约”——你写 SELECT *,等于告诉数据库:“我随时可能用到任意一列”,它只能按最坏情况准备。而生产环境里,99% 的子查询只服务于一个明确字段用途,硬编码列名才是对执行计划最诚实的表达。










