PostgreSQL PERCENT_RANK() 窗口函数完全解析

一、PERCENT_RANK() 是什么?(一句话解释)

"百分比排名:你在群体中的相对位置(0% - 100%)"

就像考试后老师说:"你的成绩超过了全班 80% 的同学",这就是 PERCENT_RANK。

复制代码
公式:(当前排名 - 1) / (总行数 - 1)

张三: 第 1 名 → (1-1)/(10-1) = 0/9 = 0.00   (0%)
李四: 第 2 名 → (2-1)/(10-1) = 1/9 = 0.11   (11%)
王五: 第 5 名 → (5-1)/(10-1) = 4/9 = 0.44   (44%)
赵六: 第 10 名 → (10-1)/(10-1) = 9/9 = 1.00 (100%)

特点:

  • 最小值永远是 0(第 1 名)
  • 最大值永远是 1(最后一名)
  • 结果是 小数(0.00 - 1.00),乘以 100 就是百分比

二、和 RANK/DENSE_RANK 的区别

函数 返回值 范围 含义 示例
RANK() 整数 1, 2, 3... 绝对排名 第 5 名
DENSE_RANK() 整数 1, 2, 3... 紧凑排名 第 5 名
PERCENT_RANK() 小数 0.0 - 1.0 相对位置 超过 44% 的人

直观对比:

sql 复制代码
SELECT 
    name,
    score,
    RANK() OVER (ORDER BY score DESC) AS rank,
    COUNT(*) OVER () AS total,
    ROUND(PERCENT_RANK() OVER (ORDER BY score DESC)::DECIMAL * 100, 2) AS percentile
FROM students;

-- 结果(假设 10 人):
-- name  | score | rank | total | percentile
-- ------+-------+------+-------+------------
-- 张三  | 100   | 1    | 10    | 0.00       ← 第 1 名,0%
-- 李四  | 95    | 2    | 10    | 11.11      ← 超过 11% 的人
-- 王五  | 90    | 3    | 10    | 22.22
-- ...
-- 赵六  | 60    | 10   | 10    | 100.00     ← 最后一名,100%

三、8 个实用场景

场景 1:计算百分位(最经典)

需求: 告诉员工"你的业绩超过了公司多少比例的人"

sql 复制代码
SELECT 
    emp_name,
    sales_amount,
    ROUND(PERCENT_RANK() OVER (ORDER BY sales_amount ASC)::DECIMAL * 100, 2) AS percentile
FROM sales_performance
ORDER BY percentile DESC;

解读:

  • percentile = 90:你的业绩超过了 90% 的人(顶尖水平)
  • percentile = 50:你的业绩中等,超过了一半的人
  • percentile = 10:你的业绩较低,只超过了 10% 的人

注意: ORDER BY ASC 时,数值越大,percentile 越高;ORDER BY DESC 时相反。


场景 2:找出前/后 N% 的数据

需求: 找出业绩最好的前 10% 员工

sql 复制代码
SELECT * FROM (
    SELECT 
        emp_name,
        sales_amount,
        PERCENT_RANK() OVER (ORDER BY sales_amount DESC) AS pr
    FROM sales_performance
) t
WHERE pr <= 0.1;  -- 前 10%

其他用法:

sql 复制代码
-- 后 20%(需要改进的员工)
WHERE pr >= 0.8

-- 中间 60%(中等水平)
WHERE pr BETWEEN 0.2 AND 0.8

-- 去掉最高和最低各 5%(排除异常值)
WHERE pr BETWEEN 0.05 AND 0.95

场景 3:分组百分位(部门内对比)

需求: 计算每个员工在部门内的百分位

sql 复制代码
SELECT 
    dept_name,
    emp_name,
    sales_amount,
    ROUND(PERCENT_RANK() OVER (
        PARTITION BY dept_name 
        ORDER BY sales_amount DESC
    )::DECIMAL * 100, 2) AS dept_percentile
FROM employees
ORDER BY dept_name, dept_percentile DESC;

结果示例:

复制代码
dept_name | emp_name | sales_amount | dept_percentile
----------+----------+--------------+----------------
销售部    | 张三     | 100000       | 0.00           ← 销售部第 1
销售部    | 李四     | 95000        | 33.33          ← 超过销售部 33% 的人
技术部    | 王五     | 80000        | 0.00           ← 技术部第 1
技术部    | 赵六     | 75000        | 50.00          ← 技术部中等

优势: 跨部门对比更公平(销售部的 10 万可能不如技术部的 8 万难拿)


场景 4:标准化不同量纲的数据

需求: 把销售额(万元)和客户数(个)统一成百分位,方便综合评分

sql 复制代码
SELECT 
    emp_name,
    sales_amount,
    customer_count,
    ROUND(PERCENT_RANK() OVER (ORDER BY sales_amount DESC)::DECIMAL * 100, 2) AS sales_pct,
    ROUND(PERCENT_RANK() OVER (ORDER BY customer_count DESC)::DECIMAL * 100, 2) AS customer_pct,
    ROUND(
        (PERCENT_RANK() OVER (ORDER BY sales_amount DESC) + 
         PERCENT_RANK() OVER (ORDER BY customer_count DESC)) / 2 * 100, 
        2
    ) AS combined_score
FROM employee_performance;

为什么用 PERCENT_RANK?

  • 销售额范围:10万 - 1000万
  • 客户数范围:5 - 500
  • 直接相加没意义,转换成百分位后可以加权平均

场景 5:检测数据分布

需求: 分析销售业绩的分布是否均匀

sql 复制代码
SELECT 
    CASE 
        WHEN PERCENT_RANK() OVER (ORDER BY sales_amount DESC) < 0.25 THEN 'Q1 前25%'
        WHEN PERCENT_RANK() OVER (ORDER BY sales_amount DESC) < 0.50 THEN 'Q2 25-50%'
        WHEN PERCENT_RANK() OVER (ORDER BY sales_amount DESC) < 0.75 THEN 'Q3 50-75%'
        ELSE 'Q4 后25%'
    END AS quartile,
    COUNT(*) AS emp_count,
    AVG(sales_amount) AS avg_sales,
    MIN(sales_amount) AS min_sales,
    MAX(sales_amount) AS max_sales
FROM sales_performance
GROUP BY quartile
ORDER BY quartile;

用途: 快速了解业绩集中度,是否有"二八定律"现象


场景 6:动态阈值划分等级

需求: 根据实际分布划分 ABCD 等级(而非固定阈值)

sql 复制代码
SELECT 
    emp_name,
    sales_amount,
    CASE 
        WHEN PERCENT_RANK() OVER (ORDER BY sales_amount DESC) <= 0.1 THEN 'S 级 (前10%)'
        WHEN PERCENT_RANK() OVER (ORDER BY sales_amount DESC) <= 0.3 THEN 'A 级 (10-30%)'
        WHEN PERCENT_RANK() OVER (ORDER BY sales_amount DESC) <= 0.6 THEN 'B 级 (30-60%)'
        WHEN PERCENT_RANK() OVER (ORDER BY sales_amount DESC) <= 0.9 THEN 'C 级 (60-90%)'
        ELSE 'D 级 (后10%)'
    END AS grade
FROM sales_performance;

优势: 自动适应数据变化,不需要手动调整阈值


场景 7:异常值检测

需求: 找出极端高或极端低的订单金额

sql 复制代码
SELECT 
    order_no,
    amount,
    ROUND(PERCENT_RANK() OVER (ORDER BY amount ASC)::DECIMAL * 100, 2) AS percentile
FROM orders
WHERE PERCENT_RANK() OVER (ORDER BY amount ASC) < 0.01  -- 最低 1%
   OR PERCENT_RANK() OVER (ORDER BY amount ASC) > 0.99; -- 最高 1%

用途: 风控系统识别可疑交易


场景 8:生成累积分布图数据

需求: 为报表生成累积分布数据

sql 复制代码
SELECT 
    ROUND(PERCENT_RANK() OVER (ORDER BY sales_amount ASC)::DECIMAL * 100, 2) AS x_axis,
    sales_amount AS y_axis
FROM sales_performance
ORDER BY x_axis;

用途: 在 Excel 或 BI 工具中绘制累积分布曲线


四、核心语法

sql 复制代码
PERCENT_RANK() OVER (
    PARTITION BY column1, column2  -- 可选:分组依据
    ORDER BY column3 ASC/DESC      -- 必填:排序规则
)

关键点:

  1. 不需要参数PERCENT_RANK() 括号里是空的
  2. 必须配合 OVER():声明这是窗口函数
  3. ORDER BY 必填:决定排名方向
  4. 返回值是小数:0.0 - 1.0,通常乘以 100 转成百分比

五、计算公式详解

复制代码
PERCENT_RANK = (rank - 1) / (total_rows - 1)

其中:
- rank = RANK() 的值(从 1 开始)
- total_rows = 窗口内的总行数

示例推导:

sql 复制代码
-- 假设有 5 行数据
SELECT 
    name,
    score,
    RANK() OVER (ORDER BY score DESC) AS rank,
    COUNT(*) OVER () AS total,
    (RANK() OVER (ORDER BY score DESC) - 1)::DECIMAL / (COUNT(*) OVER () - 1) AS manual_pr,
    PERCENT_RANK() OVER (ORDER BY score DESC) AS auto_pr
FROM students;

-- 结果:
-- name  | score | rank | total | manual_pr | auto_pr
-- ------+-------+------+-------+-----------+---------
-- 张三  | 100   | 1    | 5     | 0/4=0.00  | 0.00
-- 李四  | 95    | 2    | 5     | 1/4=0.25  | 0.25
-- 王五  | 90    | 3    | 5     | 2/4=0.50  | 0.50
-- 赵六  | 85    | 4    | 5     | 3/4=0.75  | 0.75
-- 钱七  | 80    | 5    | 5     | 4/4=1.00  | 1.00

特殊情况:

  • 如果只有 1 行:(1-1)/(1-1) = 0/0,PostgreSQL 返回 NULL
  • 如果有并列:使用 RANK() 的排名(会跳号)

六、性能优化

1. 避免重复计算

sql 复制代码
-- ❌ 慢:多次调用 PERCENT_RANK
SELECT 
    emp_name,
    PERCENT_RANK() OVER (ORDER BY sales DESC) AS pr,
    CASE 
        WHEN PERCENT_RANK() OVER (ORDER BY sales DESC) <= 0.1 THEN '优秀'
        ELSE '普通'
    END AS level
FROM employees;

-- ✅ 快:用子查询或 CTE
WITH ranked AS (
    SELECT 
        emp_name,
        sales,
        PERCENT_RANK() OVER (ORDER BY sales DESC) AS pr
    FROM employees
)
SELECT 
    emp_name,
    pr,
    CASE WHEN pr <= 0.1 THEN '优秀' ELSE '普通' END AS level
FROM ranked;

2. 合理使用索引

sql 复制代码
-- 为 ORDER BY 字段创建索引
CREATE INDEX idx_employees_sales ON employees (sales_amount);

-- 为 PARTITION BY + ORDER BY 创建复合索引
CREATE INDEX idx_emp_dept_sales ON employees (dept_id, sales_amount);

七、常见错误

错误 1:忘记乘以 100

sql 复制代码
-- ❌ 不直观:0.85 是什么意思?
SELECT PERCENT_RANK() OVER (ORDER BY score) AS pr FROM students;

-- ✅ 清晰:85.00% 一目了然
SELECT ROUND(PERCENT_RANK() OVER (ORDER BY score)::DECIMAL * 100, 2) AS percentile 
FROM students;

错误 2:混淆 ASC 和 DESC

sql 复制代码
-- 场景:分数越高越好
SELECT 
    name,
    score,
    PERCENT_RANK() OVER (ORDER BY score DESC) AS pr_desc,  -- 高分 pr 低
    PERCENT_RANK() OVER (ORDER BY score ASC) AS pr_asc     -- 高分 pr 高
FROM students;

-- 结果:
-- name  | score | pr_desc | pr_asc
-- ------+-------+---------+--------
-- 张三  | 100   | 0.00    | 1.00   ← 最高分
-- 李四  | 60    | 1.00    | 0.00   ← 最低分

--  记忆技巧:
-- ORDER BY DESC(降序):第 1 名 pr=0,最后一名 pr=1
-- ORDER BY ASC(升序):第 1 名 pr=1,最后一名 pr=0

错误 3:在 WHERE 中直接使用

sql 复制代码
-- ❌ 错误
SELECT * FROM employees
WHERE PERCENT_RANK() OVER (ORDER BY salary DESC) <= 0.1;

-- ✅ 正确:用子查询
SELECT * FROM (
    SELECT *, PERCENT_RANK() OVER (ORDER BY salary DESC) AS pr
    FROM employees
) t
WHERE pr <= 0.1;

错误 4:单行数据返回 NULL

sql 复制代码
-- 如果窗口内只有 1 行
SELECT PERCENT_RANK() OVER () AS pr FROM single_row_table;

-- 结果:pr = NULL(因为除以 0)

-- ✅ 处理:用 COALESCE
SELECT COALESCE(PERCENT_RANK() OVER (), 0) AS pr FROM single_row_table;

八、PERCENT_RANK vs CUME_DIST

这两个函数很像,但有细微差别:

函数 公式 最小值 最大值 并列处理
PERCENT_RANK() (rank-1)/(n-1) 0 1 并列值相同
CUME_DIST() 小于等于当前值的行数/n 1/n 1 并列值相同

对比示例:

sql 复制代码
SELECT 
    name,
    score,
    ROUND(PERCENT_RANK() OVER (ORDER BY score DESC)::DECIMAL * 100, 2) AS pr,
    ROUND(CUME_DIST() OVER (ORDER BY score DESC)::DECIMAL * 100, 2) AS cd
FROM students;

-- 结果(假设有并列):
-- name  | score | pr    | cd
-- ------+-------+-------+------
-- 张三  | 100   | 0.00  | 33.33  ← 3 个人并列第 1
-- 李四  | 100   | 0.00  | 33.33
-- 王五  | 100   | 0.00  | 33.33
-- 赵六  | 95    | 37.50 | 66.67
-- 钱七  | 90    | 75.00 | 100.00

-- 💡 区别:
-- PERCENT_RANK 基于排名位置
-- CUME_DIST 基于累积比例

九、记忆口诀

复制代码
PERCENT_RANK 百分位,相对位置看得清
零到一百是范围,小数乘百变百分比
前百分之几好判断,异常检测也轻松
ASC DESC 要注意,高低方向别搞混

十、总结

核心要点

  1. PERCENT_RANK() = 百分比排名(0% - 100%)
  2. 适用场景 = 百分位计算、前 N% 筛选、标准化数据、异常检测
  3. 返回值 = 小数 0.0 - 1.0,通常乘以 100 转成百分比
  4. 计算公式 = (排名 - 1) / (总行数 - 1)
  5. 使用时机 = 需要"相对位置"而非"绝对排名"时

快速参考

sql 复制代码
-- 基本模板
SELECT 
    字段列表,
    ROUND(PERCENT_RANK() OVER (
        PARTITION BY 分组字段     -- 可选
        ORDER BY 排序字段 DESC    -- 必填
    )::DECIMAL * 100, 2) AS percentile
FROM 表名;

-- 前 10%
SELECT * FROM (
    SELECT *, PERCENT_RANK() OVER (ORDER BY sales DESC) AS pr
    FROM employees
) t WHERE pr <= 0.1;

-- 分组百分位
SELECT dept, name, sales,
       ROUND(PERCENT_RANK() OVER (PARTITION BY dept ORDER BY sales DESC)::DECIMAL * 100, 2) AS pct
FROM employees;

实战速查

sql 复制代码
-- 1. 计算百分位
SELECT name, score, 
       ROUND(PERCENT_RANK() OVER (ORDER BY score DESC)::DECIMAL * 100, 2) || '%' AS percentile
FROM students;

-- 2. 找前 20%
SELECT * FROM (
    SELECT *, PERCENT_RANK() OVER (ORDER BY revenue DESC) AS pr
    FROM companies
) t WHERE pr <= 0.2;

-- 3. 部门内对比
SELECT dept, emp, sales,
       ROUND(PERCENT_RANK() OVER (PARTITION BY dept ORDER BY sales DESC)::DECIMAL * 100, 2) AS dept_pct
FROM employees;

-- 4. 异常值检测
SELECT * FROM (
    SELECT *, PERCENT_RANK() OVER (ORDER BY amount ASC) AS pr
    FROM transactions
) t WHERE pr < 0.01 OR pr > 0.99;