第一份周报:日期处理与执行顺序
老板周五要看「近 30 天每天卖多少」。你发现没有订单的日期在报表里整行消失;顺便,是时候搞清楚一条 SQL 到底按什么顺序执行了。
学 · 40 min
01date / timestamp / interval;date_trunc 按天周月截断、extract 取分量
场景周报要按天、按周汇总--不搞清楚时间类型,分组都是错的。
date只有日期,timestamp带时分秒,interval是时间段(加减日期用)。两个主力函数:date_trunc('week', 列)截断到所在周周一零点(按周分组就靠它);extract('year' from 列)取出年 / 月 / 日等分量。select date_trunc('week', current_date); -- 本周一 0 点 select extract('dow' from current_date); -- 星期几(0=周日) select current_date + interval '1 day'; -- 明天易错date_trunc('week') 的周从周一开始;业务如果按周日切周,得自己算偏移。
02generate_series 生成日期序列左连接补零
场景近 30 天日报交上去,老板发现没单的日期整行消失--不是没卖,是没卖的日子连行都没有。
解法的骨架:用
generate_series(起, 止, interval '1 day')造出完整的 30 行日期,把它当左表,left join orders。没订单的日期那行 o.* 全是 NULL,靠count(o.id)数出 0、coalesce把 NULL 金额补 0。select d::date as 日期, count(o.id) as 订单数, coalesce(sum(o.total_amount), 0) as gmv from generate_series(current_date - 29, current_date, interval '1 day') d left join orders o on o.created_at >= d and o.created_at < d + interval '1 day' group by d order by d;易错日期序列是驱动表(左表),写在 from 里、orders 去 join 它--方向反了补零就失效。
03查「上周一到上周日」不硬编码日期的写法
场景写死日期的报表下周就作废,老板每周都要--日期必须算出来。
公式就一行:
date_trunc('week', current_date)是本周一零点,减 7 天得上周一零点,左闭右开到本周一。所有「上一个周期」的需求都是这个模板:先锚定本周期的起点,再平移。where created_at >= date_trunc('week', current_date) - interval '1 week' and created_at < date_trunc('week', current_date)易错时间戳别用 between:它含两头,上周日 23:59:59.999 之后、周一零点之前的毫秒归属说不清。
04九步逻辑执行顺序:FROM -> JOIN -> WHERE -> GROUP BY -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> LIMIT
场景本周背不下来这个,下周的多表查询会处处撞墙。它是理解一切「为什么这样写不行」的钥匙。
书写顺序和执行顺序是两回事:先有表(FROM/JOIN),再筛行(WHERE),再分组(GROUP BY)、筛组(HAVING),然后才算 SELECT 里写的东西,DISTINCT、ORDER BY、LIMIT 依次收尾。你在 WHERE 里用不了 SELECT 的别名、在 WHERE 里写不了聚合,全是这一个原因。
select status, -- 6. SELECT 决定输出哪些列 count(*) as n -- 6. 聚合在这里算出来 from orders where total_amount > 100 -- 3. WHERE 过滤行 group by status -- 4. 分组,每组之后只剩一行 having count(*) > 10 -- 5. HAVING 过滤组 order by n desc -- 8. 排序(此时才能用别名 n) limit 3; -- 9. 最后才取前 3易错面试必背。默写不出来的话,把它当成「一句话说明书」:先拿数据,再筛数据,再算数据,最后摆盘。
05别名可见性:为什么 WHERE 用不了 SELECT 的别名,ORDER BY 却可以
场景你在 where 里写了 select 定义的别名,报错 column does not exist--明明拼写没错。
还是执行顺序:WHERE 是第 3 步、SELECT 是第 6 步--WHERE 执行时别名还没出生;ORDER BY 在 SELECT 之后,别名已经存在。一句话:一个子句只能用它执行时已经存在的东西。
select total_amount as amt from orders where amt > 100; -- ERROR: column "amt" does not exist -- order by amt 却没问题易错PG 的 group by 其实允许用别名(group by amt 可以),但这是方言、可移植性差--按「别名只有 ORDER BY 能用」记最安全。
练 · 65 min
- 近 30 天每日订单数和 GMV,没有订单的日期要补 0(generate_series 左连接)
参考答案
骨架是 generate_series 造出 30 行日期,左连接让「没订单的日期」也留一行(o.* 全 NULL),再靠 count(o.id) 数非空、coalesce 把 NULL 补 0。
select d::date as 日期, count(o.id) as 订单数, coalesce(sum(o.total_amount), 0) as gmv from generate_series(current_date - 29, current_date, interval '1 day') d left join orders o on o.created_at >= d and o.created_at < d + interval '1 day' group by d order by d; - 按周统计订单数与 GMV(
date_trunc('week'))参考答案
date_trunc('week', ...) 以周一为一周的开始,截断到那天的零点。
select date_trunc('week', created_at) as 周, count(*) as 单数, sum(total_amount) as gmv from orders group by 1 order by 1; - 查「上周」的订单,不许硬编码日期
参考答案
本周一零点减 7 天 = 上周一零点,左闭右开正好是上周一到上周日。
select id, created_at from orders where created_at >= date_trunc('week', current_date) - interval '1 week' and created_at < date_trunc('week', current_date); - 写一条包含全部九个子句的查询,在注释里标注每步之后大约剩多少行
参考答案
注释里的编号就是九步逻辑顺序的位置;DISTINCT 在第 7 步(SELECT 之后)。书写顺序 ≠ 执行顺序。
select status, -- 6. SELECT 决定输出哪些列 count(*) as n -- 6. 聚合在这里算出来 from orders where total_amount > 100 -- 3. WHERE 过滤行 group by status -- 4. 分组,每组之后只剩一行 having count(*) > 10 -- 5. HAVING 过滤组 order by n desc -- 8. 排序(此时才能用别名 n) limit 3; -- 9. 最后才取前 3 - 故意在 WHERE 里用 SELECT 定义的别名,记下报错并解释原因
参考答案
WHERE 是第 3 步,SELECT 是第 6 步--WHERE 执行时别名还没出生;ORDER BY 在 SELECT 之后,所以可以用别名。
select total_amount as amt from orders where amt > 100; -- ERROR: column "amt" does not exist