HOUR()仅提取小时数导致跨日同小时被合并,需用FLOOR(UNIX_TIMESTAMP(ts)/3600)*3600截断至整点实现自然小时分组。

为什么直接用 HOUR() 无法正确分组小时段
很多用户发现用 HOUR(created_at) 分组后,数据全堆在 0–23 点,丢失了日期维度——这其实不是函数错了,而是它只取小时数,把“今天 14:00”和“昨天 14:00”全算成同一个组。真正需要的是“每小时一个独立时间桶”,比如 2024-06-10 14:00:00 到 2024-06-10 14:59:59 算作一个时段。
关键在于:必须先对时间戳做截断(floor),再还原成时间类型,而不是只取小时数字。
-
HOUR()是纯提取函数,不改变时间粒度 - 要实现“按自然小时段聚合”,得把原始时间向下取整到最近的整点,例如
2024-06-10 14:37:22→2024-06-10 14:00:00 - MySQL 没有内置的
DATE_FLOOR,所以得靠UNIX_TIMESTAMP+FLOOR+FROM_UNIXTIME组合实现
用 FLOOR(UNIX_TIMESTAMP(ts) / 3600) * 3600 截断到小时
这是最常用也最可靠的方式:把时间转成秒级时间戳,除以 3600(一小时秒数),用 FLOOR 向下取整,再乘回去,就得到该小时起始时刻的时间戳。
SELECT FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(created_at) / 3600) * 3600) AS hour_start, COUNT(*) AS pv FROM access_log WHERE created_at >= '2024-06-10' AND created_at < '2024-06-11' GROUP BY FLOOR(UNIX_TIMESTAMP(created_at) / 3600) ORDER BY hour_start;
- 注意:
GROUP BY必须用计算表达式本身(不是别名),否则 MySQL 5.7+ 严格模式会报错 -
FROM_UNIXTIME(...)只用于展示,不影响分组逻辑;显示时带秒数(如14:00:00)更直观 - 如果字段是
DATETIME或TIMESTAMP类型,UNIX_TIMESTAMP()能正确处理;但如果是字符串,需先用STR_TO_DATE()转换,否则返回0
时区问题会让 UNIX_TIMESTAMP 结果偏移
MySQL 的 UNIX_TIMESTAMP() 默认按服务器时区解释输入时间。如果你的 created_at 存的是 UTC 时间,但 MySQL server 在东八区,那 UNIX_TIMESTAMP(created_at) 会多减 8 小时,导致截断错误。
- 查当前时区:
SELECT @@time_zone;,常见值是SYSTEM(继承系统)或+08:00 - 安全做法是显式转换:
UNIX_TIMESTAMP(CONVERT_TZ(created_at, '+00:00', '+08:00'))(假设原始是 UTC,要按东八区小时分组) - 或者统一存为本地时间,并确保应用写入和查询时区一致,避免隐式转换
- 测试小技巧:挑一条
created_at = '2024-06-10 14:15:30'的记录,手动算UNIX_TIMESTAMP值,再除以 3600 看是否符合预期
替代方案:用 DATE_FORMAT(ts, '%Y-%m-%d %H:00:00') 更直观?
这个写法看起来简洁,但有隐患:当 ts 是 NULL 时,DATE_FORMAT 返回 NULL,而 FLOOR(UNIX_TIMESTAMP(NULL)) 返回 NULL,两者行为一致;但真正的问题在于精度。
-
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00')本质是字符串拼接,不能用于时间运算,且在某些 MySQL 版本中可能因 locale 设置影响格式(比如月份缩写) - 它无法支持跨时区动态调整,而
UNIX_TIMESTAMP方案只要输入正确时区,结果就是确定的秒数 - 性能上差异不大,但可维护性差——别人一眼看不出你在做“向下取整”,容易误改成
%i(分钟)导致逻辑崩坏
真正复杂的地方不在公式本身,而在于你是否清楚数据里的时间到底代表什么时区、有没有被前端或 ORM 自动转换过。没确认这点,再标准的 FLOOR + UNIX_TIMESTAMP 也会统计错。

















