直接用 hour() 和 minute() 分组会因跨小时导致边界错乱,正确做法是用 unix_timestamp() 除以 1800 取整再转回时间,天然对齐半小时窗口。

为什么直接用 HOUR() 和 MINUTE() 会出错
很多人尝试写成 GROUP BY HOUR(time_col), FLOOR(MINUTE(time_col)/30),结果发现 14:00–14:29 和 14:30–14:59 被分到同一组——因为 FLOOR(29/30)=0,FLOOR(30/30)=1,看似合理,但一旦跨小时(比如 14:59 → 15:00),HOUR() 变了而 FLOOR(MINUTE()/30) 回到 0,导致 14:59 和 15:00 被强行拆开,实际业务中这属于同一半小时窗口的边界点,必须对齐。
正确做法:用 FROM_UNIXTIME() + FLOOR(UNIX_TIMESTAMP() / 1800)
核心思路是把时间转为秒级时间戳,除以 1800(30 分钟 = 30×60 秒),取整后还原为起始时间点。这样能天然对齐 00:00、00:30、01:00… 这类标准半小时边界。
SELECT FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(dt) / 1800) * 1800) AS half_hour, COUNT(*) FROM tbl GROUP BY half_hour;- 注意:
dt必须是DATETIME或TIMESTAMP类型;若为字符串需先用STR_TO_DATE()转换 - 该方法在 MySQL 5.6+ 全版本可用,无需窗口函数,兼容性好
- 如果表数据量大,建议给
dt字段加索引——虽然UNIX_TIMESTAMP(dt)无法走索引,但聚合前的 WHERE 条件仍可利用索引过滤
遇到时区问题怎么办:统一用 CONVERT_TZ() 预处理
MySQL 服务器时区(system_time_zone)和业务期望的统计时区(比如东八区)不一致时,UNIX_TIMESTAMP() 默认按系统时区转,会导致半小时切片偏移。例如服务器在 UTC,你却想按北京时间(UTC+8)每半小时统计,直接算会晚 8 小时。
- 先确认当前会话时区:
SELECT @@time_zone;,如果不是目标时区,执行SET time_zone = '+08:00'; - 更稳妥的方式是在计算前转换:
UNIX_TIMESTAMP(CONVERT_TZ(dt, '+00:00', '+08:00'))(假设原始数据存的是 UTC) - 避免在
GROUP BY中嵌套函数调用多次CONVERT_TZ(),可先在子查询里转换好再聚合
性能敏感场景:用生成列 + 索引加速半小时分组
当单表日增百万级记录、且高频执行半小时聚合时,每次计算 FLOOR(UNIX_TIMESTAMP()/1800) 会产生较大 CPU 开销。MySQL 5.7+ 支持生成列,可把半小时窗口值固化为虚拟列并建索引。
- 添加生成列:
ALTER TABLE tbl ADD COLUMN half_hour_slot INT AS (FLOOR(UNIX_TIMESTAMP(dt) / 1800)) STORED; - 创建索引:
CREATE INDEX idx_half ON tbl(half_hour_slot); - 后续查询直接
GROUP BY half_hour_slot,配合SELECT FROM_UNIXTIME(half_hour_slot * 1800)展示可读时间 - 注意:
STORED列会占用磁盘空间,但换来的是索引加速和查询稳定性;若只读不写频繁,VIRTUAL也可,但无法建索引
实际使用中,最常被忽略的是时区对齐和跨天边界处理——比如凌晨 00:00 的半小时窗口,必须确保它不被误判为前一天的最后一组。用时间戳整除法天然规避了日期逻辑,只要时区一步设对,后续就不用再担心“23:59 和 00:00 是否同属一个半小时”这类问题。











