标准SQL大多不支持MEDIAN(),需用ROW_NUMBER()与COUNT() OVER()配合手动计算:先按组排序编号,再根据组内行数取中间位置值平均。

中位数计算为什么不能直接用 MEDIAN()
因为标准 SQL(如 PostgreSQL 9.4+ 以外、MySQL 8.0 以前、SQL Server 2012+)压根不提供内置 MEDIAN() 聚合函数;即使有(如 Oracle、PostgreSQL 9.4+ 的 PERCENTILE_CONT(0.5)),它在分组场景下行为和直觉不符——比如会把整组当作一个连续分布,而不是按每组独立算中位数。
真正可靠、跨数据库兼容的做法,是靠窗口函数 + 行号逻辑手动定位中间位置。核心思路就一句:先排序,再找每组的第 N/2 和第 (N/2)+1 行(N 是组内行数),取平均值。
ROW_NUMBER() 和 COUNT(*) OVER() 必须配对使用
只用 ROW_NUMBER() 不够:它只给顺序编号,但不知道当前组总共有多少行;只用 COUNT(*) OVER(PARTITION BY ...) 也不行:它知道总数,但不知道哪一行是“中间”。必须两者结合,才能判断某行是否落在中位位置上。
常见错误是写成 ROW_NUMBER() OVER(ORDER BY x)(漏了 PARTITION BY),结果全表排一列序,完全破坏分组逻辑。
- 正确写法必须带
PARTITION BY group_col到两个窗口函数里 - 排序字段(
ORDER BY)建议加NULLS LAST或显式处理NULL,否则不同数据库对NULL排序策略不同,中位数可能漂移 - 如果组内行数为奇数,中位数就是第
(cnt + 1) / 2行;偶数则取第cnt / 2和cnt / 2 + 1行的平均值
MySQL 8.0+ 和 PostgreSQL 的写法差异很小,但 SQL Server 要绕开 AVG() 隐式转换
三者都支持 ROW_NUMBER() 和 COUNT() OVER(),语法几乎一致。真正容易踩坑的是数值类型处理:
- MySQL:若原始列为
INT,直接AVG(val)返回DECIMAL,没问题 - PostgreSQL:同上,
AVG()自动升为NUMERIC - SQL Server:
AVG(INT)仍返回INT,导致小数被截断!必须显式转成FLOAT或DECIMAL,例如AVG(CAST(val AS FLOAT))
示例片段(以 PostgreSQL/MySQL 为例):
SELECT group_col,
AVG(val) FILTER (WHERE rn IN (FLOOR((cnt + 1) / 2.0), CEIL((cnt + 1) / 2.0)))
FROM (
SELECT group_col, val,
ROW_NUMBER() OVER (PARTITION BY group_col ORDER BY val) AS rn,
COUNT(*) OVER (PARTITION BY group_col) AS cnt
FROM t
) t2
GROUP BY group_col;
性能敏感时,避免在大表上反复扫描
上面的子查询结构本质是两次窗口计算 + 一次分组聚合,对千万级表可能触发多次全表扫描(尤其当没在 PARTITION BY 和 ORDER BY 字段建联合索引时)。
优化方向很实际:
- 确保
(group_col, val)有联合索引,让ROW_NUMBER()和COUNT() OVER()能走索引扫描 - 如果只是偶尔查、且数据量不大,别过早优化;真卡了再考虑物化中间结果(如临时表存
rn和cnt) - 注意:有些数据库(如旧版 MySQL)不支持在同一个查询里多次引用窗口函数别名,得用两层嵌套,多一层
SELECT就多一次逻辑读
最常被忽略的一点:中位数对重复值和极端离群值不敏感,但窗口函数实现时,ORDER BY val 遇到大量相同 val,ROW_NUMBER() 会任意打乱顺序(无稳定排序),导致中位数在同等数据下浮动。加个次要排序字段(如主键)能彻底解决,但很多人根本没想到这层。

















