必须先将时间归一化到每小时维度再分组统计:PostgreSQL用DATE_TRUNC('hour',t),SQL Server用DATEPART拼接,MySQL用DATE_FORMAT;禁用HOUR()等仅提取小时数的方式,否则丢失日期上下文;正确做法是两层查询——先按小时分组计数,再从中找出峰值。

用 DATE_TRUNC 或 DATEPART 提取小时粒度时间
直接对原始时间字段做分组统计前,必须先归一化到“每小时”维度。PostgreSQL 用 DATE_TRUNC('hour', created_at),SQL Server 用 DATEPART(YEAR, t)、DATEPART(MONTH, t)... 拼接,MySQL 则常用 DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00')。别用 GROUP BY HOUR(created_at)——它会把所有天的 14 点混在一起,失去日期上下文。
常见错误是只截取 HOUR() 或 EXTRACT(HOUR FROM ...),结果得到的是“全天各小时的总请求量”,而非“每小时窗口内的峰值”。你需要的是每个独立的小时段(如 2024-05-01 14:00:00–14:59:59),不是所有日子里的 14 点汇总。
先聚合再找峰值:避免在 GROUP BY 中嵌套窗口函数出错
不能写成 SELECT ..., MAX(COUNT(*)) OVER (PARTITION BY hour_slot)——COUNT 是聚合函数,不能直接出现在窗口函数里。正确路径是两层查询:第一层按小时分组计数,第二层对结果集用窗口函数或子查询找最大值。
典型做法:
WITH hourly_counts AS (
SELECT
DATE_TRUNC('hour', event_time) AS hour_slot,
COUNT(*) AS cnt
FROM logs
GROUP BY DATE_TRUNC('hour', event_time)
)
SELECT
hour_slot,
cnt AS peak_count,
(SELECT MIN(event_time)
FROM logs
WHERE event_time >= hour_slot AND event_time < hour_slot + INTERVAL '1 hour') AS first_hit
FROM hourly_counts
WHERE cnt = (SELECT MAX(cnt) FROM hourly_counts);
注意:这里 first_hit 只是该小时内最早一条记录的时间,不代表峰值时刻本身——因为“峰值”是数量概念,不是时间点。若真要定位“访问最密集的那分钟”或“单秒最高并发”,需进一步降粒度。
想定位“每小时内最忙的那一分钟”?得重切时间粒度
如果业务需要知道“哪一分钟扛住了最多请求”,就不能只按小时分组。要把 event_time 截到分钟级:DATE_TRUNC('minute', event_time),再统计每分钟请求数,最后按小时分组取 MAX。
关键逻辑:
- 先算出每分钟的请求量(
cnt_per_min) - 再按
DATE_TRUNC('hour', minute_slot)分组,对cnt_per_min取MAX() - 用
ROW_NUMBER() OVER (PARTITION BY hour_slot ORDER BY cnt_per_min DESC)标记每小时 Top 1 的分钟
否则你会误把“某小时共 1200 次请求”当作峰值,而实际可能是均匀分布在 60 分钟里(20/分钟),也可能是集中在第 37 分钟爆发了 800 次——后者才是运维真正关心的“时刻”。
时区处理不当会让峰值偏移整整一整天
event_time 存的是 UTC,但业务看板按本地时区(如 Asia/Shanghai)展示,若漏掉时区转换,DATE_TRUNC('hour', event_time) 会按 UTC 小时切,导致中国用户看到的“14:00 峰值”其实是北京时间 22:00。
PostgreSQL 正确写法:DATE_TRUNC('hour', event_time AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Shanghai');SQL Server 要用 AT TIME ZONE 链式转换;MySQL 依赖 CONVERT_TZ()。没做这步,所有“每小时”结论都不可信。

















