正确做法是用FLOOR(UNIX_TIMESTAMP(ts)/60)截断到分钟起点,而非直接取模;MySQL需避免GROUP BY别名,PostgreSQL应优先用DATE_TRUNC('minute', ts)并显式处理时区。

GROUP BY 分钟对齐必须先截断,不能直接取模
直接写 GROUP BY UNIX_TIMESTAMP(ts) % 60 看似简洁,但结果完全错位:你得到的不是“每分钟从 :00 开始”的组,而是按 Unix epoch 起点(1970-01-01 00:00:00)秒数余数分组。比如 UNIX_TIMESTAMP('2026-06-06 10:03:27') 是 1749204207,1749204207 % 60 = 27,它被分进“27 秒组”,而非“10:03:00 组”。这会导致同一分钟的数据散落在多个组,跨小时/跨天时更混乱。
MySQL 正确写法:用 FLOOR(UNIX_TIMESTAMP() / 60) 截断到分钟起点
核心是把时间“向下取整”到最近的整分钟(单位:秒),再转回可读格式:
SELECT FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(ts) / 60) * 60) AS minute, AVG(value) AS avg_value, COUNT(*) AS sample_count FROM sensor_data WHERE ts >= NOW() - INTERVAL '30 minutes' GROUP BY FLOOR(UNIX_TIMESTAMP(ts) / 60) ORDER BY minute;
-
FLOOR(UNIX_TIMESTAMP(ts) / 60)返回该时间所属的“第多少个分钟”(从 epoch 起算),是稳定分组键 - 不能在
GROUP BY中用别名minute,MySQL 不支持 -
WHERE条件必须直接作用于原始ts字段,否则索引失效;NOW() - INTERVAL '30 minutes'比硬编码时间更安全 - 若
ts是TIMESTAMP类型,存储为 UTC,FROM_UNIXTIME()默认按系统时区输出——跨时区部署时务必用CONVERT_TZ()显式转换
PostgreSQL 应优先用 DATE_TRUNC('minute', ts)
它语义明确、时区安全、且能下推优化,比手动算 epoch 更可靠:
SELECT
DATE_TRUNC('minute', ts AT TIME ZONE 'Asia/Shanghai') AS minute,
AVG(value) AS avg_value,
COUNT(*) AS sample_count
FROM sensor_data
WHERE ts >= NOW() AT TIME ZONE 'Asia/Shanghai' - INTERVAL '30 minutes'
GROUP BY DATE_TRUNC('minute', ts AT TIME ZONE 'Asia/Shanghai')
ORDER BY minute;- 若原始列是
TIMESTAMP WITHOUT TIME ZONE,必须显式指定时区,否则按数据库默认时区解释,极易漂移 -
DATE_TRUNC自动处理夏令时切换,而FLOOR(EXTRACT(EPOCH FROM ts)/60)*60会跳过或重复某分钟 - 避免在
WHERE中写DATE_TRUNC('minute', ts) = '2026-06-06 10:00:00'——这会让索引失效;改用范围条件:ts >= '2026-06-06 10:00:00' AND ts
真正容易被忽略的是时区和索引双重陷阱
一个 GROUP BY 语句跑得慢或结果不准,90% 不是因为语法写错,而是这两点没对齐:一是 ts 列类型(DATETIME vs TIMESTAMP)与时区上下文不一致;二是过滤条件用了函数包裹 ts,导致 B-tree 索引完全失效。哪怕加了索引,只要 WHERE 里出现 FROM_UNIXTIME(ts) 或 DATE_TRUNC(ts),查询就只能全表扫描。先确保 WHERE ts >= ? AND ts 这类纯范围条件存在,再做分组,才是可落地的降采样方案。

















