SQL 计算百分位数和中位数

在分析数据时,平均值可能会产生误导。少数异常值会显著扭曲均值。这就是为什么专业人士依赖中位数百分位数来获得更稳健的洞察。

但在大多数数据库中,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

工作原理

  1. ROW_NUMBER():为每一行分配位置 1 到 N
  2. COUNT(*) OVER ():获取总行数
  3. 奇数行数(N+1)/2 给出中间位置
  4. 偶数行数 :位置 N/2N/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 Boy,原文链接:https://www.hisqlboy.com/blog/calculating-percentiles-median-sql。著作权归原作者所有,本文仅用于学习交流,非商业用途。

相关推荐
数据库小组17 小时前
2026 年,MySQL 到 SelectDB 同步为何更关注实时、可观测与可校验?
数据库·mysql·数据库管理工具·数据同步·ninedata·selectdb·迁移工具
华科易迅17 小时前
MybatisPlus增删改查操作
android·java·数据库
Kethy__18 小时前
计算机中级-数据库系统工程师-计算机体系结构与存储系统
大数据·数据库·数据库系统工程师·计算机中级
SHoM SSER18 小时前
MySQL 数据库连接池爆满问题排查与解决
android·数据库·mysql
熬夜的咕噜猫18 小时前
MySQL备份与恢复
数据库·oracle
jnrjian19 小时前
recover database using backup controlfile until cancel 假recover,真一致
数据库·oracle
lifewange19 小时前
java连接Mysql数据库
java·数据库·mysql
大妮哟20 小时前
postgresql数据库日志量异常原因排查
数据库·postgresql·oracle
还是做不到嘛\.20 小时前
Dvwa靶场-SQL Injection (Blind)-基于sqlmap
数据库·sql·web安全
不写八个20 小时前
PHP教程004:php链接mysql数据库
数据库·mysql·php