报表系统中常见的写法:先在子查询里 GROUP BY 汇总,外层再按条件过滤。在 MySQL 5.7 中,这种写法会先对 100 万行全量分组物化为临时表,再在外层过滤--明明只需要 1 个用户的数据,却分组了 10 万个用户。
-- 5.7: 先全量分组 100 万行,再过滤到 1 行
SELECT *
FROM (
SELECT user_id, SUM(amount) AS total
FROM t_order
GROUP BY user_id
) t
WHERE t.user_id = 100;::: warning 真实场景 FROM 子查询(派生表)在 5.7 中会被完整物化为临时表,外层 WHERE 条件无法下推。这是 5.7 升级 8.0 后最明显的"免费提速"场景之一。8.0 的派生条件下推(derived condition pushdown)自动将外层条件下推到子查询内部。 :::
SELECT *
FROM (
SELECT user_id, SUM(amount) AS total
FROM t_order
GROUP BY user_id
) t
WHERE t.user_id = 100;+----+-------------+------------+-------+-------------+-------------+--------+----------+---------------------------------+
| id | select_type | table | type | key | key_len | rows | filtered | Extra |
+----+-------------+------------+-------+-------------+-------------+--------+----------+---------------------------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | 100000 | 10.00 | Using where |
| 2 | DERIVED | t_order | index | idx_user_id | idx_user_id | 998560 | 100.00 | NULL |
+----+-------------+------------+-------+-------------+-------------+--------+----------+---------------------------------+
5.7 的执行流程:
- 派生表物化(id=2):对
t_order全表 100 万行按user_id分组,生成 10 万行临时表 - 外层过滤(id=1):在 10 万行临时表中扫描过滤
user_id = 100,filtered=10.00表示只命中 10%
明明只需要 1 个用户的数据,却分组了 10 万个用户。99999 行的分组和物化完全浪费。
-- 8.0 中同样的 SQL,优化器自动将 WHERE user_id=100 下推到派生表内部
-- 等价于: SELECT user_id, SUM(amount) FROM t_order WHERE user_id = 100 GROUP BY user_id
SELECT *
FROM (
SELECT user_id, SUM(amount) AS total
FROM t_order
GROUP BY user_id
) t
WHERE t.user_id = 100;8.0 的派生条件下推(derived condition pushdown):
- 优化器检测到外层
WHERE t.user_id = 100可以下推到派生表内部 - 等价改写为:
SELECT user_id, SUM(amount) FROM t_order WHERE user_id = 100 GROUP BY user_id - 只分组
user_id=100的约 10 行数据,而非全量 100 万行 - 物化行数从 10 万降到 1
| 5.7(无下推) | 8.0(自动下推) | |
|---|---|---|
| 派生表 type | index(全索引扫描) |
ref(索引精确查找) |
| 派生表 rows | ~998,560 | ~10 |
| 物化行数 | 100,000 | 1 |
| 外层过滤 | Using where(扫 10 万行) |
无需过滤 |
| 耗时 | ~680 ms | ~5 ms |
::: warning 注意事项
-
5.7 中需手动改写。将外层条件移到子查询内部:
-- 5.7 手动下推 SELECT user_id, SUM(amount) AS total FROM t_order WHERE user_id = 100 GROUP BY user_id;
-
不是所有条件都能下推。包含聚合函数的条件(如
HAVING SUM(amount) > 100)无法下推,因为依赖聚合结果。 -
8.0 下推是自动的,无需修改 SQL,升级 8.0 即可受益。但建议检查 EXPLAIN 确认下推生效。
-
视图也有类似优化。8.0 对视图的条件下推也做了增强,合并视图定义中的条件。 :::
| 特性 | 5.7 | 8.0 |
|---|---|---|
| 派生条件下推 | ❌ 不支持 | ✅ 自动下推 |
| 派生表物化 | 总是物化 | ✅ 延迟物化(必要时才物化) |
| 视图合并 | 部分支持 | ✅ 增强合并算法 |
::: tip 8.0 升级免费提速
派生条件下推是 8.0 最重要的优化器改进之一。升级 8.0 后,大量 FROM (子查询) WHERE 写法会自动受益,无需改 SQL。
:::
# 默认在 MySQL 8.0 上运行
./scripts/run-case.sh 78-derived-condition-pushdown
# 在 MySQL 5.7 上运行(对比)
./scripts/run-case.sh 78-derived-condition-pushdown --ver 5.7
# 跳过造数据重跑
./scripts/run-case.sh 78-derived-condition-pushdown --no-seed