必须预聚合,因原始明细表按日写入且缺乏月粒度分区,直接跨24个月GROUP BY会全表扫描、无法索引下推;多表JOIN后中间结果膨胀,应先对orders按月聚合再JOIN维度表,最后与递归生成的month_series左连补全月份。

为什么跨月跨年统计报表必须预聚合
因为原始明细表(比如 orders)通常按天写入、无分区或仅按日分区,直接在大时间跨度(如 24 个月)上 GROUP BY DATE_FORMAT(create_time, '%Y-%m') 会扫描全部历史数据,且无法利用索引下推聚合。尤其当关联用户、商品等维度表时,中间结果集极易膨胀——你不是在查“12个月”,而是在查“数千万行 × 多表JOIN × 全字段投影”。
用子查询提前聚合明细数据
别让 SELECT 里带相关子查询,也别等所有 JOIN 完再 SUM()。把聚合动作压到最靠近数据源的位置:
- 先对
orders表按DATE_FORMAT(create_time, '%Y-%m')分组,只保留month_key、order_count、total_amount等必要指标 - 再把这个结果作为派生表或 CTE,和
categories或regions表LEFT JOIN - 如果还要补全缺失月份,就把这个预聚合结果和
month_series(用WITH RECURSIVE生成)做LEFT JOIN,而不是拿原始明细去左连
示例片段:
WITH RECURSIVE month_series AS (
SELECT DATE_FORMAT('2025-01-01', '%Y-%m-01') AS month_start
UNION ALL
SELECT DATE_ADD(month_start, INTERVAL 1 MONTH)
FROM month_series
WHERE month_start < '2026-08-01'
),
monthly_agg AS (
SELECT
DATE_FORMAT(create_time, '%Y-%m') AS month_key,
COUNT(*) AS order_count,
COALESCE(SUM(amount), 0) AS total_amount
FROM orders
WHERE create_time >= '2025-01-01' AND create_time < '2026-09-01'
GROUP BY DATE_FORMAT(create_time, '%Y-%m')
)
SELECT
ms.month_start,
COALESCE(ma.order_count, 0) AS order_count,
COALESCE(ma.total_amount, 0) AS total_amount
FROM month_series ms
LEFT JOIN monthly_agg ma ON DATE_FORMAT(ms.month_start, '%Y-%m') = ma.month_key;
避免在预聚合层用函数破坏索引
在 WHERE 或 GROUP BY 中对日期字段用 YEAR()、MONTH()、DATE_FORMAT() 是常见错误——它会让 create_time 上的索引完全失效。正确做法是:
- 用范围条件限定扫描区间:
create_time >= '2025-01-01' AND create_time (注意开闭区间) - 再在过滤后的结果上做
DATE_FORMAT(create_time, '%Y-%m')分组,此时扫描量已大幅减少 - 若高频按月查,可考虑在表上加生成列:
ALTER TABLE orders ADD COLUMN month_key VARCHAR(7) GENERATED ALWAYS AS (DATE_FORMAT(create_time, '%Y-%m')) STORED,然后给它建索引
跨年场景下递归CTE的两个硬限制
WITH RECURSIVE 是生成连续月份序列最可控的方式,但 MySQL 对它有两处隐性卡点:
- 默认递归深度是 1000,但跨年统计(如 36 个月)需要显式设:
SET SESSION cte_max_recursion_depth = 100;,否则报错Recursive query aborted after 1001 iterations - 起始日期必须规整为当月第一天:
DATE_FORMAT(in_start, '%Y-%m-01'),否则递归可能多算或少算一行;终止条件必须用month_start ,不能用字符串比较(如 <code>month_start ),否则跨年时排序错乱
真正容易被忽略的是:预聚合后补月逻辑,必须和业务时间范围严格对齐。比如统计「2025-01 至 2026-08」,month_series 的 in_end 得是 '2026-08-01',而不是 '2026-08-31'——后者会导致递归多跑一次,最后一个月重复出现。


















