使用 DATE_FORMAT 按小时分组必须配合 GROUP BY,否则在严格模式下报错、非严格模式下统计失真;GROUP BY 表达式须与 SELECT 中完全一致;注意时区问题,UTC 存储需用 CONVERT_TZ 转换;补全空缺小时推荐应用层处理。
用
按小时分组必须加
,否则结果不可靠
MySQL 的
本身只是格式化函数,不带聚合语义。如果只写
却漏掉
,在严格模式下会报错
;非严格模式下则可能返回任意一行的
值,统计完全失真。
正确做法是把格式化结果作为分组依据:
注意:
中的表达式必须和
中的格式化写法完全一致(包括引号、空格、冒号),否则 MySQL 可能视为不同字段
如果表数据量大,建议在
字段上建索引,WHERE 条件才能有效走索引
是最常用形式,也可用
纯数字便于排序或导出,但可读性差
跨天查询时,
的时区行为容易被忽略
MySQL 默认按服务器系统时区解释时间字面量和执行
。如果你的业务数据按 UTC 存储,但服务器时区是
(UTC+8),直接用
会把 UTC 时间 +8 小时再截断——导致凌晨 00:00~00:59 的数据被归到第二天的 08:00 小时段。
解决方式取决于存储规范:
若
是
类型且存的是本地时间(如东八区),无需额外处理
若存的是 UTC 时间,应先转为本地时区再格式化:
更稳妥的做法是统一用
类型存时间,并确认
设置与业务一致,避免隐式转换
想补全空缺小时(比如某小时无数据就不显示),纯 SQL 很难做到
MySQL 原生不支持生成连续时间序列,
只会返回有数据的小时段。如果报表要求每小时都有一行(无数据填 0),不能靠
一个不存在的“小时表”来硬凑。
可行路径只有两个:
MySQL(Linux)
MySQL 9.6.0是面向Linux平台的2026年创新版本,核心架构迎来重大革新。其将外键约束与级联操作从InnoDB引擎层上移至SQL层,确保所有数据变更均被完整记录至Binlog,彻底解决了CDC(变更数据捕获)与主从复制中的数据不一致难题。此外,该版本引入container_aware启动选项以原生适配容器环境,并对审计日志进行了组件化重构,为追求极致数据一致性与云原生体验的开发者提供了全新选择。
下载
应用层补全:查出有数据的小时段后,在代码里遍历起止时间,用 map 补 0 值(推荐,逻辑清晰、可控)
用递归 CTE(MySQL 8.0+)生成小时序列再左连,但性能差、语句长、维护成本高,例如:
实际项目中,除非强依赖纯 SQL 输出,否则别这么写。
性能不如直接用日期函数截断
对百万级以上表,
是字符串操作,无法利用索引;而用算术方式截断到小时(如
)虽然可读性差,但部分场景下优化器能更好下推条件。
不过更现实的优化点是:
优先确保 WHERE 条件中的时间范围足够精确,让索引生效
避免在
或
中对时间字段做函数运算,例如
会强制全表扫描
真正需要高性能小时统计时,考虑预计算汇总表,按小时写入计数,查询直接查汇总表
格式化本身不是瓶颈,但组合不当会让整个查询退化。关键还是看时间条件怎么写、索引是否存在、以及是否误把格式化当成了分组或过滤工具。
DATE_FORMATGROUP BYDATE_FORMATSELECT DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00') AS hour_slot, COUNT(*)GROUP BYERROR 1055create_timeSELECT
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 BYSELECTcreate_time'%Y-%m-%d %H:00:00''%Y%m%d%H'DATE_FORMATDATE_FORMATAsia/ShanghaiDATE_FORMAT(create_time, '%Y-%m-%d %H:00:00')create_timeDATETIMEDATE_FORMAT(CONVERT_TZ(create_time, '+00:00', '+08:00'), '%Y-%m-%d %H:00:00')TIMESTAMPtime_zoneDATE_FORMAT + GROUP BYLEFT JOINWITH 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-02 23:00:00'
)
SELECT
h.h AS hour_slot,
COALESCE(t.cnt, 0) AS cnt
FROM hours h
LEFT JOIN (
SELECT DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00') AS hour_slot, COUNT(*) AS cnt
FROM orders
WHERE create_time >= '2024-06-01' AND create_time < '2024-06-03'
GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00')
) t ON h.h = t.hour_slot;DATE_FORMATDATE_FORMAT(create_time, '%Y-%m-%d %H:00:00')FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(create_time)/3600)*3600)WHEREGROUP BYWHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-06-01'