跨月统计报表必须先生成完整月份序列再左关联业务表,避免漏月;用WITH RECURSIVE生成连续月份序列(MySQL 8.0+),需设cte_max_recursion_depth、起始日为当月1日,并用month_start >= in_start AND month_start <= in_end作终止条件。

跨月统计报表在存储过程中不能靠 GROUP BY MONTH(create_time) 硬凑——它会漏掉没数据的月份,导致折线图断层、同比计算错位。必须先生成完整月份序列,再左关联业务表。
用 WITH RECURSIVE 生成连续月份序列(MySQL 8.0+)
这是最干净的做法,执行计划稳定、代码可读性强,且天然支持参数化起止时间。
- 递归深度默认限制为 1000,跨年统计(如 24 个月)需提前设
SET SESSION cte_max_recursion_depth = 100; - 起始日期必须是当月 1 日(
DATE_FORMAT(in_start, '%Y-%m-01')),否则递归可能多算或少算一行 - 终止条件用
WHERE month_start >= in_start AND month_start 比字符串比较更安全,避免时区或格式歧义 - 示例片段:
WITH RECURSIVE month_series AS ( SELECT DATE_FORMAT(in_start, '%Y-%m-01') AS month_start UNION ALL SELECT DATE_ADD(month_start, INTERVAL 1 MONTH) FROM month_series WHERE month_start < in_end ) SELECT ... FROM month_series ms LEFT JOIN orders o ON ...
GROUP BY 前必须处理 NULL 和聚合逻辑错位
LEFT JOIN 后直接 COUNT(*) 会把无订单月份计为 0 行,但实际返回的是 NULL;而 COUNT(o.order_id) 才真正统计该月订单数。
-
COUNT(*)统计结果集行数(含左表空匹配行),COUNT(字段)只统计非 NULL 字段值——这是跨月统计里最常踩的坑 - 聚合字段若含 NULL,
SUM()和AVG()默认忽略,但报表常需显示 0,得用COALESCE(SUM(amount), 0) - 别在
WHERE里写o.create_time IS NOT NULL——这会把无数据月份过滤掉,破坏序列完整性
存储过程里传参控制统计粒度(避免硬编码)
不同报表要按「年+月」「季度」「自然周」切分,不能每个都写一个存储过程。用输入参数驱动分组逻辑更可持续。
- 用
IN period_type VARCHAR(10)控制分组方式:'month'→DATE_FORMAT(o.create_time, '%Y-%m'),'quarter'→CONCAT(YEAR(o.create_time), '-Q', QUARTER(o.create_time)) - 动态拼接
GROUP BY表达式时,务必校验参数值(IF period_type NOT IN ('month','quarter','year') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid period_type';) - 注意 MySQL 对表达式别名的限制:
GROUP BY必须写完整表达式,不能只写AS report_period
大表跨月统计的性能雷区
千万级订单表直接 LEFT JOIN 12 行月份序列,仍可能触发全表扫描——关键不在序列长度,而在关联字段是否走索引。
- 确保业务表上有
INDEX (create_time)或组合索引INDEX (create_time, status)(如果带状态过滤) - 避免在
ON条件里对字段函数化:DATE_FORMAT(o.create_time, '%Y-%m') = ms.month_str会让索引失效;应改用范围查询:o.create_time >= ms.month_start AND o.create_time - 若统计口径固定(如每月销售总额),考虑用物化视图或预计算汇总表,存储过程只查汇总表,而非原始明细
跨月统计真正的难点不在生成序列,而在让关联不丢月份、聚合不歪数据、参数不破索引——每一步都得盯着执行计划里的 type 和 key 列验证。

















