子查询条件没下推导致全表扫描,因MySQL 5.7无法将外层关联条件或子查询WHERE条件下推至内层表索引过滤,只能先全量执行子查询再回表匹配;8.0虽支持部分下推(如ICP、semi-join),但遇GROUP BY、LIMIT、函数或派生表含聚合时仍退化为物化或嵌套循环,需配合联合索引、分区键对齐及EXPLAIN验证attached_condition才能真正生效。

子查询条件没下推,导致全表扫描
当子查询的 WHERE 条件无法被下推到内层表时,数据库只能先执行完整子查询,再拿结果去外层过滤。比如 SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE region = 'CN'),如果 region 字段没索引,MySQL 5.7 就会扫完整个 customers 表,哪怕外层只查某一天的订单。
- EXPLAIN 中看到
type=ALL或Extra列出现Using where(但没走索引),基本就是条件没下推 - 分区表更敏感:若子查询里没显式带上分区键(如
dt = '2024-06-01'),即使主表已按日期分区,也照样全分区扫描 - PostgreSQL 虽默认开启
enable_partition_pruning,但一旦子查询含IN (SELECT ...)且结果超阈值,优化器仍可能放弃裁剪,退回哈希连接
相关子查询天然阻断下推
像 WHERE EXISTS (SELECT 1 FROM logs l WHERE l.user_id = o.user_id AND l.event_type = 'pay') 这种写法,o.user_id 是外层引用,优化器无法提前确定要查哪些 user_id,也就没法把 event_type = 'pay' 下推到预过滤阶段——只能每行都触发一次子查询执行。
- MySQL 5.7 对这类
DEPENDENT SUBQUERY几乎不优化,执行计划里rows常显示为内层表总行数,实际却是 N×M 次随机 I/O - 即使加了
(user_id, event_type)复合索引,若外层orders行数达 10 万,仍可能产生百万级索引查找 - MySQL 8.0+ 支持部分半连接优化,但遇到
GROUP BY、LIMIT或UNION就自动退回到物化模式
派生表强制物化让下推失效
写成 FROM (SELECT user_id FROM logs WHERE dt >= '2024-01-01') AS recent_logs,MySQL 5.7 默认把它当派生表并强制物化,生成无索引临时表,后续所有 JOIN 或 WHERE 都无法下推到原始 logs 表上执行。
- 哪怕你只选
user_id一列,且dt有索引,物化后仍是全量扫描 + 内存排序 - EXPLAIN 的
Extra列出现Using temporary就是明确信号 - MySQL 8.0 开启
derived_merge=on(默认)能合并简单派生表,但只要子查询里带DISTINCT、聚合或ORDER BY,就立刻失效
为什么有些下推“看似成功”却没提速
下推本身不等于性能提升——它只是把条件挪到更早执行阶段。如果下推后的子查询仍缺乏有效索引,或者数据分布倾斜(比如 90% 的 user_id 都集中在某几个分区),下推反而放大 I/O 压力。
- 检查
EXPLAIN FORMAT=JSON里的attached_condition,确认下推的条件是否真被用于索引查找,而非仅挂在最后过滤环节 - 对高频子查询,宁可提前物化到带索引的临时表(如
CREATE TEMPORARY TABLE tmp_users AS SELECT DISTINCT user_id FROM logs WHERE dt = CURDATE()),也别依赖不可控的下推 - Oracle 和 PostgreSQL 的 CTE 在某些场景下支持下推,但 MySQL 的
WITH子句在 8.0.23 前不保证下推行为,需实测验证
真正卡住性能的,往往不是“有没有下推”,而是下推之后,底层表是否还剩几十万行要筛。索引、分区键对齐、避免函数包裹字段——这些才是让下推落地的关键。











