CUME_DIST()初筛尾部异常值更稳,因其返回≤当前值的行数占比且对重复值连续,需配合PARTITION BY和ORDER BY使用,NULL应先过滤,嵌套子查询后用>0.95筛选前5%。

用 CUME_DIST() 初筛尾部异常值,比 PERCENT_RANK() 更稳
CUME_DIST() 返回“≤当前值的行数 / 组内总行数”,结果范围是 (0, 1],对重复值天然连续,不会因排序抖动把边界点漏掉。比如一批 status = 'error' 的日志值全一样,CUME_DIST() 会统一返回 1.0,而 PERCENT_RANK() 可能因内部排名计算返回 0,导致 WHERE percent_rank > 0.99 漏掉这批真实异常。
必须配合 ORDER BY 使用,否则报错:ERROR: window function cume_dist requires an ORDER BY clause;不能直接写在 WHERE 里,得嵌套子查询;NULL 默认排最前,若业务中 NULL 表示“未上报”,应先加 WHERE col IS NOT NULL 过滤。
抓响应时间最长的前 5% 记录,正确写法是:
SELECT service_name, request_id, duration_ms
FROM (
SELECT service_name, request_id, duration_ms,
CUME_DIST() OVER (PARTITION BY service_name ORDER BY duration_ms) AS cume_dist
FROM api_logs
WHERE duration_ms IS NOT NULL AND duration_ms > 0
) t
WHERE cume_dist > 0.95;-
PARTITION BY service_name别漏,否则全表当一组算 - 想抓最大值异常,
ORDER BY duration_ms(升序)——尾部才是最大值;抓最小值异常才用DESC - 用
>而不是>=,避免把并列第 95 名全捞出来
LAG() + 滚动 STDEV() 检测阶跃型突变
标准差本身对“前后突变”不敏感,比如日活从 5000 突增到 15000,全局 STDEV() 很难识别。但 LAG() 直接对比相邻行,再结合滚动窗口的标准差,就能精准定位阶跃点。
SQL Server 中 STDEV() 遇 NULL 就整行返回 NULL,必须提前过滤或用 COALESCE(amount, 0);滚动窗口要用 ROWS BETWEEN 29 PRECEDING AND CURRENT ROW,比 RANGE 更稳定;ORDER BY 字段不唯一(如多笔同天订单)时,务必补上主键等确定性字段,例如 ORDER BY order_date, order_id。
检测单用户订单金额突变的逻辑:
ABS(amount - LAG(amount) OVER (PARTITION BY user_id ORDER BY order_date, order_id)) > 3 * COALESCE(STDEV(amount) OVER (PARTITION BY user_id ORDER BY order_date, order_id ROWS BETWEEN 29 PRECEDING AND CURRENT ROW), 0)
- 首 29 行因前序不足,
STDEV()返回NULL,用COALESCE(..., 0)填充,但填充值要和业务逻辑一致(比如设为 10 元比设为 0 更合理) - 滚动窗口严格按分区隔离,不会混入其他用户的订单数据
-
LAG()排序字段必须和STDEV()保持完全一致,否则窗口对不齐
用 ROW_NUMBER() 构造“孤岛标识”,识别连续段断裂
判定“连续登录 3 天”这类业务规则,核心不是看日期本身,而是用 ROW_NUMBER() 和转成整数天数的日期做差,差值相同即属同一连续段。MySQL 用 TO_DAYS(date),PostgreSQL 用 DATE_PART('day', date::timestamp - '1970-01-01'::date)。
漏掉 PARTITION BY user_id 就会把所有人的记录拉通排序,结果全错;ORDER BY login_date 必须在 ROW_NUMBER() 和后续 GROUP BY 中完全一致;如果数据是小时级连续,就别用天数差——该用 EXTRACT(EPOCH FROM date) / 3600 转小时再做差。
构造孤岛 ID 的关键表达式:
TO_DAYS(login_date) - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS grp
- 粒度必须匹配业务定义:拿“连续 3 天”当孤岛,却用毫秒级时间戳做差,结果一定碎成一地
-
grp不是最终结果,只是分组依据;MIN(login_date)和MAX(login_date)才是孤岛起止点 - 同时按多维分组(如
user_id, device_type),PARTITION BY里字段顺序无关,但必须和业务语义对齐
旧版数据库(MySQL 5.7 / SQL Server 2012)怎么绕过窗口函数限制
这些版本不支持窗口函数,硬用变量模拟 @rn := @rn + 1 风险极高:执行顺序不保证,LIMIT 或优化器重写会导致行号跳变或重复。更稳妥的替代方案是用自连接统计“比当前日期小的记录数”,但数据量过万就明显变慢。
生产环境建议优先补全日期序列(应用层生成日期范围,再 LEFT JOIN 原表),比硬扛变量方案可靠得多;如果必须用变量,至少加显式 ORDER BY 子句,并禁用查询缓存和某些优化开关。
真正容易被忽略的是:没有验证粒度一致性。比如业务定义“连续 3 小时”,却在变量模拟时用 DATE(login_time) 截断到天,所有小时级连续性都被抹平了。

















