应使用百分比变化率计算环比增长率,通过lag()获取上一时段计数,用nullif()避免除零,round()控制精度,并在cte中完成计算后外层过滤;需注意时区一致性、毛刺干扰及源头定位。

用 LAG() 和 ROUND() 计算环比增长率
直接比较当前小时交易量和上一小时的差值容易受基数影响,比如从 10 笔涨到 20 笔(+100%)和从 1000 笔涨到 1010 笔(+1%)应区别对待。必须用百分比变化率,并处理除零问题。
- 先按时间窗口(如每小时)聚合交易数:
GROUP BY DATE_TRUNC('hour', created_at)(PostgreSQL)或DATEADD(hour, DATEDIFF(hour, 0, created_at), 0)(SQL Server) - 用
LAG(count) OVER (ORDER BY hour_ts)拿上一时段计数,别漏掉ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW——默认框架不影响LAG,但显式写清更稳妥 - 计算增长率时包裹
NULLIF(prev_count, 0)避免除零错误;再套一层ROUND(..., 2)控制小数位,否则浮点误差可能导致 99.9999999% 被误判
设阈值并过滤出激增点用 FILTER 或子查询
窗口函数不能直接在 WHERE 中使用,所以不能写 WHERE growth_rate > 300。必须把窗口计算结果包进子查询或 CTE,再外层过滤。
- 推荐 CTE 写法:先
WITH hourly AS (...), with_growth AS (SELECT ..., ROUND((curr - LAG(curr) OVER (...)) / NULLIF(LAG(curr) OVER (...), 0), 2) AS growth_rate FROM hourly),再SELECT * FROM with_growth WHERE growth_rate > 3.0 - MySQL 8.0+ 支持
FILTER,但仅限聚合函数,不适用于LAG这类标量窗口函数,别误用 - 注意时区一致性:如果
created_at是TIMESTAMP WITHOUT TIME ZONE,DATE_TRUNC可能按数据库本地时区截断,导致跨日异常被漏检
排除毛刺干扰:加滑动窗口平滑或最小持续时间约束
单个小时暴涨可能是正常促销或爬虫触发,真异常通常持续至少两小时。单纯看单点增长率会误报。
- 用
COUNT(*) OVER (ORDER BY hour_ts ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)统计连续小时数,配合growth_rate > 3.0找出“当前小时和前一小时都超阈值”的组合 - 或者改用 3 小时滑动平均:
AVG(count) OVER (ORDER BY hour_ts ROWS BETWEEN 2 PRECEDING AND CURRENT ROW),再比当前值是否超过均值的 3 倍——这比固定环比更能抗短时毛刺 - 避免用
RANK()或DENSE_RANK()替代时间排序:它们按值排序,会导致“高交易量小时”被集中排前面,破坏时间序列逻辑
查出来之后怎么定位源头?补 STRING_AGG() 或关联明细
只看到“某小时增长 420%”没用,得知道是哪些用户、商户或 IP 在驱动。窗口函数本身不支持聚合字符串,但可以结合其他聚合函数一起输出上下文。
- 在小时聚合层加
STRING_AGG(DISTINCT user_id, ',') FILTER (WHERE count > 10) AS big_users(PostgreSQL),快速抓出高频用户 - 若需完整明细,别在窗口查询里硬塞
LIMIT——它会截断分组内数据。正确做法是外层用JOIN关联原始表,条件为hour_ts = DATE_TRUNC('hour', t.created_at)且该小时命中激增条件 - 警惕
STRING_AGG默认 1MB 限制(PostgreSQL),激增时段若涉及上千用户,可能被截断,建议加ORDER BY count DESC LIMIT 5控制长度
实际跑的时候,最常卡在时区转换和除零保护这两步。尤其是 LAG 返回 NULL 后直接参与除法,整列变 NULL——看着像没数据,其实是计算崩了。











