sql无标准median函数,主流引擎如mysql 8.0+和sql server不支持,postgresql等虽有median()聚合函数但非窗口函数,无法用于分组动态计算,必须用row_number()+count()手动定位中位位置或percentile_cont(0.5)等替代方案。

为什么不能直接用 MEDIAN() 函数?
多数主流 SQL 引擎(如 PostgreSQL、Oracle)确实有 MEDIAN() 聚合函数,但 MySQL 8.0+ 和 SQL Server 完全不支持,SQLite 也不支持。即使支持,它也不是窗口函数——你没法写成 MEDIAN(x) OVER (PARTITION BY category)。所以真要按分组动态算中位数,必须靠窗口函数手动构造。
常见错误是试图用 AVG() 或 PERCENTILE_CONT(0.5) 混淆概念:前者是均值,后者虽能模拟中位数,但兼容性极差(PostgreSQL 支持,MySQL 不支持,SQL Server 需要特定版本且语法不同)。
用 ROW_NUMBER() + COUNT() 手动定位中位位置
核心思路:对每组数据排序编号,再找出中间一或两个位置的值,取平均。
- 先用
ROW_NUMBER() OVER (ORDER BY x)给组内行编号(注意:必须用ORDER BY,否则编号无意义) - 用
COUNT(*) OVER ()算出当前分组总行数n - 中位数位置是第
FLOOR((n+1)/2)和CEIL((n+1)/2)行(统一处理奇偶) - 用布尔条件过滤出这两行,再套一层
AVG()
示例(MySQL 8.0+ / PostgreSQL):
SELECT
category,
AVG(val) AS median_val
FROM (
SELECT
category,
val,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY val) AS rn,
COUNT(*) OVER (PARTITION BY category) AS cnt
FROM data_table
) t
WHERE rn IN (FLOOR((cnt + 1) / 2), CEIL((cnt + 1) / 2))
GROUP BY category;
PERCENT_RANK() 和 NTILE(2) 的坑
有人尝试用 PERCENT_RANK() 找最接近 0.5 的值,但结果不稳定:当存在重复值时,PERCENT_RANK() 可能跳过 0.5;而 NTILE(2) 分两桶,右桶第一个值不等于中位数(尤其偶数行时,它只取上半部分起点)。
-
NTILE(2) OVER (ORDER BY x)在 4 行数据时会分出 [1,1,2,2],取tile = 2的最小值 ≠ 中位数 -
PERCENT_RANK()对 [1,2,2,3] 返回 [0,0.33,0.33,1],没有精确 0.5,强行ORDER BY ABS(pct - 0.5) LIMIT 1会偏移 - 两者都无法在窗口上下文中直接聚合,仍需子查询+过滤
性能与 NULL 处理必须显式声明
中位数计算天然需要排序,大数据量时 OVER (ORDER BY ...) 是主要瓶颈。别指望加索引就能跳过排序——窗口函数的 ORDER BY 总会触发一次内存排序。
- 务必用
WHERE val IS NOT NULL预过滤,因为NULL会被ROW_NUMBER()排在最前或最后(行为因引擎而异),直接影响中位位置 - MySQL 8.0 对大结果集可能报
Window memory limit exceeded,需调大sort_buffer_size或限制分组粒度 - PostgreSQL 中
ORDER BY val NULLS LAST更安全,明确控制NULL位置
实际写的时候,别省那几行子查询——中位数没捷径,靠编号和计数是最稳的路径。容易被忽略的是:分组字段和排序字段必须一致使用非表达式(比如别写 ORDER BY ABS(x) 后又想按原 x 取值),否则语义断裂。











