必须将时间归一化到「年-月-日-小时」粒度再分组,mysql用date_format(created_at, '%y-%m-%d %h:00:00'),postgresql用date_trunc('hour', created_at),sql server用datefromparts构造整点时间,并补全空缺小时。

直接用 HOUR() 或 EXTRACT(HOUR FROM ) 分组会丢日期上下文,统计结果不可信。必须把时间归一化到「年-月-日-小时」粒度再分组。
MySQL 用 DATE_FORMAT() 截断到小时精度
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') 是最稳妥的做法,它生成字符串形式的整点时间戳,能正确区分不同日期的同一小时(比如 2026-07-20 14:00:00 和 2026-07-21 14:00:00)。
- 别用
HOUR(created_at)单独分组——它只返回 0–23 的数字,所有天的 14 点全混在一起 - 字段类型必须是
DATETIME或TIMESTAMP;如果是字符串,先用STR_TO_DATE(log_time, '%Y-%m-%d %H:%i:%s')转换,再格式化 -
WHERE created_at IS NOT NULL必须加,否则DATE_FORMAT(NULL, ...)返回NULL,所有空时间被归为一组,虚高统计值 - 示例:
SELECT DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') AS hour_slot, COUNT(*) AS req_count FROM api_logs WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) AND created_at IS NOT NULL GROUP BY DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') ORDER BY hour_slot;
PostgreSQL 用 DATE_TRUNC('hour', created_at)
DATE_TRUNC('hour', created_at) 直接返回 TIMESTAMP 类型的整点时间(如 '2026-07-21 12:00:00'),比字符串更利于后续计算和索引下推。
- 单位必须是单数
'hour',写成'hours'会报错invalid value for type interval - 如果
created_at是TIMESTAMPTZ,注意时区:先用AT TIME ZONE 'Asia/Shanghai'统一转换,再截断 - 不能用
EXTRACT(HOUR FROM created_at)替代——它只取小时数,丢失日期,无法支撑跨天分析 - 示例:
SELECT DATE_TRUNC('hour', created_at) AS hour_slot, COUNT(*) AS req_count FROM api_logs WHERE created_at >= NOW() - INTERVAL '24 hours' GROUP BY DATE_TRUNC('hour', created_at) ORDER BY hour_slot;
SQL Server 用 DATEPART 拼接构造小时槽
SQL Server 没有原生小时截断函数,得靠 DATEPART 提取年月日小时,再拼成标准时间字符串或 datetime 值。
- 推荐用
DATEFROMPARTS(YEAR(t), MONTH(t), DAY(t), DATEPART(HOUR, t), 0, 0)构造整点时间,类型是DATETIME,可参与范围查询 - 避免用
FORMAT(t, 'yyyy-MM-dd HH:00')——性能差,大数据量下易成瓶颈 -
DATEPART(HOUR, created_at)只能用于简单日报表;做滚动窗口或补全空缺小时时,必须带日期部分 - 示例(构造整点时间):
SELECT DATEFROMPARTS( YEAR(created_at), MONTH(created_at), DAY(created_at), DATEPART(HOUR, created_at), 0, 0 ) AS hour_slot, COUNT(*) AS req_count FROM api_logs WHERE created_at >= DATEADD(hour, -24, GETDATE()) GROUP BY DATEFROMPARTS( YEAR(created_at), MONTH(created_at), DAY(created_at), DATEPART(HOUR, created_at), 0, 0 ) ORDER BY hour_slot;
补全空缺小时(避免漏掉零请求时段)
原始数据里没有请求的小时不会出现在结果中,但监控场景常需完整 24 小时序列。- MySQL 不支持
GENERATE_SERIES,得靠数字表或递归 CTE 补全,成本高;生产环境建议在应用层补零 - PostgreSQL 可直接用
GENERATE_SERIES(NOW() - INTERVAL '24 hours', NOW(), '1 hour')生成时间序列,再LEFT JOIN原始聚合结果 - SQL Server 可用
master..spt_values或递归 CTE 生成小时序列,但要注意JOIN条件必须严格对齐:左表小时槽 ≤ 请求时间 - 关键点:补全不是为了“好看”,而是让下游告警、图表渲染不因缺失行出错
真正卡住效果的,往往不是语法本身,而是没意识到 HOUR() 丢日期、NULL 时间污染分组、以及补全逻辑在不同数据库里的实现成本差异。











