使用 date_format 按小时分组必须配合 group by,否则在严格模式下报错、非严格模式下统计失真;group by 表达式须与 select 中完全一致;注意时区问题,utc 存储需用 convert_tz 转换;补全空缺小时推荐应用层处理。

用 DATE_FORMAT 按小时分组必须加 GROUP BY,否则结果不可靠
MySQL 的 DATE_FORMAT 本身只是格式化函数,不带聚合语义。如果只写 SELECT DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00') AS hour_slot, COUNT(*) 却漏掉 GROUP BY,在严格模式下会报错 ERROR 1055;非严格模式下则可能返回任意一行的 create_time 值,统计完全失真。
正确做法是把格式化结果作为分组依据:
SELECT DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00') AS hour_slot, COUNT(*) AS cnt FROM orders WHERE create_time BETWEEN '2024-06-01 00:00:00' AND '2024-06-02 23:59:59' GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00') ORDER BY hour_slot;
- 注意:
GROUP BY中的表达式必须和SELECT中的格式化写法完全一致(包括引号、空格、冒号),否则 MySQL 可能视为不同字段 - 如果表数据量大,建议在
create_time字段上建索引,WHERE 条件才能有效走索引 -
'%Y-%m-%d %H:00:00'是最常用形式,也可用'%Y%m%d%H'纯数字便于排序或导出,但可读性差
跨天查询时,DATE_FORMAT 的时区行为容易被忽略
MySQL 默认按服务器系统时区解释时间字面量和执行 DATE_FORMAT。如果你的业务数据按 UTC 存储,但服务器时区是 Asia/Shanghai(UTC+8),直接用 DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00') 会把 UTC 时间 +8 小时再截断——导致凌晨 00:00~00:59 的数据被归到第二天的 08:00 小时段。
解决方式取决于存储规范:
- 若
create_time是DATETIME类型且存的是本地时间(如东八区),无需额外处理 - 若存的是 UTC 时间,应先转为本地时区再格式化:
DATE_FORMAT(CONVERT_TZ(create_time, '+00:00', '+08:00'), '%Y-%m-%d %H:00:00') - 更稳妥的做法是统一用
TIMESTAMP类型存时间,并确认time_zone设置与业务一致,避免隐式转换
想补全空缺小时(比如某小时无数据就不显示),纯 SQL 很难做到
MySQL 原生不支持生成连续时间序列,DATE_FORMAT + GROUP BY 只会返回有数据的小时段。如果报表要求每小时都有一行(无数据填 0),不能靠 LEFT JOIN 一个不存在的“小时表”来硬凑。
可行路径只有两个:
- 应用层补全:查出有数据的小时段后,在代码里遍历起止时间,用 map 补 0 值(推荐,逻辑清晰、可控)
- 用递归 CTE(MySQL 8.0+)生成小时序列再左连,但性能差、语句长、维护成本高,例如:
WITH RECURSIVE hours AS ( SELECT '2024-06-01 00:00:00' AS h UNION ALL SELECT DATE_ADD(h, INTERVAL 1 HOUR) FROM hours WHERE h = '2024-06-01' AND create_time <p>实际项目中,除非强依赖纯 SQL 输出,否则别这么写。</p> <h3> <code>DATE_FORMAT</code> 性能不如直接用日期函数截断</h3> <p>对百万级以上表,<code>DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00')</code> 是字符串操作,无法利用索引;而用算术方式截断到小时(如 <code>FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(create_time)/3600)*3600)</code>)虽然可读性差,但部分场景下优化器能更好下推条件。</p> <p>不过更现实的优化点是:</p>
- 优先确保 WHERE 条件中的时间范围足够精确,让索引生效
- 避免在
WHERE或GROUP BY中对时间字段做函数运算,例如WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-06-01'会强制全表扫描 - 真正需要高性能小时统计时,考虑预计算汇总表,按小时写入计数,查询直接查汇总表
格式化本身不是瓶颈,但组合不当会让整个查询退化。关键还是看时间条件怎么写、索引是否存在、以及是否误把格式化当成了分组或过滤工具。











