GROUP BY日期后缺天是因为表中无对应记录,需用递归CTE补全日期轴;正确写法是终止条件放在UNION ALL后的WHERE中,否则会无限循环。

为什么GROUP BY日期后缺了某些天的数据
因为SQL的GROUP BY只对实际存在的记录分组,表里没有2024-03-15的订单,那天就不会出现在结果里——不是语法错了,是逻辑上本就“没数据”。想让空日期也占一行,得主动补全日期轴,不能指望原始数据凑齐。
用递归CTE生成连续日期序列的写法
核心是用WITH RECURSIVE从起始日开始逐日叠加,直到达到截止日。注意终止条件必须写在UNION ALL后的WHERE里,否则会无限循环。
常见错误:把终止条件写在递归CTE外部(比如放在SELECT之后的WHERE),那递归部分根本不会停止。
WITH RECURSIVE date_series AS ( SELECT '2024-03-01'::DATE AS dt UNION ALL SELECT dt + INTERVAL '1 day' FROM date_series WHERE dt < '2024-03-31'::DATE ) SELECT ds.dt, COALESCE(t.cnt, 0) AS order_count FROM date_series ds LEFT JOIN ( SELECT DATE(created_at) AS d, COUNT(*) AS cnt FROM orders WHERE created_at >= '2024-03-01' AND created_at < '2024-04-01' GROUP BY DATE(created_at) ) t ON ds.dt = t.d;
MySQL 8.0+和PostgreSQL的语法差异点
MySQL不支持INTERVAL '1 day'这种写法,得改用DATE_ADD(dt, INTERVAL 1 DAY);PostgreSQL中::DATE类型转换更简洁,MySQL要用CAST('2024-03-01' AS DATE)。
- PostgreSQL:递归字段名必须和CTE定义里的完全一致,大小写敏感
- MySQL:递归深度默认限制100层,如果跨度超过100天,要先执行
SET cte_max_recursion_depth = 1000 - 两者都不允许在递归分支里用聚合函数或
GROUP BY,补日期这步必须纯生成,统计逻辑放到外面JOIN
性能要注意的三个地方
递归CTE本身不慢,但和大表LEFT JOIN时容易拖垮查询。特别是当日期范围拉到几年、而业务表有千万级记录时。
- 确保
orders.created_at字段有索引,且WHERE过滤条件能命中索引最左前缀 - 避免在递归CTE里做任何计算,比如
EXTRACT(YEAR FROM dt),全挪到外层SELECT - 如果只是查月度汇总,别生成每日序列再
GROUP BY EXTRACT(MONTH FROM dt),直接用GENERATE_SERIES(PostgreSQL)或构造月份字符串数组更高效
真正麻烦的是跨年+按周统计+要考虑节假日跳过——这时候递归CTE就得加额外判断逻辑,代码立刻变脆,不如把日期轴预生成到一张小维度表里。

















