PostgreSQL中DATE_TRUNC('week', ...)默认截断至周日零点,非周一;按周一聚合需手动偏移或用EXTRACT(ISODOW)计算;DATE_TRUNC('month', ...)不带时区,timestamptz字段需先显式转时区再截断;GROUP BY应统一使用DATE_TRUNC避免粒度混乱;性能关键是要为常用表达式建匹配的索引。

DATE_TRUNC('week', ...) 默认从周日开始,不是周一
PostgreSQL 的 DATE_TRUNC('week', ...) 会把时间截断到**最近的周日零点**(UTC 时间下),不是很多人默认的周一。比如 '2024-06-10'::date(周一)会被截成 '2024-06-09'(周日),导致跨周错位。
要按「周一为每周起点」聚合,得手动偏移:
- 先减去 1 天,再用
DATE_TRUNC('week', ...),最后加 1 天:DATE_TRUNC('week', ts::date - INTERVAL '1 day') + INTERVAL '1 day' - 或用
EXTRACT(ISODOW FROM ...)手动计算周一日期:ts::date - (EXTRACT(ISODOW FROM ts)::int - 1) % 7(更直观但稍慢)
按月聚合时注意时区和边界值
DATE_TRUNC('month', ...) 返回的是当月第一天的 TIMESTAMP WITHOUT TIME ZONE,且**不带时区信息**。如果你的原始字段是 TIMESTAMP WITH TIME ZONE(如 timestamptz),直接截断可能因时区转换导致日期“跳变”。
例如:'2024-03-01 00:00:00+08'::timestamptz 在 UTC 时区下是 '2024-02-29 16:00:00',DATE_TRUNC('month', ...) 会先转成本地时区再截断,结果可能是 2 月而非 3 月。
- 稳妥做法:先用
timezone('Asia/Shanghai', col)显式转成目标时区,再DATE_TRUNC('month', ...) - 或统一用
col::date转日期再DATE_TRUNC('month', col::date)(避免时间部分干扰) - 聚合时记得
GROUP BY和ORDER BY保持一致,否则排序可能乱序
聚合查询中混用 DATE\_TRUNC 和其他时间函数易出错
常见错误是把 DATE_TRUNC('week', created_at) 和 EXTRACT(YEAR FROM created_at) 放在同一个 GROUP BY —— 这会导致分组粒度不一致,同一周跨年时(如 2024-12-30 是 2024 年第 53 周,但属于 2025 年第 1 周)逻辑混乱。
- 只用
DATE_TRUNC('week', ...)就够了,它返回完整时间戳,自带年份和周信息 - 需要显示“2024-W01”格式?用
to_char(DATE_TRUNC('week', created_at), 'IYYY-IW')(IYYY是 ISO 年,IW是 ISO 周) - 避免在
WHERE中对DATE_TRUNC结果做函数运算(如EXTRACT(MONTH FROM DATE_TRUNC('month', x))),会无法走索引
性能关键:给原始时间字段建表达式索引
DATE_TRUNC 是计算型操作,直接 WHERE DATE_TRUNC('month', created_at) = ... 无法利用 created_at 上的普通 B-tree 索引。
- 建表达式索引:
CREATE INDEX idx_orders_month ON orders (DATE_TRUNC('month', created_at)); - 注意:索引表达式必须和查询中完全一致(包括大小写、空格、时区处理)
- 如果经常按「周一为起点的周」查,索引也得对应:
CREATE INDEX idx_orders_mon_week ON orders (DATE_TRUNC('week', created_at - INTERVAL '1 day') + INTERVAL '1 day'); - 索引字段类型要匹配——若
created_at是timestamptz,索引表达式结果也是timestamptz,别隐式转成timestamp
实际聚合时最容易被忽略的是时区一致性与索引表达式的严格匹配——差一个 INTERVAL 或少一层时区转换,结果就可能偏差数天,而执行计划里还看不出问题。

















