PostgreSQL 14+ 推荐用 date_bin('30 minutes', ts, '2000-01-01') 实现半小时分组,锚点时间决定桶起点(如设为 '2000-01-01 00:15' 则桶从 00:15 开始),需显式处理时区,避免 date_trunc 配算术偏移导致的错位、边界遗漏和环境不一致问题。

PostgreSQL 用 date_bin() 实现半小时分组最稳
直接用 date_trunc('hour', ts) 只能按整点切(如 08:00、09:00),没法对齐 08:30、09:30 这类偏移桶。PostgreSQL 14+ 原生支持 date_bin(),它是专为任意间隔设计的,语义清晰、时区可控、不依赖字符串或算术偏移:
-
date_bin('30 minutes'::interval, ts, '2000-01-01')—— 第三个参数是“锚点时间”,决定第一个桶起点;设为'2000-01-01 00:00',则桶就是 00:00–00:30、00:30–01:00… - 若想让桶从 00:15 开始(如 00:15–00:45),锚点改用
'2000-01-01 00:15' - 必须显式指定时区:如果
ts是TIMESTAMP WITHOUT TIME ZONE,先转成带时区再进date_bin(),例如date_bin('30 minutes', ts AT TIME ZONE 'UTC', '2000-01-01' AT TIME ZONE 'UTC')
别硬套 date_trunc() + 算术偏移,容易错位
网上常见写法:date_trunc('hour', ts) + INTERVAL '30 minutes' * (EXTRACT(MINUTE FROM ts)/30)::int,看着能跑,但有硬伤:
- 当
ts是TIMESTAMP WITHOUT TIME ZONE时,EXTRACT(MINUTE FROM ts)按服务器本地时区解释,不同部署环境结果不一致 - 跨小时边界时(如 08:59 → 09:00),整除逻辑可能漏掉最后 1 分钟,导致某条记录被分到错误桶
- 无法控制起始偏移,所有桶强制对齐到整点,做不到“从 08:15 开始每 30 分钟一桶”这种业务需求
兼容旧版本(
如果还在用 13 或更早,date_bin() 不可用,只能退而求其次用时间戳换算,但必须统一时区:
- 先确保字段是带时区的:
ts AT TIME ZONE 'UTC'(哪怕原始是TEXT或无时区TIMESTAMP,也得强制转) - 再用:
TO_TIMESTAMP(FLOOR(EXTRACT(EPOCH FROM (ts AT TIME ZONE 'UTC')) / 1800) * 1800) AT TIME ZONE 'UTC' - 1800 是 30 分钟的秒数;
FLOOR向下取整保证不跨桶;TO_TIMESTAMP转回TIMESTAMP WITH TIME ZONE,避免后续聚合时区混淆 - 注意:这个表达式不能直接用于索引,如果高频查询,建议加函数索引:
CREATE INDEX idx_ts_30min ON sensor_data (TO_TIMESTAMP(FLOOR(EXTRACT(EPOCH FROM (ts AT TIME ZONE 'UTC')) / 1800) * 1800))
GROUP BY 半小时聚合慢?先查 work_mem 是否够用
半小时桶比小时桶多一倍,分组键数量上升,哈希聚合或排序更容易落盘。执行 EXPLAIN (ANALYZE, BUFFERS) 后重点看:
- 如果
HashAggregate节点出现Disk Usage: 128kB,说明内存不足,正在写临时文件 - 临时调高:在查询前加
SET LOCAL work_mem = '32MB';,测试是否提速 - 别全局改
work_mem,尤其在线上高并发服务——每个 GROUP BY 操作都独占一份work_mem,设成 64MB 并发 20 个就吃掉 1.2GB 内存
真正卡住性能的,往往不是 SQL 写法,而是这 1MB 的内存没配对。

















