必须构造动态连续日期序列再left join,否则图表断档;起止日期需用min/max动态计算,join时须对齐字段类型与粒度,多维分组需cross join组合,聚合需coalesce处理null,where条件不可误放on中。

直接 GROUP BY 日期字段不会补缺失日,必须先构造完整时间轴再 LEFT JOIN,否则图表断档、趋势误判是常态。
生成连续日期序列时别硬写起止值
起止日期必须动态算,不能写死 '2023-01-01' 这类常量——业务数据范围变化后,补的日期就可能漏头或拖尾。
- 先用
SELECT MIN(dt), MAX(dt) FROM sales拿出真实边界,再喂给GENERATE_SERIES(PostgreSQL)或递归 CTE(MySQL/SQL Server) - PostgreSQL 中写成
GENERATE_SERIES((SELECT MIN(dt) FROM sales)::DATE, (SELECT MAX(dt) FROM sales)::DATE, '1 day') - MySQL 8.0+ 递归 CTE 的终止条件必须用
WHERE dt ,不能写 <code>,否则多出一天 - SQL Server 要加
OPTION (MAXRECURSION 0),否则默认 100 层递归,补一年就爆
LEFT JOIN 时 ON 条件必须对齐粒度
原始表字段和生成的日期字段类型、精度不一致,JOIN 就会失效——比如 created_at 是 TIMESTAMP,而生成的是 DATE,直接等值比较永远为 false。
- PostgreSQL:用
DATE_TRUNC('day', t.created_at) = d.dt或t.created_at::DATE = d.dt - MySQL:用
DATE(t.created_at) = d.dt,别用HOUR()+DATE()拼,跨天会错乱 - 如果按月分组,生成的
month_start是'2023-01-01',那右表就得DATE_TRUNC('month', t.created_at)::DATE对齐 - 多维分组(如
user_id+dt)时,不能只 JOIN 日期,得先CROSS JOIN所有user_id和日期组合,再 LEFT JOIN 原表,否则某用户某天全空就彻底消失
聚合时 NULL 不等于 0,必须显式处理
SUM(amount) 自动忽略 NULL 没问题,但 COUNT(*) 会把补上的空行也计进去,COUNT(amount) 才只计有值的行;更关键的是,没数据的日期行在结果里 amount 是 NULL,不是 0。
- 销量类指标一律用
COALESCE(SUM(t.amount), 0),别信“反正 SUM 会返回 0”——它返回的是 NULL - 存在性判断(比如“当天是否有订单”)用
CASE WHEN COUNT(t.id) > 0 THEN 1 ELSE 0 END,而不是COUNT(*) - WHERE 条件别写在 ON 里,例如
ON d.dt = DATE(t.created_at) AND t.status = 'paid'会让未支付的日期也变 NULL;应放到子查询里过滤,或用WHERE t.status = 'paid' OR t.status IS NULL(后者慎用,逻辑易错) - 原表日期字段没索引?JOIN 时会全表扫描,补 1 年日期 + 10 万用户 = 千万级临时结果,慢到超时
真正麻烦的不是语法,而是分组维度和时间范围的耦合关系——每个用户有自己的活跃周期,每个产品有自己的上架时间,硬套全局日期序列只会让空值爆炸。先拆分维度边界,再逐组生成时间轴,才是稳住性能和语义的底线。











