SQL标准无MEDIAN()聚合函数,需用ROW_NUMBER()与COUNT() OVER()配合:先按组排序编号,再根据总行数奇偶性取中间一行或两行平均。

中位数不是聚合函数,不能直接 SELECT MEDIAN(col)
SQL 标准里压根没有 MEDIAN() 聚合函数(PostgreSQL 9.4+ 和 Oracle 12c+ 虽有,但行为不一致、不跨数据库兼容)。想在指定范围(比如按 category 分组,或 WHERE 筛出某时间段)内算中位数,必须靠窗口函数构造排序位置,再取中间行。核心思路是:先排序编号,再找「总行数一半」附近的位置。
用 ROW_NUMBER() 和 COUNT() OVER() 定位中间行
关键不是“平均两个中间值”,而是判断奇偶后动态选行。常见错误是直接用 PERCENT_RANK() 或 NTILE(2),它们对重复值敏感,且边界处理不稳。
- 对每个分组(如
PARTITION BY category),用ROW_NUMBER() OVER (ORDER BY value)给每行编连续序号(rn) - 同时用
COUNT(*) OVER (PARTITION BY category)得到该组总行数(cnt) - 中位数行满足:
rn = FLOOR((cnt + 1) / 2.0)(奇数)或rn IN (cnt/2, cnt/2 + 1)(偶数)——但更稳妥写法是统一用rn IN (FLOOR((cnt + 1)/2.0), CEIL((cnt + 1)/2.0))
示例(MySQL 8.0+/PostgreSQL):
SELECT category, AVG(value) AS median
FROM (
SELECT category, value,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY value) AS rn,
COUNT(*) OVER (PARTITION BY category) AS cnt
FROM sales
WHERE sale_date BETWEEN '2024-01-01' AND '2024-06-30'
) t
WHERE rn IN (FLOOR((cnt + 1)/2.0), CEIL((cnt + 1)/2.0))
GROUP BY category;
WHERE 范围前置过滤比窗口内过滤更高效
如果只想要某时间范围或某状态的中位数,务必把 WHERE 写在子查询最外层或窗口函数之前。写成 COUNT(*) OVER (PARTITION BY x WHERE condition) 是错的——WHERE 不能出现在窗口定义里。
- ✅ 正确:先
WHERE sale_date >= '2024-01-01',再开窗口 - ❌ 错误:试图在
OVER (PARTITION BY x)里加WHERE,语法报错 - ⚠️ 注意:若用
FILTER (WHERE ...)(PostgreSQL 特有),仅适用于聚合函数,不适用于ROW_NUMBER()这类排序函数
NULL 值和重复值会直接影响结果位置
ORDER BY value 默认把 NULL 排在最前(SQL 标准),但 MySQL 和 SQL Server 可能排最后,导致 ROW_NUMBER() 编号偏移。重复值不会让 ROW_NUMBER() 并列,它强制给唯一序号(比如两个 100 都排第3和第4),这反而是优点——避免中位数被相同值“挤”偏。
- 保险做法:显式声明
ORDER BY value NULLS LAST(PostgreSQL/Oracle)或在 MySQL 中用ORDER BY value IS NULL, value - 若业务要求忽略
NULL,必须提前WHERE value IS NOT NULL,否则COUNT(*) OVER会把NULL算进总数,但排序时又参与占位,造成位置错乱
真正麻烦的是业务语义:中位数要不要包含 NULL?是否视重复值为同一档?这些得由需求定,窗口函数只负责忠实执行排序逻辑。

















