用stddev_samp(x)/avg(x)计算变异系数需加having count(*)>1、分母用nullif(avg(x),0),并按业务加avg(x)>0;浮点误差下percentile_cont不可直接用于z-score,应优先在聚合前用窗口函数打标离群值。

用STDDEV_SAMP + AVG判断每组内部离群程度
直接看标准差和均值的比值(即变异系数CV),是识别分组内数据离散程度最直观的方式。但STDDEV_SAMP(x) / AVG(x)不能裸写,否则单行组、零均值、负均值都会让结果失效或报错。
- 必须加
HAVING COUNT(*) > 1,排除样本量不足的组(STDDEV_SAMP在单行时返回NULL,但业务上这组根本不可信) - 分母要用
NULLIF(AVG(x), 0),而不是COALESCE(AVG(x), 1)——后者会人为压低CV,掩盖真实波动 - 若业务逻辑要求均值为正(如销售额、响应时长),建议显式加
AND AVG(x) > 0,避免亏损组或测试数据污染判断 - 浮点误差要防:用
ABS(AVG(x)) 代替<code>AVG(x) = 0,更鲁棒
用窗口函数动态算IQR剔除每组离群值
硬编码范围(如salary BETWEEN 5000 AND 80000)只适用于静态阈值场景。真实业务中,各组数据分布差异大,必须按组独立计算四分位距(IQR)。
- 先用
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY x)和PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY x)算出每组Q1/Q3 - IQR = Q3 − Q1,离群下界 = Q1 − 1.5 × IQR,上界 = Q3 + 1.5 × IQR
- 注意:PostgreSQL/SQL Server支持该语法;MySQL 8.0+需用
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY x)+COUNT(*) OVER (PARTITION BY group_col)手动逼近 - 别在
WHERE里直接引用窗口函数结果——必须套一层CTE或子查询,否则报错ERROR: window functions are not allowed in WHERE
用FILTER子句(PostgreSQL专属)简化条件聚合
比起嵌套CASE WHEN,FILTER语义更清晰、容错性更高,适合快速写出“只对非离群值求平均”的逻辑。
- 正确写法:
AVG(salary) FILTER (WHERE salary BETWEEN lower_bound AND upper_bound),括号必须紧跟在聚合函数后,不能漏空格 - 错误写法:
AVG(salary FILTER (WHERE ...))——FILTER不是函数参数,位置错就语法报错 -
FILTER自动把不满足条件的行视作NULL,而AVG天然跳过NULL,无需额外CASE兜底 - MySQL/SQL Server不支持
FILTER,强行使用会提示Unknown column 'FILTER'或Incorrect syntax near 'FILTER'
警惕GROUP BY后二次去噪的逻辑陷阱
有人想“先分组聚合,再把各组的平均值拿去算百分位,剔除均值过大的组”——这是典型误用。聚合后的结果集已丢失原始分布信息,PERCENTILE_CONT无法作用于它。
- 错误示例:
HAVING AVG(sales) > PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY AVG(sales))→ 报错或返回NULL - 正确路径:用CTE先算出各组
AVG(sales),再在外层用AVG() OVER()和STDDEV() OVER()构造Z-score,或直接用子查询SELECT AVG(sales) * 3 FROM group_summary - 更稳妥的做法是把离群判断留在聚合前:用窗口函数在原始行上打标(如
is_outlier = CASE WHEN x > Q3 + 1.5*IQR THEN 1 ELSE 0 END),再WHERE is_outlier = 0过滤
实际执行时,最容易被忽略的是NULL值的传播路径:窗口函数不处理NULL、聚合函数跳过NULL、但FILTER子句对NULL的判定逻辑又和CASE不同。同一份数据,在PostgreSQL里用FILTER跑通,换到MySQL里用CASE重写时,可能因未显式处理NULL分支而漏掉整组数据。










