postgresql中计算分组mad需分两步:先用percentile_cont(0.5)求各组中位数,再计算绝对偏差并对其求中位数;mysql需用窗口函数模拟中位数并注意偶数长度取平均;sqlite受限需json聚合+外部计算或退化为aad。

PostgreSQL里用percentile_cont快速算分组内MAD
中值绝对偏差(MAD)不是内置聚合函数,但PostgreSQL的percentile_cont可以绕过手写排序逻辑。关键在于:先按组求中位数,再对每行算绝对偏差,最后对偏差序列再求中位数。
常见错误是试图在一个聚合里嵌套两次中位数计算——这会报错subquery in aggregate context。正确做法是用WITH或窗口函数分两步走:
- 第一步:用
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x)算出每组的中位数med - 第二步:用
ABS(x - med)生成偏差列,再对其调用PERCENTILE_CONT(0.5)
示例(按category分组):
WITH medians AS (
SELECT category,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY value) AS med
FROM data_table
GROUP BY category
)
SELECT m.category,
PERCENTILE_CONT(0.5) WITHIN GROUP (
ORDER BY ABS(d.value - m.med)
) AS mad
FROM data_table d
JOIN medians m ON d.category = m.category
GROUP BY m.category;
MySQL 8.0+用窗口函数模拟median实现MAD
MySQL没有percentile_cont,但可用ROW_NUMBER()和COUNT(*)手动定位中位数位置。难点在于:中位数本身需要窗口计算,而MAD又依赖这个中位数,所以必须用两层子查询或CTE。
容易踩的坑是忽略偶数长度时中位数要取中间两数平均值——直接用FLOOR((cnt+1)/2)只拿到下中位数,会导致MAD偏小。
- 先用
COUNT(*) OVER (PARTITION BY category)得每组总数cnt - 再用
ROW_NUMBER() OVER (PARTITION BY category ORDER BY value)给每行编号 - 筛选出第
FLOOR((cnt+1)/2)和CEIL((cnt+1)/2)行,取平均得中位数 - 最后对
ABS(value - med)重复上述流程
性能上,两次全量排序开销大,数据量超10万行建议加(category, value)联合索引。
SQLite里用json_group_array + 自定义函数补足缺失能力
SQLite连窗口函数都受限(3.25+才支持),原生无法做分组中位数。可行路径是:把每组value聚合成JSON数组,再用Python或JavaScript扩展写一个mad()函数解析并计算。
但要注意:默认编译的SQLite不启用json扩展,执行前先确认SELECT json_array(1,2);是否返回[1,2];否则需重新编译或换用spatialite等增强版。
- 聚合阶段:
SELECT category, json_group_array(value) AS vals FROM t GROUP BY category - 在应用层解析
vals为列表,用numpy.median(np.abs(arr - np.median(arr)))算MAD - 若坚持纯SQL,只能退化为近似法:用
AVG(ABS(value - (SELECT AVG(value) FROM ...)))——但这算的是平均绝对偏差(AAD),不是MAD
为什么不能直接用AVG(ABS(value - MEDIAN(value)))?
因为几乎所有SQL引擎都不允许在聚合函数里嵌套另一个非标量聚合(如MEDIAN)。即使某些方言看似支持,实际执行时也会报错misuse of aggregate: median或静默返回NULL。
根本矛盾在于:中位数是顺序敏感的统计量,必须先完成分组、排序、定位三步,而标准SQL的聚合执行模型要求所有聚合函数独立作用于同一行集——它无法表达“先按组求中位数,再用该中位数参与下一轮聚合”这种依赖关系。
真正复杂的地方不在公式本身,而在如何把“中位数作为中间变量”这件事,在不同引擎的执行计划约束下安全落地。别被函数名迷惑,重点始终是数据流的分阶段可控性。










