MySQL中用HOUR()分组会混淆不同日期的同小时数据,必须用DATE_FORMAT(时间字段,'%Y-%m-%d %H:00:00')截断到小时精度再分组;PostgreSQL可用GENERATE_SERIES补全空缺小时;ClickHouse须用toStartOfHour确保索引下推;时区不一致是常见漏统计根源。

MySQL中用DATE_SUB和HOUR分组统计每小时数据
直接用 WHERE 过滤时间范围 + GROUP BY HOUR(时间字段) 会出错:它只取小时数(0–23),不区分日期,导致昨天和今天同小时的数据被合并。必须把日期和小时一起归一化成小时级时间戳或字符串。
推荐做法是用 DATE_FORMAT(时间字段, '%Y-%m-%d %H:00:00') 截断到小时精度,再分组:
SELECT DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') AS hour_slot, COUNT(*) AS cnt FROM logs WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY hour_slot ORDER BY hour_slot;
-
DATE_SUB(NOW(), INTERVAL 24 HOUR)是动态起点,注意时区要和created_at字段一致(比如都是 UTC 或都用系统时区) - 如果
created_at是INT时间戳(秒级),得先转成 datetime:FROM_UNIXTIME(created_at) - 结果可能缺某些小时(比如某小时没数据),需要外部补全,SQL 本身不自动填充空档
PostgreSQL里用GENERATE_SERIES补全缺失小时
PostgreSQL 的优势在于能用 GENERATE_SERIES 主动生成过去24个整点时间,再左连接原始数据,确保每小时都有记录(哪怕 count 是 0)。
关键点是把原始时间对齐到整点(向下取整到小时):
SELECT
h.hour_slot,
COALESCE(t.cnt, 0) AS cnt
FROM GENERATE_SERIES(
NOW() - INTERVAL '24 hours',
NOW(),
'1 hour'
) AS h(hour_slot)
LEFT JOIN (
SELECT
DATE_TRUNC('hour', created_at) AS hour_slot,
COUNT(*) AS cnt
FROM logs
WHERE created_at >= NOW() - INTERVAL '24 hours'
GROUP BY DATE_TRUNC('hour', created_at)
) AS t USING (hour_slot)
ORDER BY h.hour_slot;-
DATE_TRUNC('hour', created_at)比EXTRACT(HOUR FROM ...)更安全,它保留日期部分 -
GENERATE_SERIES的终点用NOW()会导致最后一小时包含“当前未结束的小时”,如需严格 24 个完整小时,终点应设为NOW() - INTERVAL '1 hour' - 若表很大,
WHERE条件务必确保created_at字段上有索引,否则扫描全表很慢
ClickHouse中避免使用toStartOfHour以外的函数做分组
ClickHouse 对时间函数优化极强,但乱用会破坏索引下推。必须用 toStartOfHour(created_at) —— 它是原生支持、可下推到存储层的函数;而 formatDateTime(created_at, '%Y-%m-%d %H:00:00') 或 toHour(created_at) 都不行。
-
toStartOfHour返回的是 DateTime 类型,值为该小时的第一秒(如2024-05-20 14:00:00),天然适合分组和排序 - 过滤条件写成
created_at >= toStartOfHour(now() - INTERVAL 24 HOUR),比用字符串或时间戳更高效 - 如果分区键含日期(比如按
toDate(created_at)分区),这个写法还能精准命中相关分区
注意时区和字段类型不匹配导致的漏统计
最常被忽略的坑是:数据库服务器时区、应用写入时区、查询时区三者不一致。比如日志写入用 UTC,但 NOW() 返回的是本地时区时间,DATE_SUB(NOW(), INTERVAL 24 HOUR) 就可能漏掉或重复计算几个小时的数据。
- 查当前会话时区:
SELECT @@time_zone(MySQL)、SHOW TIME ZONE(PostgreSQL) - 统一用 UTC 处理最稳妥:写入时存 UTC 时间,查询时也用 UTC 起点,例如 MySQL 中
WHERE created_at >= CONVERT_TZ(NOW(), @@session.time_zone, '+00:00') - INTERVAL 24 HOUR - 如果
created_at是TIMESTAMP类型(自动时区转换),而你用的是DATETIME(无时区),那根本不能直接比——类型不一致时 MySQL 可能隐式转换出错
小时级统计看着简单,实际卡在时区、截断精度、空档处理这三点上。跑通一条 SQL 不难,让结果稳定准确才真正考验对时间和类型的理解。

















