递归CTE生成连续日期序列需明确起止时间与深度限制,锚点单行、递归用UNION ALL,LEFT JOIN原表补空并COALESCE转0,多维分组需先CROSS JOIN再LEFT JOIN。

用递归CTE生成连续日期序列
直接用递归 CTE 构造日期范围,是补全缺失日期最可控的方式。关键不是“能不能”,而是递归深度和起止时间必须明确,否则容易报错或性能崩掉。
-
WITH RECURSIVE是标准写法(PostgreSQL、SQL Server、SQLite 支持;MySQL 8.0+ 也支持,但需显式声明RECURSIVE) - 锚点(anchor)必须是单行,比如
SELECT '2024-01-01'::DATE AS dt,不能带FROM表 - 递归部分必须引用自身别名,且
UNION ALL(不能用UNION),否则终止条件失效 - 默认递归上限通常是 100 层,超限会报错
maximum recursion depth exceeded,PostgreSQL 可加MAXRECURSION 0(SQL Server)或设search_path参数(PG 中用SET statement_timeout不起作用,得靠max_recursive_depth配置)
把生成的日期序列和原表 LEFT JOIN 补空
构造完日期后,不能直接 JOIN 原表——那只会保留有数据的日期。必须用 LEFT JOIN,让日期主表驱动,再用 COALESCE 或 COALESCE(count_col, 0) 把空值转成 0。
- JOIN 条件要严格对齐:比如原表的
DATE(created_at)和序列的dt比较,避免时区/类型隐式转换导致漏匹配 - 如果原表有多个分组维度(如
user_id,status),则需要先CROSS JOIN所有分组组合 × 日期序列,再LEFT JOIN原事实表——否则会漏掉“某用户某状态在某天完全没记录”的情况 - 别在递归 CTE 里直接
GROUP BY,先生成完整序列,再在外面套一层查询聚合,逻辑更清晰、也更容易调试
MySQL 8.0 下递归 CTE 的典型写法与坑
MySQL 对递归 CTE 限制较多,不支持 ORDER BY 在递归体中,也不允许子查询出现在递归部分。最稳妥的方式是只做日期递增,其他逻辑后置。
WITH RECURSIVE date_series AS ( SELECT '2024-01-01' AS dt UNION ALL SELECT dt + INTERVAL 1 DAY FROM date_series WHERE dt = '2024-01-01' AND created_at
- 注意 MySQL 中日期相加要用
+ INTERVAL 1 DAY,不能写dt + 1 -
WHERE中的终止条件必须写在递归SELECT后面,且不能引用外部表字段 - 如果原始数据量大,但日期跨度小(比如只查 30 天),这种写法比用日历表或数字表更轻量;反之,若要查 5 年日期,建议预建
calendar表
替代方案:用数字表 + DATE_ADD 构造(兼容老版本 MySQL)
当数据库不支持递归 CTE(如 MySQL 5.7),就退回到「数字表 + 时间偏移」方式。核心是先有一列连续整数,再用它生成日期。
- 临时数字表可用
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 ...)拼出来,最多到 1000 行以内还行;超过建议建物理numbers表 -
DATE_ADD('2024-01-01', INTERVAL n DAY)是通用写法,所有主流 SQL 引擎都支持 - 注意
n起始为 0,否则会跳过首日;结束值应为DATEDIFF('2024-01-31', '2024-01-01'),即 30,不是 31 - 这种方式没有递归深度限制,但手拼数字表易出错,且难以动态控制跨度——所以一旦项目稳定,还是推荐迁移到递归 CTE 或日历表
实际补日期最难的不是语法,是想清楚“缺失”到底指什么:是整个时间轴上所有日期都要有?还是每个分组下各自的时间轴?后者必须先 DISTINCT 出所有分组组合,再和日期交叉,少这一步,补出来的就是假全。










