正确做法是先将时间“归零”到周期边界再分组,PostgreSQL推荐用DATE_TRUNC(语义清晰、时区安全、可下推优化),MySQL可用FLOOR(UNIX_TIMESTAMP(ts)/3600)*3600配合CONVERT_TZ;避免在WHERE中对时间字段用函数导致索引失效。

用 EXTRACT 或 FLOOR 对时间戳做周期对齐分组
直接对 timestamp 字段取模(如 EXTRACT(EPOCH FROM ts) % 3600)无法稳定划分自然小时——因为模运算结果依赖起始偏移,不同天的同一小时会散落到不同组。正确做法是先将时间“归零”到周期边界,再分组。例如按小时降采样,应统一截断到每小时的开始时刻:FLOOR(EXTRACT(EPOCH FROM ts) / 3600) * 3600,再转回 TIMESTAMP;PostgreSQL 更推荐用 DATE_TRUNC('hour', ts),它语义清晰、时区安全、且能下推优化。
PostgreSQL 中 DATE_TRUNC 是最稳的选择
DATE_TRUNC 不仅支持 'minute'、'hour'、'day' 等标准粒度,还处理夏令时、闰秒和时区转换。比如 DATE_TRUNC('hour', ts AT TIME ZONE 'Asia/Shanghai') 会真实按北京时间整点切分,而不是简单丢弃分钟。注意:若原始列是 TIMESTAMP WITHOUT TIME ZONE,必须显式指定时区,否则按数据库默认时区解释,容易导致跨时区查询结果漂移。
MySQL 8.0+ 可用 DATE_SUB + INTERVAL 模拟对齐
MySQL 没有原生 DATE_TRUNC,但可用 DATE_SUB(ts, INTERVAL SECOND(ts) SECOND) 清除秒级,再逐级清除分钟或小时。更可靠的是用 FLOOR(UNIX_TIMESTAMP(ts) / 3600) * 3600 转为整点 Unix 时间,再用 FROM_UNIXTIME() 转回。但要注意:该方式忽略时区,若 ts 是 DATETIME 类型(无时区),结果恒为系统时区;若为 TIMESTAMP 类型,则已隐式转为 UTC 存储,FROM_UNIXTIME() 默认按系统时区输出,可能造成一小时偏差。务必统一使用 CONVERT_TZ() 显式转换。
避免在 WHERE 中对时间字段用函数导致索引失效
常见错误是写 WHERE DATE_TRUNC('hour', ts) = '2024-01-01 10:00:00'——这会让数据库无法使用 ts 上的 B-tree 索引。正确写法是改用范围查询:WHERE ts >= '2024-01-01 10:00:00' AND ts 。如果必须按降采样后的时间过滤,先在子查询或 CTE 中完成分组,再对外层结果筛选。另外,高频写入场景下,按小时分组的聚合结果建议物化为汇总表,并用 <code>ON CONFLICT(PG)或 INSERT ... ON DUPLICATE KEY UPDATE(MySQL)做增量更新,避免每次全量重算。
真正难的不是写出第一行 GROUP BY DATE_TRUNC('hour', ts),而是确认你的时区上下文是否一致、索引是否被绕过、以及凌晨跨天时夏令时切换会不会让某个小时空或重复——这些细节往往在压测或上线后才暴露。

















