直接写复杂join+group by查询变慢,主因是执行计划退化为全表扫描或临时表排序,常见于大表关联、索引未覆盖、函数导致索引失效等场景;解决方案是用按需设计的汇总表替代实时计算,并通过view自动路由查询。

为什么直接写复杂 JOIN + GROUP BY 的统计查询越来越慢
MySQL 在面对多表关联、嵌套子查询、大范围日期聚合时,执行计划很容易退化为全表扫描或临时表排序。尤其是当 WHERE 条件无法命中复合索引前缀,或 GROUP BY 字段未被索引覆盖时,EXPLAIN 里常出现 Using temporary; Using filesort —— 这基本意味着查询已脱离高效路径。
常见诱因包括:
- 主表数据量超 500 万,且关联的维度表(如用户、地区、品类)也达百万级
- 统计口径频繁变动(比如今天按日汇总、明天加渠道维度、后天要同比),导致难以固化索引
- 查询中混用
DATE(created_at)、YEARWEEK()等函数,使索引失效
用汇总表替代实时计算:核心就是「提前算好,按需取」
这不是建个新表就完事,关键在设计粒度和更新机制。汇总表本质是物化视图的低成本实现,它不依赖 MySQL 8.0+ 的原生物化视图(仍为 Labs 特性且不支持自动刷新),而是靠应用层或定时任务驱动。
你需要决定三个东西:
-
粒度:比如按date+product_id+channel三级聚合,而不是宽泛的“每日总销售额” -
指标字段:只存确定会被查的聚合值,如sum(amount)、count(distinct user_id)、max(order_time),避免冗余 -
更新方式:推荐“增量追加 + 每日补全”,而非全量重刷。例如每天凌晨跑一次:INSERT INTO sales_daily_summary (dt, product_id, channel, total_amount, order_cnt) SELECT '2024-06-15', product_id, channel, SUM(amount), COUNT(*) FROM orders WHERE DATE(created_at) = '2024-06-15' GROUP BY product_id, channel;
怎么让查询自动走汇总表而不改业务代码
硬编码切换表名会污染业务逻辑,也不利于灰度。更稳妥的是用 MySQL 的 VIEW 做一层路由:
CREATE VIEW sales_summary AS SELECT dt, product_id, channel, total_amount, order_cnt FROM sales_daily_summary UNION ALL SELECT DATE(created_at) AS dt, product_id, 'unknown' AS channel, amount, 1 FROM orders WHERE DATE(created_at) > (SELECT MAX(dt) FROM sales_daily_summary);
这样应用层仍查 sales_summary,但热数据走汇总表,冷数据(比如刚插入还没汇总的订单)回退到原始表。注意两点:
-
UNION ALL比UNION快,前提是业务能接受重复主键不合并(汇总表与原始表时间范围无交集即可保证) - 必须给
sales_daily_summary.dt加索引,否则MAX(dt)子查询会变瓶颈
容易被忽略的坑:时区、空值、幂等性
-
dt 字段务必用 DATE 类型,别用 VARCHAR 存 '20240615' —— 后者无法用范围查询,也难和其他日期函数对齐
- 所有
JOIN 到汇总表的查询,WHERE 中的 dt 条件必须用确定值(如 dt = '2024-06-15'),别写 dt >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) —— 这会让优化器放弃使用 dt 索引
- 增量写入脚本必须带
ON DUPLICATE KEY UPDATE 或先 DELETE 再 INSERT,否则重复跑会导致数据翻倍;主键建议设为 (dt, product_id, channel)
- 如果原始表有软删除(
is_deleted = 1),汇总逻辑里漏掉这个条件,结果就会偏高——这种错误线上很难察觉
dt 字段务必用 DATE 类型,别用 VARCHAR 存 '20240615' —— 后者无法用范围查询,也难和其他日期函数对齐JOIN 到汇总表的查询,WHERE 中的 dt 条件必须用确定值(如 dt = '2024-06-15'),别写 dt >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) —— 这会让优化器放弃使用 dt 索引ON DUPLICATE KEY UPDATE 或先 DELETE 再 INSERT,否则重复跑会导致数据翻倍;主键建议设为 (dt, product_id, channel)
is_deleted = 1),汇总逻辑里漏掉这个条件,结果就会偏高——这种错误线上很难察觉汇总表不是银弹。当维度组合爆炸(比如 10 个标签两两组合)、或要求秒级实时,就得考虑 ClickHouse 或预计算服务。但对 T+1 场景下的运营/BI 报表,它是最可控、最易 debug 的提速手段。











