直接用 UNIX_TIMESTAMP() 取模会错乱时间对齐,因为取模基于 Unix epoch 起点的秒数余数,而非分钟起点;正确做法是先用 FLOOR(UNIX_TIMESTAMP(ts)/60) 截断到分钟粒度再分组。

为什么直接用 UNIX_TIMESTAMP() 取模会错乱时间对齐?
很多人一上来就写 GROUP BY UNIX_TIMESTAMP(time_col) % 60,结果发现每组数据的时间戳不是整分钟(比如落在 12:03:27 而不是 12:03:00),且跨天或跨小时时分组完全错位。这是因为取模只看“秒数余数”,不考虑时间起点——UNIX_TIMESTAMP('2024-01-01 00:00:00') 是 1704067200,而 1704067200 % 60 = 0,但 1704067227 % 60 = 27,这 27 秒是相对于 Unix epoch 起点的偏移,不是你想要的“每分钟从 :00 开始”。
正确做法是先“截断”到分钟粒度,再分组:
- 用
FLOOR(UNIX_TIMESTAMP(time_col) / 60)把时间向下取整到最近的分钟起点(单位:分钟数) - 再乘以 60 恢复成秒级时间戳,或直接用该值分组
- 配合
FROM_UNIXTIME()把分组键转回可读时间
MySQL 中按分钟降采样的标准写法
假设表叫 metrics,时间字段为 ts(类型 DATETIME 或 TIMESTAMP),要取每分钟的平均值:
SELECT FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(ts) / 60) * 60) AS minute, AVG(value) AS avg_value, COUNT(*) AS sample_count FROM metrics WHERE ts >= '2024-05-01 00:00:00' GROUP BY FLOOR(UNIX_TIMESTAMP(ts) / 60) ORDER BY minute;
注意:FLOOR(UNIX_TIMESTAMP(ts) / 60) 是分组依据,不能用别名 minute 替代(MySQL 不支持在 GROUP BY 中直接引用 SELECT 别名)。
- 如果只要最近 1 小时数据,
WHERE条件用ts >= NOW() - INTERVAL 1 HOUR更安全(避免硬编码) -
FLOOR()比TRUNCATE()更稳妥,因为UNIX_TIMESTAMP()返回的是DOUBLE,浮点误差下TRUNCATE(x, 0)可能截错 - 索引有效性:若
ts有索引,这个写法仍能走范围扫描,但UNIX_TIMESTAMP(ts)表达式本身无法直接利用索引——所以务必把过滤条件(如WHERE ts >= ...)写在最外层
PostgreSQL 怎么做?别碰 EXTRACT(EPOCH FROM ...) 取模
PostgreSQL 没有 UNIX_TIMESTAMP(),但常见错误是写 EXTRACT(EPOCH FROM ts)::int % 60 —— 同样错在没对齐到分钟边界。
正确方式是用时间截断函数:
SELECT
DATE_TRUNC('minute', ts) AS minute,
AVG(value) AS avg_value
FROM metrics
WHERE ts >= NOW() - INTERVAL '1 hour'
GROUP BY DATE_TRUNC('minute', ts)
ORDER BY minute;-
DATE_TRUNC('minute', ts)直接返回TIMESTAMP类型的整分钟时间(如'2024-05-01 12:03:00'),语义清晰、性能好、天然可索引 - 如果必须用秒级数值做分组(例如对接下游要求整数时间戳),可用
EXTRACT(EPOCH FROM DATE_TRUNC('minute', ts))::bigint - 注意:PostgreSQL 的
DATE_TRUNC对TIMESTAMPTZ会按当前时区截断,若需 UTC 对齐,先ts AT TIME ZONE 'UTC'
时区和夏令时会导致什么坑?
如果你的 ts 存的是本地时间(比如 '2024-03-10 02:15:00' 在美国东部时间 DST 切换当天),直接 DATE_TRUNC('minute', ts) 或 UNIX_TIMESTAMP() 都可能因系统时区设置不同而产出歧义结果。
- 最佳实践:存储统一用 UTC 时间(
TIMESTAMP WITHOUT TIME ZONE或TIMESTAMP WITH TIME ZONE并设为 UTC),所有降采样逻辑在 UTC 下进行 - MySQL 中确保
time_zone系统变量不是SYSTEM(它会随系统走),查SELECT @@time_zone,建议设为'+00:00' - 哪怕只是展示,也别在 SQL 里用
CONVERT_TZ()做时区转换后再截断——先截断再转,否则02:59 → 03:00的跳跃会让一分钟的数据被拆到两组
真正麻烦的不是语法,而是时间字段的物理含义是否明确:它记录的是采集时刻的本地时间?服务端收到时间?还是设备上报的原始时间?这些决定了你该在哪一层做对齐。


















