group by month()漏掉空月份是因为sql聚合只处理实际存在的记录,需用递归cte生成完整时间序列再left join业务表,注意过滤条件须下推至join子查询或on条件中避免null行丢失。

为什么 GROUP BY MONTH() 会漏掉空月份
直接用 GROUP BY MONTH(order_date) 或 GROUP BY YEAR(order_date), MONTH(order_date) 只会返回有数据的月份,数据库不会凭空生成缺失的月份行。这不是语法错误,而是 SQL 的基本行为:聚合只作用于实际存在的记录。
补零的本质是「构造完整的时间序列」再左连接原始数据,而不是靠聚合函数本身实现。
用递归 CTE 生成连续月份(PostgreSQL / SQL Server / MySQL 8.0+)
最可控的方式是先生成目标时间范围内所有年月组合,再与业务表 LEFT JOIN。递归 CTE 是跨库兼容性较好的方案(注意 MySQL 需开启 cte_max_recursion_depth)。
- 起始月份和结束月份必须明确指定,不能依赖表中 min/max —— 否则又会漏掉两端空月
- 生成的
year_month建议统一为DATE类型(如每月 1 日),避免字符串比较陷阱 - JOIN 条件要对齐:比如用
DATE_TRUNC('month', order_date) = month_series.dt(PostgreSQL)或YEAR(o.date) = YEAR(m.dt) AND MONTH(o.date) = MONTH(m.dt)(MySQL)
WITH RECURSIVE month_series AS ( SELECT '2023-01-01'::DATE AS dt UNION ALL SELECT dt + INTERVAL '1 month' FROM month_series WHERE dt <h3>用数字表或 calendar 表替代递归(兼容 MySQL 5.7 / SQLite / 旧版 SQL Server)</h3><p>没有递归支持时,可硬编码 12 行数字(<code>VALUES (1),(2),...,(12)</code>),或建一张最小粒度为月的 <code>calendar</code> 维度表。后者更利于复用,且能附带节假日等属性。</p>
- 硬编码数字表只适合固定年份范围;跨年需配合
CROSS JOIN年份列表 -
calendar表的year_month字段建议设为CHAR(7)(如 '2023-01')或DATE(存每月 1 日),避免用整数 202301 —— 排序和索引都不友好 - LEFT JOIN 后必须用
COALESCE(SUM(...), 0),不能只写SUM(...),否则 NULL 会被当成 0 参与计算(虽结果一样,但语义不清)
WHERE 条件写在 JOIN 里还是外部?这是关键坑点
如果只想补 2023 年的零,但又在主查询加了 WHERE order_date >= '2023-01-01',会导致左连接失效——NULL 行被过滤掉。正确做法是把时间过滤下推到 JOIN 条件中,或提前在子查询里限定业务数据范围。
- 错误写法:
LEFT JOIN orders o ON ... WHERE o.order_date >= '2023-01-01'→ 消灭 NULL 行 - 正确写法 1:
LEFT JOIN (SELECT * FROM orders WHERE order_date >= '2023-01-01' AND order_date - 正确写法 2:
LEFT JOIN orders o ON ... AND o.order_date >= '2023-01-01' AND o.order_date
时间边界用「左闭右开」([start, end))比 BETWEEN 更安全,尤其当 order_date 含时间部分时。










