用 PERCENTILE_CONT 计算 IQR 法识别离群值最稳健,需正确使用 OVER 子句、按组分区、处理 NULL 及排序唯一性,并通过子查询或 CTE 引用窗口结果。

用 PERCENTILE_CONT 找数值型离群值最稳
直接用四分位距(IQR)法比均值±3σ 更抗噪,尤其当数据偏态或含极端值时。PERCENTILE_CONT 是标准 SQL 窗口函数,主流数据库(PostgreSQL、SQL Server、Oracle、BigQuery)都支持,MySQL 8.0+ 也已支持。
常见错误是把 PERCENTILE_CONT(0.25) 和 PERCENTILE_CONT(0.75) 写成聚合函数,漏掉 OVER() —— 这会导致“窗口函数必须有 OVER 子句”报错。
- 必须写成
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY value) OVER ()(PostgreSQL/Oracle 风格)或PERCENTILE_CONT(0.25) OVER (ORDER BY value)(SQL Server/BigQuery 风格),具体看数据库文档 - 如果要按分组分别计算离群值(比如每个
product_id单独算 IQR),OVER (PARTITION BY product_id ORDER BY sales)才对;只写ORDER BY会做全局累计,结果完全不对 -
PERCENTILE_CONT返回的是插值结果,比PERCENTILE_DISC更平滑,但若字段为整型且要求返回原始值,得转成ROUND(……)或改用DISC
用 LAG/LEAD 检测突变型离群点
适合时间序列或有序业务数据(如日活、订单量、传感器读数),目标不是找统计异常,而是识别“前后相邻记录之间跳变过大”的点。
典型错误是没处理 LAG 在首行返回 NULL,导致 ABS(value - LAG(value)) > threshold 整行被过滤掉——其实首行该保留或单独标记。
- 务必用
COALESCE(LAG(value) OVER (ORDER BY ts), value)填充首行,避免NULL参与运算 - 排序字段(如
ts)必须严格唯一,否则LAG行为不可控;如有重复时间戳,加id作为第二排序键:ORDER BY ts, id - 阈值设太小会误报(比如日活从 999→1002 被标为异常),建议先用
PERCENTILE_CONT(0.95)看下历史差值分布再定
ROW_NUMBER() + 分位数过滤的性能陷阱
有些老版本数据库不支持 PERCENTILE_CONT,就有人用 ROW_NUMBER() OVER (ORDER BY value) 手动算中位数。这方法可行,但容易在大数据量下拖垮查询。
问题出在:先全排序再编号,即使只想要前 1% 的离群高值,也得把千万行全排一遍。
- 替代方案:用
WHERE value > (SELECT PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY value) FROM t),子查询可走索引(如果value有索引) - 如果硬要用
ROW_NUMBER,至少加上WHERE value > (SELECT AVG(value) * 2 FROM t)先粗筛,减少排序基数 - 注意
ROW_NUMBER()的序号从 1 开始,算第 99 百分位时别写成WHERE rn = FLOOR(0.99 * cnt),而应是WHERE rn > 0.99 * cnt(因为序号是离散的)
离群值标记后怎么安全剔除?
别一上来就 DELETE。多数场景下,离群值需要人工复核,或仅用于监控告警,而非直接丢弃。
最容易被忽略的是:用窗口函数标记离群值后,直接在 WHERE 里引用窗口别名(比如 WHERE is_outlier = 1),会报错“窗口函数不能出现在 WHERE 中”。
- 正确做法是套一层子查询或 CTE:
WITH flagged AS (SELECT *, CASE WHEN value > q3 + 1.5*(q3-q1) THEN 1 ELSE 0 END AS is_outlier FROM (……)) SELECT * FROM flagged WHERE is_outlier = 1 - 如果后续要更新标记状态(比如人工确认某条是正常峰值),记得给离群标记字段建索引,否则
UPDATE …… WHERE is_outlier = 1会全表扫 - 千万别用
DELETE代替WHERE来“隐藏”离群值——审计和回溯时会找不到原始依据
事情说清了就结束。离群值本身没有对错,关键是你定义它的上下文是否和业务一致。