真正卡顿的根源是执行路径错误,导致join前未过滤而加载大量冗余数据;应优先下推where条件、用子查询或cte预聚合、确保group by字段匹配索引顺序,并避免在视图中嵌套完整join+group by逻辑。

先看EXPLAIN里rows是否爆炸
如果JOIN后GROUP BY的rows值比最终分组数高一两个数量级(比如100万→800),说明数据库在聚合前已经加载了大量冗余数据。这不是索引能解决的问题,而是执行路径错了。
- 立刻用
EXPLAIN FORMAT=TREE或EXPLAIN ANALYZE确认中间结果集大小 - 检查
type字段:出现ALL或index且rows巨大,说明驱动表没走索引或过滤太晚 - 别只盯着GROUP BY字段有没有索引——先让参与JOIN的数据变少,比给百万行结果加索引更有效
把WHERE条件下推到子查询或ON里
写成SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE o.created_at > '2026-06-01',MySQL 5.7很可能先全量JOIN再过滤;而写成JOIN (SELECT * FROM orders WHERE created_at > '2026-06-01') o ON u.id = o.user_id,就能让过滤提前生效。
- LEFT JOIN右表的过滤条件必须写进
ON,不能放WHERE,否则退化为INNER JOIN语义 - 时间、状态等高选择性字段(如
status = 'paid')优先下推,哪怕多套一层子查询 - MySQL 8.0+可用CTE预聚合:
WITH recent_orders AS (SELECT user_id, SUM(amount) FROM orders WHERE created_at >= '2026-06-01' GROUP BY user_id),再JOIN
GROUP BY字段必须匹配驱动表索引顺序
如果最终要按users.region, users.level分组,但驱动表users上只有INDEX(level, region),MySQL可能无法利用该索引避免排序,被迫建临时表+filesort。
- 确保
GROUP BY字段顺序与联合索引字段顺序一致,且索引前缀覆盖所有分组列 - 若分组字段来自被驱动表(如
products.category_name),给该表建索引时把JOIN键前置:CREATE INDEX idx_cat_id ON products(category_name, id) - PostgreSQL可加
ORDER BY NULL显式禁用隐式排序;MySQL中SQL_BIG_RESULT提示有时能绕过内存排序限制
别在视图里塞完整JOIN+GROUP BY链路
视图定义里写SELECT u.name, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id,每次调用都会展开并执行整条逻辑——哪怕你只查最近7天数据,它也先把历史全部订单JOIN一遍。
- 视图只保留轻量关联,聚合逻辑拆到上游:先建
orders_7d_summary物理表或用CTE预聚合 - 多个报表共用同一聚合逻辑时,优先建预聚合表,比反复跑视图稳定得多
- 临时表是快速降级方案:
CREATE TEMPORARY TABLE tmp_orders AS SELECT user_id, COUNT(*) cnt FROM orders WHERE created_at >= '2026-06-01' GROUP BY user_id,再加INDEX(user_id)
真正卡住的地方,往往不是GROUP BY本身,而是你让数据库先连了100万行,再挑出800个分组。先筛、再连、最后聚,这个顺序不能颠倒。











