在分析数据时,平均值可能会产生误导。少数异常值会显著扭曲均值。这就是为什么专业人士依赖中位数 和百分位数来获得更稳健的洞察。
但在大多数数据库中,SQL 没有内置的 MEDIAN() 函数。让我们探索如何计算这些统计量。

图:分布曲线突出显示中位数和百分位数
为什么中位数很重要
考虑员工薪资的例子:
- 平均薪资:$100,000
- 但如果 CEO 赚 1,000,000,其他人都赚 50,000,平均值就会产生误导!
- 中位数薪资:$50,000("中间"值)才能真实反映情况
中位数不受极端值影响,更能代表数据的典型水平。
方法 1:使用 PERCENT_RANK 计算百分位数
PERCENT_RANK() 函数为每一行分配一个百分位等级(0 到 1)。
示例数据
假设我们有以下 employees 表:
| employee_name | salary |
|---|---|
| Alice | 45000 |
| Bob | 50000 |
| Carol | 55000 |
| David | 60000 |
| Eve | 500000 |
SQL 查询
sql
-- 计算每个薪资的百分位等级
SELECT
employee_name,
salary,
ROUND(PERCENT_RANK() OVER (ORDER BY salary) * 100) as percentile
FROM employees
ORDER BY salary;
查询结果
| employee_name | salary | percentile |
|---|---|---|
| Alice | 45000 | 0 |
| Bob | 50000 | 25 |
| Carol | 55000 | 50 |
| David | 60000 | 75 |
| Eve | 500000 | 100 |
解读:百分位数为 50 意味着一半的数据低于该值。Carol 的薪资处于第 50 百分位,意味着她的薪资高于 50% 的员工。
方法 2:使用窗口函数查找中位数
中位数是第 50 百分位的值。以下是如何找到它:
SQL 查询
sql
-- 查找中位数薪资
SELECT AVG(salary) as median_salary
FROM (
SELECT
salary,
ROW_NUMBER() OVER (ORDER BY salary) as rn,
COUNT(*) OVER () as total_count
FROM employees
)
WHERE rn IN ((total_count + 1) / 2, (total_count + 2) / 2);
查询结果
| median_salary |
|---|
| 55000 |
工作原理
- ROW_NUMBER():为每一行分配位置 1 到 N
- COUNT(*) OVER ():获取总行数
- 奇数行数 :
(N+1)/2给出中间位置 - 偶数行数 :位置
N/2和N/2+1的平均值
为什么这样有效:
- 对于 5 行(奇数):中位数是第 3 行((5+1)/2 = 3)
- 对于 6 行(偶数):中位数是第 3 和第 4 行的平均值(6/2 = 3, 6/2+1 = 4)
方法 3:使用 NTILE 计算四分位数
NTILE(4) 将数据分成 4 个相等的组(四分位数)。
SQL 查询
sql
-- 分成四分位数
SELECT
employee_name,
salary,
NTILE(4) OVER (ORDER BY salary) as quartile,
CASE
NTILE(4) OVER (ORDER BY salary)
WHEN 1 THEN 'Bottom 25%'
WHEN 2 THEN 'Lower Middle 25%'
WHEN 3 THEN 'Upper Middle 25%'
WHEN 4 THEN 'Top 25%'
END as quartile_label
FROM employees;
查询结果
| employee_name | salary | quartile | quartile_label |
|---|---|---|---|
| Alice | 45000 | 1 | Bottom 25% |
| Bob | 50000 | 2 | Lower Middle 25% |
| Carol | 55000 | 3 | Upper Middle 25% |
| David | 60000 | 4 | Top 25% |
| Eve | 500000 | 4 | Top 25% |
提示 :你可以使用 NTILE(10) 计算十分位数,或使用 NTILE(100) 计算细粒度的百分位数。
方法 4:百分位数边界
查找特定百分位数的值(例如,第 75 百分位):
SQL 查询
sql
-- 查找第 75 百分位的值
SELECT salary as p75_salary
FROM (
SELECT
salary,
NTILE(100) OVER (ORDER BY salary) as percentile
FROM employees
)
WHERE percentile = 75
LIMIT 1;
查询结果
| p75_salary |
|---|
| 60000 |
解读:第 75 百分位的薪资是 $60,000,意味着 75% 的员工薪资低于这个值。
一次计算多个百分位数
需要 P25、P50(中位数)和 P75?使用条件聚合:
SQL 查询
sql
-- 获取 P25, P50 (中位数), P75
SELECT
MAX(CASE WHEN quartile = 1 THEN salary END) as p25,
MAX(CASE WHEN quartile = 2 THEN salary END) as p50_median,
MAX(CASE WHEN quartile = 3 THEN salary END) as p75
FROM (
SELECT
salary,
NTILE(4) OVER (ORDER BY salary) as quartile
FROM employees
);
查询结果
| p25 | p50_median | p75 |
|---|---|---|
| 45000 | 50000 | 55000 |
注意 :这种方法使用四分位数的边界值作为近似。对于更精确的百分位数,使用 NTILE(100) 或 ROW_NUMBER 方法。
分组级别的百分位数
计算每个部门的中位数薪资:
sql
SELECT
department,
AVG(salary) as median_salary
FROM (
SELECT
department,
salary,
ROW_NUMBER() OVER (
PARTITION BY department
ORDER BY salary
) as rn,
COUNT(*) OVER (PARTITION BY department) as dept_count
FROM employees
)
WHERE rn IN ((dept_count + 1) / 2, (dept_count + 2) / 2)
GROUP BY department;
关键点 :使用 PARTITION BY department 为每个部门单独计算中位数。
对比:均值 vs 中位数
让我们看看异常值如何影响均值但不影响中位数:
SQL 查询
sql
-- 比较均值和中位数
SELECT
ROUND(AVG(salary)) as mean_salary,
(
SELECT AVG(salary)
FROM (
SELECT
salary,
ROW_NUMBER() OVER (ORDER BY salary) as rn,
COUNT(*) OVER () as total_count
FROM employees
)
WHERE rn IN ((total_count + 1) / 2, (total_count + 2) / 2)
) as median_salary
FROM employees;
查询结果
| mean_salary | median_salary |
|---|---|
| 142000 | 55000 |
关键洞察:注意异常值(500,000)如何显著影响均值(142,000)但不影响中位数($55,000)!这就是为什么中位数对于有偏数据更稳健。
快速参考
| 统计量 | SQL 方法 |
|---|---|
| 百分位等级 | PERCENT_RANK() OVER (ORDER BY col) |
| 中位数 | ROW_NUMBER() + 中间位置公式 |
| 四分位数(4 组) | NTILE(4) OVER (ORDER BY col) |
| 十分位数(10 组) | NTILE(10) OVER (ORDER BY col) |
| 特定百分位数 | NTILE(100) + 过滤 |
跨数据库实现
不同数据库对百分位数的支持略有不同:
| 数据库 | 中位数函数 | 百分位数函数 | 备注 |
|---|---|---|---|
| PostgreSQL | PERCENTILE_CONT(0.5) |
PERCENTILE_CONT(0.75) |
内置函数,最方便 |
| MySQL | 使用 ROW_NUMBER | 使用 NTILE | 无内置函数 |
| SQL Server | PERCENTILE_CONT(0.5) |
PERCENTILE_CONT(0.75) |
内置函数 |
| SQLite | 使用 ROW_NUMBER | 使用 NTILE | 无内置函数 |
| Oracle | MEDIAN() |
PERCENTILE_CONT(0.75) |
有专用 MEDIAN 函数 |
PostgreSQL 示例
sql
-- PostgreSQL 内置函数
SELECT
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) as median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY salary) as p75
FROM employees;
SQL Server 示例
sql
-- SQL Server 内置函数
SELECT
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) OVER () as median,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY salary) OVER () as p75
FROM employees;
最佳实践
1. 对有偏数据使用中位数
收入、价格和响应时间通常有异常值。中位数比均值更能代表典型值。
示例场景:
- 房价分析(少数豪宅会扭曲均值)
- 网站响应时间(偶尔的超时会扭曲均值)
- 用户活跃度(超级用户会扭曲均值)
2. 同时报告均值和中位数
如果它们差异显著,说明你的数据有偏。
判断标准:
- 如果
均值 / 中位数 > 1.5,数据右偏(有高异常值) - 如果
均值 / 中位数 < 0.67,数据左偏(有低异常值)
3. 考虑 NTILE 的限制
对于小数据集,NTILE 分组可能不均匀。
示例:
- 5 行数据使用
NTILE(4):分组大小为 2, 1, 1, 1(不均匀) - 建议:至少 100 行数据才使用
NTILE(100)
4. 索引 ORDER BY 列
百分位数计算需要排序数据,因此索引有助于性能。
sql
-- 为薪资列创建索引
CREATE INDEX idx_salary ON employees(salary);
5. 使用 CTE 提高可读性
对于复杂的百分位数计算,使用 CTE(公用表表达式)使查询更易读:
sql
WITH ranked_salaries AS (
SELECT
salary,
ROW_NUMBER() OVER (ORDER BY salary) as rn,
COUNT(*) OVER () as total_count
FROM employees
)
SELECT AVG(salary) as median_salary
FROM ranked_salaries
WHERE rn IN ((total_count + 1) / 2, (total_count + 2) / 2);
实际应用场景
1. 性能监控
计算 P95 和 P99 响应时间,识别性能瓶颈:
sql
SELECT
MAX(CASE WHEN percentile = 95 THEN response_time END) as p95,
MAX(CASE WHEN percentile = 99 THEN response_time END) as p99
FROM (
SELECT
response_time,
NTILE(100) OVER (ORDER BY response_time) as percentile
FROM api_logs
WHERE log_date = CURRENT_DATE
);
2. 薪资基准
比较不同职位的薪资中位数,确保公平性:
sql
SELECT
job_title,
AVG(salary) as median_salary
FROM (
SELECT
job_title,
salary,
ROW_NUMBER() OVER (
PARTITION BY job_title
ORDER BY salary
) as rn,
COUNT(*) OVER (PARTITION BY job_title) as job_count
FROM employees
)
WHERE rn IN ((job_count + 1) / 2, (job_count + 2) / 2)
GROUP BY job_title;
3. 客户细分
根据购买金额将客户分成四分位数:
sql
SELECT
customer_id,
total_spent,
NTILE(4) OVER (ORDER BY total_spent) as spending_quartile,
CASE
NTILE(4) OVER (ORDER BY total_spent)
WHEN 4 THEN 'VIP'
WHEN 3 THEN 'High Value'
WHEN 2 THEN 'Medium Value'
WHEN 1 THEN 'Low Value'
END as customer_segment
FROM customer_spending;
4. 异常值检测
使用四分位距(IQR)识别异常值:
sql
WITH quartiles AS (
SELECT
MAX(CASE WHEN quartile = 1 THEN value END) as q1,
MAX(CASE WHEN quartile = 3 THEN value END) as q3
FROM (
SELECT
value,
NTILE(4) OVER (ORDER BY value) as quartile
FROM measurements
)
)
SELECT
m.value,
CASE
WHEN m.value < q.q1 - 1.5 * (q.q3 - q.q1) THEN 'Low Outlier'
WHEN m.value > q.q3 + 1.5 * (q.q3 - q.q1) THEN 'High Outlier'
ELSE 'Normal'
END as outlier_status
FROM measurements m
CROSS JOIN quartiles q;
常见错误
错误 1:忘记处理偶数行
❌ 错误示例:
sql
-- 只取中间行,偶数行时不准确
SELECT salary
FROM (
SELECT salary, ROW_NUMBER() OVER (ORDER BY salary) as rn
FROM employees
)
WHERE rn = (SELECT COUNT(*) FROM employees) / 2;
✅ 正确示例:
sql
-- 偶数行时取两个中间值的平均
SELECT AVG(salary) as median_salary
FROM (
SELECT
salary,
ROW_NUMBER() OVER (ORDER BY salary) as rn,
COUNT(*) OVER () as total_count
FROM employees
)
WHERE rn IN ((total_count + 1) / 2, (total_count + 2) / 2);
错误 2:混淆 PERCENT_RANK 和 NTILE
❌ 错误理解:
PERCENT_RANK()返回百分位数值(错误!)NTILE(100)返回百分位等级(错误!)
✅ 正确理解:
PERCENT_RANK()返回百分位等级(0 到 1)NTILE(100)将数据分成 100 组,返回组号(1 到 100)
错误 3:在小数据集上使用 NTILE(100)
❌ 问题:
sql
-- 只有 20 行数据,使用 NTILE(100) 会导致很多空组
SELECT NTILE(100) OVER (ORDER BY value) FROM small_table;
✅ 解决方案:
sql
-- 使用合适的分组数量
SELECT NTILE(10) OVER (ORDER BY value) FROM small_table;
经验法则:数据行数应至少是 NTILE 参数的 2 倍。
结论
虽然 SQL 在大多数数据库中缺少原生的 MEDIAN() 函数,但窗口函数提供了强大的替代方案:
- PERCENT_RANK():用于百分位等级
- NTILE():用于分成组
- ROW_NUMBER():结合算术运算计算精确中位数
这些技术为你提供了超越简单平均值的稳健统计洞察。
关键要点:
- 中位数不受异常值影响,更能代表典型值
- 百分位数提供了数据分布的完整视图
- 窗口函数是计算这些统计量的核心工具
- 不同数据库有不同的内置函数,但核心逻辑相同
- 始终同时报告均值和中位数,了解数据的偏度
相关文章推荐
- SQL for Data Analysis: The Ultimate Guide
- Ranking Data with SQL: RANK, DENSE_RANK, and ROW_NUMBER Explained
- Mastering SQL LEAD and LAG Functions for Row Comparisons
版权声明 :本文转载自 SQL Boy,原文链接:https://www.hisqlboy.com/blog/calculating-percentiles-median-sql。著作权归原作者所有,本文仅用于学习交流,非商业用途。