DATE_TRUNC('month', date)是最直接的写法,将日期截断为当月第一天00:00:00,支持TIMESTAMP和DATE类型,但需注意时区影响及类型一致性问题。

DATE_TRUNC('month', date) 是最直接的写法
PostgreSQL 的 DATE_TRUNC 函数支持按 'month' 精度截断,它会把任意日期时间值归到当月第一天的 00:00:00(时区按当前 session 设置)。注意不是四舍五入,而是向下取整到月份边界。
- 输入是
TIMESTAMP或DATE都可以,结果统一为TIMESTAMP WITHOUT TIME ZONE(除非原值带时区且 session 有明确设置) - 例如:
DATE_TRUNC('month', '2024-07-15 14:22:03'::TIMESTAMP)返回2024-07-01 00:00:00 - 如果传入的是
DATE类型(如'2024-07-15'),内部会先转成当天 00:00:00 的 timestamp 再截断,效果一样
小心时区导致的意外偏移
当输入值是 TIMESTAMP WITH TIME ZONE(即 TIMESTAMPTZ)时,DATE_TRUNC 先按 session 时区转换成本地时间,再截断。这可能导致跨月错位。
- 比如服务器在 UTC+8,你插入
'2024-07-01 01:00:00+00'(即 UTC 时间 7 月 1 日凌晨 1 点),session 时区为Asia/Shanghai,它会被转成2024-07-01 09:00:00,截断后仍是2024-07-01 - 但若输入是
'2024-07-01 16:00:00+00'(UTC 7 月 1 日下午 4 点),转本地时间为2024-07-02 00:00:00,截断就变成2024-07-02—— 实际你想按 UTC 月份分组,结果却按北京时间分了 - 解决办法:显式转成 UTC 再截断:
DATE_TRUNC('month', your_tz_column AT TIME ZONE 'UTC')
和生成月度汇总 SQL 联用的常见模式
做月度聚合时,DATE_TRUNC('month', created_at) 常作为 GROUP BY 和 ORDER BY 字段,但要注意别漏掉类型一致性问题。
- 不要写
GROUP BY EXTRACT(YEAR FROM created_at), EXTRACT(MONTH FROM created_at)—— 这样无法保证年月顺序正确(比如 2023-12 会排在 2024-01 后面) - 推荐写法:
GROUP BY DATE_TRUNC('month', created_at) ORDER BY 1,结果天然有序且可直接用于时间范围筛选 - 如果后续要查“上个月”,别用
DATE_TRUNC('month', NOW()) - INTERVAL '1 month',它可能因月末天数不同出错(如 3 月 31 日减一个月不是 2 月 31 日,而是 3 月 3 日);稳妥做法是:DATE_TRUNC('month', NOW() - INTERVAL '1 day') - INTERVAL '1 day',先退一天再截断
替代方案:用 make_date() + EXTRACT 组合更可控?
纯日期场景(无时间部分)下,有人倾向用 MAKE_DATE(EXTRACT(YEAR FROM d)::INT, EXTRACT(MONTH FROM d)::INT, 1),但它只接受 DATE 输入,不兼容 timestamp,且不能处理时区逻辑。
-
DATE_TRUNC更通用,支持所有时间类型,也内建时区处理逻辑 -
MAKE_DATE的优势是结果类型明确为DATE,如果你确定字段全是DATE且不需要时区转换,它语义更清晰 - 但多数生产场景中,原始数据是
TIMESTAMPTZ,硬转DATE会丢失时区信息,反而增加歧义
实际用的时候,优先用 DATE_TRUNC('month', ...),但得盯住输入类型和时区上下文——这两点错了,结果可能全偏。










