先构造完整日期序列再聚合,核心是「先造全、再连、最后算」;PostgreSQL 12+用GENERATE_SERIES支持时间类型且性能好,步长须显式写单位如'1 day',起止时间宜用子查询动态计算并配合DATE_TRUNC对齐自然月;MySQL 8.0+需递归CTE并设置MAXRECURSION防截断。

直接生成完整日期序列再聚合,核心是「先造全、再连、最后算」——不是靠原始数据凑日期,而是主动构造时间轴,确保每个目标时间段都有对应行,哪怕值为 NULL。
用 GENERATE_SERIES(PostgreSQL)一步生成日期序列
PostgreSQL 12+ 最省事:不用递归、不写 CTE,GENERATE_SERIES 原生支持时间类型,且性能好。
-
GENERATE_SERIES的步长必须显式写单位,比如'1 day'、'1 month',不能只写1 - 起止时间建议用子查询动态算,避免硬编码:例如
(SELECT MIN(dt) FROM sales)和(SELECT MAX(dt) FROM sales) - 若需对齐到自然月开头,别直接传
'2026-01-01',而应先DATE_TRUNC('month', ...)再喂给GENERATE_SERIES - 错误示例:
SELECT GENERATE_SERIES('2026-01-01', '2026-12-31')→ 缺少步长,报错
正确写法:
SELECT d.dt
FROM GENERATE_SERIES(
(SELECT DATE_TRUNC('day', MIN(dt)) FROM sales),
(SELECT DATE_TRUNC('day', MAX(dt)) FROM sales),
'1 day'
) AS d(dt);
MySQL 8.0+ 必须用递归 CTE,且注意深度限制
MySQL 没 GENERATE_SERIES,只能靠 WITH RECURSIVE,但默认递归上限是 1000,补一年日期(365)安全,补三年就可能截断。
- 务必在语句末尾加
OPTION (MAXRECURSION 10000)(MySQL 8.0.3+ 支持),否则超限直接报错Recursive query aborted after 1000 iterations - 终止条件必须用
而非,否则最后一日会多生成一次 - 日期运算必须用
DATE_ADD(dt, INTERVAL 1 DAY),不能写dt + 1(会当整数加) - 起始值若来自子查询,需包一层
(SELECT ...),否则 MySQL 报语法错
示例:
WITH RECURSIVE date_range AS ( SELECT (SELECT MIN(dt) FROM sales) AS dt UNION ALL SELECT DATE_ADD(dt, INTERVAL 1 DAY) FROM date_range WHERE dt <h3>LEFT JOIN 后聚合时,NULL 处理和 GROUP BY 字段必须对齐</h3> <p>日期序列只是骨架,真正要聚合的是业务数据。LEFT JOIN 后常见坑不在“怎么连”,而在“怎么算”。</p>
-
GROUP BY必须用左表的日期字段(如d.dt),不能用右表的t.dt(它可能是 NULL) -
SUM(t.amount)自动忽略 NULL,没问题;但COUNT(t.amount)只计非 NULL 行,COUNT(*)会把补的空行也计进去——按需选 - 要显示“0 销量”,必须写
COALESCE(SUM(t.amount), 0),不能只依赖SUM - WHERE 过滤条件如果写在 JOIN 外层(如
WHERE t.status = 'paid'),会把没匹配的日期行整个过滤掉 → 应该移到子查询里预过滤右表,或改用ON ... AND t.status = 'paid'(注意这会让不满足 status 的日期也显示为 NULL,符合补零逻辑)
大范围日期序列性能敏感点
生成 1 年日期对任何数据库都轻松,但若为 10 万用户各生成 1 年,则中间结果达 3650 万行——这时生成方式直接影响执行是否超时。
- PostgreSQL 的
GENERATE_SERIES在内存中完成,快且稳定 - SQL Server 若用
master..spt_values,虽快但不跨版本兼容;用递归 CTE 则必须加OPTION (MAXRECURSION 0) - MySQL 递归 CTE 在大数据量下易触发临时表溢出,建议限定范围:比如只补最近 90 天,而非从建表日起
- 更稳的做法是预建一张索引化的日期维度表(
dim_date),包含date、year、month_start等字段,JOIN 时走索引,比每次生成快得多
真正容易被忽略的,是日期字段的类型和时区一致性:序列用 CURRENT_DATE 生成的是当前会话时区,而业务表存的可能是 UTC 时间,不统一转换,补出来的“同一天”实际跨了 8 小时。










