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。著作权归原作者所有,本文仅用于学习交流,非商业用途。

相关推荐
亓才孓2 小时前
[SpringBoot]UnableToConnectException : Public Key Retrieval is not allowed
java·数据库·spring boot
好学且牛逼的马2 小时前
从“混沌初开”到“有序统一”:Java集合框架发展历程与核心知识点详解
前端·数据库·python
REDcker3 小时前
HDR Vivid 技术介绍
数据库·算法·视频·sdr·屏幕·显示技术·dhr
冰暮流星3 小时前
sql语句之union语句
数据库·sql
2401_876381924 小时前
程序人生-Hello’s P2P
数据库·程序人生·p2p
VX:Fegn08954 小时前
计算机毕业设计|基于springboot + vue养老院管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
湘-枫叶情缘4 小时前
从数据库写作到情绪工程:网络文学工程化转向的理论综述
数据库·人工智能
heimeiyingwang4 小时前
企业非结构化数据的 AI 处理与价值挖掘
大数据·数据库·人工智能·机器学习·架构