用HOUR()直接分组会丢失日期维度导致数据错乱,正确做法是按“年-月-日-小时”截断后再分组,MySQL推荐DATE_FORMAT,PostgreSQL用date_trunc,ClickHouse用toStartOfHour,并注意时区与索引优化。

MySQL里用 HOUR() 提取小时再分组会丢数据
直接对 datetime 字段用 HOUR(created_at) 分组,看似简单,但实际会把不同日期的同一小时(比如 1月1日 14点 和 1月2日 14点)强行合并,统计结果完全失真。这不是分组,是错乱聚合。
正确做法是先截断到「年-月-日-小时」粒度,再分组。MySQL 8.0+ 推荐用 DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00');5.7 及更早可用 STR_TO_DATE(DATE_FORMAT(created_at, '%Y-%m-%d %H'), '%Y-%m-%d %H'),确保每条记录落到它真实的小时槽位。
- 别用
HOUR()单独分组,除非你明确只要全天各小时的总频次(忽略日期) - 时间字段为
timestamp时注意时区——created_at存的是 UTC,但DATE_FORMAT按会话时区解析,建议统一用CONVERT_TZ()转成目标时区后再格式化 - 如果表数据量大,记得给
created_at加索引;仅对函数结果建索引不生效,可考虑生成列 + 索引(MySQL 5.7+):ALTER TABLE logs ADD COLUMN hour_slot VARCHAR(13) STORED AS (DATE_FORMAT(created_at, '%Y-%m-%d %H'));
PostgreSQL 中用 date_trunc('hour', created_at) 最稳妥
PG 原生支持按时间单位截断,date_trunc('hour', created_at) 直接返回该小时的起始时间戳(如 2024-06-15 14:00:00+08),语义清晰、无时区歧义、且能走索引(配合 created_at 的 B-tree 索引)。
常见错误是写成 EXTRACT(HOUR FROM created_at) —— 这和 MySQL 的 HOUR() 一样,只取小时数字,丢失日期维度。
- 需要补零填充显示(如
14:00)?用TO_CHAR(date_trunc('hour', created_at), 'YYYY-MM-DD HH24:MI') - 想按本地时区分组?先用
created_at AT TIME ZONE 'Asia/Shanghai'转换,再date_trunc - 如果
created_at是text类型,必须先::timestamptz强转,否则date_trunc报错function date_trunc(unknown, text) does not exist
ClickHouse 的 toStartOfHour() 性能比字符串格式化高得多
在 ClickHouse 里拼 toString(toYear(ts)) + '-' + toString(toMonth(ts)) + '-' + ... 不仅难写,还会让查询无法利用跳数索引(skip index)。原生函数 toStartOfHour(created_at) 返回 DateTime 类型,能命中索引,执行快 3–5 倍。
注意:ClickHouse 默认时区是服务器本地时区,如果写入时没指定时区,toStartOfHour() 结果可能和业务预期不符。安全做法是显式声明:toStartOfHour(created_at, 'Asia/Shanghai')。
- 字段类型是
UInt32(秒级时间戳)?先转:toStartOfHour(toDateTime(created_at)) - 需要导出为字符串用于 BI 工具?用
formatDateTime(toStartOfHour(created_at), '%Y-%m-%d %H:00:00'),别在 WHERE 或 GROUP BY 里做格式化 - 如果统计的是最近 1 小时的实时调用量,注意
toStartOfHour(now())和toStartOfHour(now() - INTERVAL 1 HOUR)的边界——后者覆盖的是整点到整点,不是自然小时
跨天高峰时段容易漏掉 00:00–00:59 这一分钟
很多接口监控埋点在凌晨写入有延迟,或日志采集任务在 00:00 触发导致部分记录落进下一个小时分组。单纯按小时聚合会把这部分“迟到”的请求算到第二天 00 点,造成前一日流量虚低、当日虚高。
解决方法不是修数据,而是定义清楚 SLA:比如接受 5 分钟延迟,那统计逻辑就该查 WHERE created_at >= '2024-06-15 00:00:00' AND created_at ,然后用 toStartOfHour() 或 date_trunc 分组——这样 00:00–00:04 的延迟数据仍归入 00 点槽位。
- 报警阈值若依赖小时统计,务必确认下游系统是否也采用相同延迟容忍窗口
- ClickHouse 的
ReplacingMergeTree表可以靠version字段覆盖重写迟到数据,但前提是你的写入逻辑能识别并打上正确版本号 - 别指望数据库自动“智能修正”时间,分组逻辑必须和数据写入、采集、时区转换链条对齐










