跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

SQL如何查找分组统计中缺失的日期日期_利用递归CTE构造日期序列

递归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-31' ) SELECT ds.dt, COALESCE(t.cnt, 0) AS cnt FROM date_series ds LEFT JOIN ( SELECT DATE(created_at) AS dt, COUNT(*) AS cnt FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01' GROUP BY DATE(created_at) ) t ON ds.dt = t.dt;
注意 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
出所有分组组合,再和日期交叉,少这一步,补出来的就是假全。

相关文章