PostgreSQL的DATE_TRUNC('week',...)默认以周日为起点,需用DATE_TRUNC('week', date - INTERVAL '1 day') + INTERVAL '1 day'对齐周一;GROUP BY必须与SELECT中表达式完全一致;时区处理应先AT TIME ZONE再截断;高频查询可建对应表达式索引。

DATE_TRUNC('week', ...) 默认从周日开始,不是周一
PostgreSQL 的 DATE_TRUNC('week', ...) 默认以**周日为每周起点**,这和很多业务场景(如中国、欧盟习惯的周一为周始)不一致。直接用会导致分组错位——比如 2024-06-03(周一)会被归入 2024-06-02 所在的周,而不是你预期的“6月3日当周”。
解决办法是手动偏移:先减去 1 天,再截断到周,最后加回 1 天,把起点强行对齐到周一:
DATE_TRUNC('week', your_date::date - INTERVAL '1 day') + INTERVAL '1 day'这个表达式等价于“截断到最近的周一(含当天)”。验证一下:DATE '2024-06-03' - INTERVAL '1 day' 是周一前一日(周日),DATE_TRUNC('week', ...) 截到那个周日,再加一天就回到周一。
GROUP BY 中必须用相同表达式,不能只写列别名
很多人写完 SELECT 里用了偏移后的周表达式,GROUP BY 却直接写 week_start 别名,结果报错 column "week_start" does not exist —— PostgreSQL 不允许在 GROUP BY 中引用 SELECT 中定义的别名(除非用子查询或 CTE)。
- ✅ 正确:GROUP BY 和 SELECT 中使用完全一致的表达式
- ❌ 错误:SELECT 中写
DATE_TRUNC(...) + ... AS week_start,GROUP BY 写week_start - ⚠️ 注意:即使加了括号或空格差异,也会被视作不同表达式(如
(expr)≠expr)
按周统计时要注意时区和时间类型
DATE_TRUNC 对 TIMESTAMP WITHOUT TIME ZONE 按本地服务器时区处理;对 TIMESTAMP WITH TIME ZONE 则先转成 UTC 再截断——但结果仍是带时区类型,可能影响后续比较或展示。
推荐统一转成日期再处理,避免时区干扰:
DATE_TRUNC('week', (your_timestamp AT TIME ZONE 'Asia/Shanghai')::date - INTERVAL '1 day') + INTERVAL '1 day'常见坑:
- 表中存的是 UTC 时间,但业务要按北京时间周统计 → 必须先
AT TIME ZONE 'Asia/Shanghai' - 字段是
timestamptz,但没指定时区上下文,::date会按服务器时区转,不可靠 - 用
current_date做基准时,也要注意它不带时区,和 timestamptz 混用可能隐式转换出错
性能提示:给时间字段建表达式索引
如果高频执行按周聚合(比如每天跑报表),且过滤条件常带“某周内”,可以为偏移后周表达式建索引:
CREATE INDEX idx_orders_week_start ON orders
((DATE_TRUNC('week', created_at AT TIME ZONE 'Asia/Shanghai')::date - INTERVAL '1 day' + INTERVAL '1 day'));注意点:
- 索引表达式必须和查询中 GROUP BY / WHERE 里用的一模一样(包括类型转换顺序)
- PostgreSQL 12+ 支持函数索引下推,但旧版本可能不走索引,建议 EXPLAIN 验证
- 别忘了
VACUUM ANALYZE更新统计信息,否则优化器可能忽略新索引
真正容易被忽略的,是时区转换和索引表达式的一致性——差一个 ::date 或多一层括号,索引就形同虚设。

















