子查询中使用SELECT *会导致性能严重劣化,因其强制生成冗余字段的临时表,引发内存溢出、IO激增和索引失效;应显式指定所需字段以实现覆盖索引和降低网络吞吐。

子查询里用 SELECT * 会把整张表的字段结构“复制”进中间结果集
这不是语法错误,而是数据流被硬塞进了冗余通道。比如 SELECT * FROM (SELECT * FROM user) t,外层没加任何过滤或投影,数据库必须把内层查出的每一行所有字段原样打包、序列化、传给上层执行器——哪怕你最终只取 t.id。这个过程不省略任何字段解析、类型校验、内存分配步骤。
常见错误现象:
- EXPLAIN 显示
Using temporary+Using filesort,尤其当内层表有 TEXT/BLOB 字段时 - 网络监控看到 DB 到应用层的流量突增数倍,但业务逻辑实际只用 2–3 个字段
- MySQL 连接池频繁超时,
net_read_timeout被反复触发
关键点在于:子查询的结果集是临时表,而 SELECT * 让这个临时表和原表字段完全一致——包括那些你根本不会访问的字段。InnoDB 在构造临时表时,仍要为每个字段预留空间、处理溢出页(比如超过 728 字节的 TEXT 字段会额外触发一次磁盘 IO),这些开销全被算进子查询生命周期里。
MySQL 8.0+ 对无别名 SELECT * 子查询直接报错
错误信息是:Every derived table must have its own alias,但很多人误以为加个别名就安全了,其实不是。加了别名后,问题只是从语法错误变成性能黑洞。
使用场景里最危险的是 JOIN 嵌套:
-
SELECT o.order_no FROM order o JOIN (SELECT * FROM user) u ON o.user_id = u.id——u临时表含 47 个字段,其中 3 个是 JSON,2 个是MEDIUMTEXT - 即使
o.user_id = u.id走了索引,MySQL 仍得把全部 47 列加载进 join buffer,极易触发磁盘临时表
参数差异:MySQL 的 tmp_table_size 和 max_heap_table_size 默认通常只有 16MB,一旦子查询返回的单行数据因 SELECT * 膨胀到几百 KB,几十行就撑爆内存,强制落盘。
SELECT * 子查询让覆盖索引彻底失效
假设 user 表有联合索引 idx_status_created(status, created_at),你想查最近上线的活跃用户 ID:
❌ 危险写法:SELECT id FROM (SELECT * FROM user WHERE status = 1) t WHERE created_at > '2026-10-01'
✅ 正确写法:SELECT id FROM (SELECT id, status, created_at FROM user WHERE status = 1) t WHERE created_at > '2026-10-01'
区别在哪?前者强制 MySQL 先回表读全字段(哪怕只为了后续过滤 created_at),后者让内层查询完全走索引覆盖——id、status、created_at 全在索引里,零回表。
性能影响明显:实测某订单明细表(23 字段,含 2 个 JSON),子查询用 SELECT * 平均耗时 85ms;显式指定 4 个字段后降到 9ms,网络吞吐下降 92%。
ORM 框架里隐式生成的子查询更难察觉
MyBatis 的 <selectresultmap></selectresultmap> 或 Hibernate 的 @Subselect 注解,如果底层映射没约束字段,很容易在动态 SQL 中透出 SELECT *。例如:
SELECT * FROM (SELECT * FROM user WHERE ${dynamic_condition}) t —— 只要 ${dynamic_condition} 一变,整个子查询字段体积就不可控。
容易踩的坑:
- 开发期用小数据集测试,看不出问题;上线后数据量翻倍,子查询内存占用指数增长
- DBA 看慢查询日志只关注外层 SQL,忽略括号里的子查询才是瓶颈源头
- 认为“反正只查一行”,但子查询本身可能扫描上万行才过滤出这一行,
SELECT *把这上万行的全字段都搬了一遍
真正麻烦的不是写几个字段名,而是子查询天然缺乏上下文感知——它不知道外层要什么,只能照单全收。一旦习惯性写 SELECT *,就等于把数据传输的决策权交给了表结构,而不是业务逻辑。

















