PostgreSQL RANK() 窗口函数完全解

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

"并列排名,但会跳号"

就像奥运会奖牌榜:如果两个人并列第一,就没有第二名,直接跳到第三名。

复制代码
张三: 100分 → 第 1 名
李四: 100分 → 第 1 名  ← 并列
王五: 95分  → 第 3 名  ← 跳过第 2 名!
赵六: 90分  → 第 4 名

二、和 ROW_NUMBER() 的区别

函数 相同值处理 示例 特点
ROW_NUMBER() 强制不同 1, 2, 3, 4 即使分数相同,排名也不同
RANK() 并列,跳号 1, 1, 3, 4 有并列时,后面的排名会跳过
DENSE_RANK() 并列,不跳号 1, 1, 2, 3 紧凑排名,最符合直觉

直观对比:

sql 复制代码
SELECT 
    name,
    score,
    ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
    RANK() OVER (ORDER BY score DESC) AS rank,
    DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank
FROM students;

-- 结果:
-- name  | score | row_num | rank | dense_rank
-- ------+-------+---------+------+------------
-- 张三  | 100   | 1       | 1    | 1
-- 李四  | 100   | 2       | 1    | 1      ← 并列第一
-- 王五  | 95    | 3       | 3    | 2      ← RANK 跳过 2
-- 赵六  | 90    | 4       | 4    | 3

三、8 个实用场景

场景 1:生成排行榜(允许并列)

需求: 销售排行榜,业绩相同的销售人员排名相同

sql 复制代码
SELECT 
    emp_name,
    sales_amount,
    RANK() OVER (ORDER BY sales_amount DESC) AS rank
FROM sales_performance
WHERE rank <= 10;  -- 前 10 名(可能超过 10 人,因为有并列)

为什么用 RANK 不用 ROW_NUMBER?

  • 如果用 ROW_NUMBER():两个业绩相同的人,一个排第 1,一个排第 2,不公平
  • RANK():两人都是第 1 名,公平合理

场景 2:找出所有并列第一名

需求: 找出考试成绩最高的所有学生(可能有多个)

sql 复制代码
SELECT * FROM (
    SELECT 
        student_id,
        name,
        score,
        RANK() OVER (ORDER BY score DESC) AS rank
    FROM exam_scores
) t
WHERE rank = 1;  -- 所有并列第一的学生

优势:MAX() + 子查询更简洁


场景 3:分段奖励(金银铜牌)

需求: 给前 3 名发金牌,4-10 名发银牌,11-50 名发铜牌

sql 复制代码
SELECT 
    emp_name,
    sales_amount,
    RANK() OVER (ORDER BY sales_amount DESC) AS rank,
    CASE 
        WHEN RANK() OVER (ORDER BY sales_amount DESC) <= 3 THEN '🥇 金牌'
        WHEN RANK() OVER (ORDER BY sales_amount DESC) <= 10 THEN '🥈 银牌'
        WHEN RANK() OVER (ORDER BY sales_amount DESC) <= 50 THEN ' 铜牌'
        ELSE '参与奖'
    END AS award
FROM sales_performance;

注意: 如果有并列,可能导致某个奖项人数超预期(比如 5 个人并列第 3,那金牌就有 5 个)。


场景 4:检测数据异常(跳号分析)

需求: 找出订单编号不连续的地方

sql 复制代码
SELECT 
    order_no,
    created_at,
    RANK() OVER (ORDER BY order_no) AS expected_no,
    order_no - RANK() OVER (ORDER BY order_no) AS gap
FROM orders
WHERE order_no - RANK() OVER (ORDER BY order_no) > 0;  -- 有缺口的地方

原理: 如果编号连续,order_no - RANK() 应该始终是同一个值;如果出现变化,说明中间有缺失。


场景 5:分组后取每组的前 N 名(含并列)

需求: 每个部门业绩最好的员工(包括并列)

sql 复制代码
SELECT * FROM (
    SELECT 
        dept_name,
        emp_name,
        sales_amount,
        RANK() OVER (PARTITION BY dept_name ORDER BY sales_amount DESC) AS rank
    FROM employees
) t
WHERE rank <= 3;  -- 每个部门前 3 名(含并列)

与 ROW_NUMBER 的区别:

  • ROW_NUMBER():每个部门严格 3 个人
  • RANK():如果第 3 名有并列,可能返回 4、5 个人

场景 6:计算累计占比

需求: 计算每个销售额在总销售额中的累计占比

sql 复制代码
SELECT 
    emp_name,
    sales_amount,
    SUM(sales_amount) OVER (ORDER BY sales_amount DESC) AS running_total,
    SUM(sales_amount) OVER () AS grand_total,
    ROUND(
        SUM(sales_amount) OVER (ORDER BY sales_amount DESC)::DECIMAL / 
        SUM(sales_amount) OVER () * 100, 
        2
    ) AS cumulative_percent
FROM sales_performance;

结合 RANK 使用:

sql 复制代码
-- 查看前 20% 的销售人员贡献了多少业绩
SELECT * FROM (
    SELECT 
        emp_name,
        sales_amount,
        RANK() OVER (ORDER BY sales_amount DESC) AS rank,
        COUNT(*) OVER () AS total_count
    FROM sales_performance
) t
WHERE rank::DECIMAL / total_count <= 0.2;  -- 前 20%

场景 7:去重但保留并列记录

需求: 删除重复订单,但如果金额和时间完全相同,保留所有

sql 复制代码
-- ❌ ROW_NUMBER 会只保留一条
DELETE FROM orders
WHERE id IN (
    SELECT id FROM (
        SELECT 
            id,
            order_no,
            ROW_NUMBER() OVER (PARTITION BY order_no ORDER BY id) AS rn
        FROM orders
    ) t
    WHERE rn > 1
);

-- ✅ RANK 会保留所有并列的
DELETE FROM orders
WHERE id IN (
    SELECT id FROM (
        SELECT 
            id,
            order_no,
            RANK() OVER (PARTITION BY order_no, amount, created_at ORDER BY id) AS rn
        FROM orders
    ) t
    WHERE rn > 1
);

场景 8:生成序号但允许空缺

需求: 给学生分配座位号,但如果有请假,座位号跳过

sql 复制代码
SELECT 
    student_id,
    name,
    status,
    CASE 
        WHEN status = '请假' THEN NULL
        ELSE RANK() OVER (
            PARTITION BY CASE WHEN status != '请假' THEN 1 ELSE 0 END 
            ORDER BY student_id
        )
    END AS seat_number
FROM students
ORDER BY student_id;

四、核心语法

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

关键点:

  1. 不需要参数RANK() 括号里是空的
  2. 必须配合 OVER():声明这是窗口函数
  3. ORDER BY 必填:决定排名顺序
  4. PARTITION BY 可选:是否分组排名

五、性能优化

1. 避免重复计算

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

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

2. 合理使用索引

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

-- 这样 RANK() 计算会更快
SELECT 
    emp_name,
    sales_amount,
    RANK() OVER (ORDER BY sales_amount DESC) AS rank
FROM sales_performance;

六、常见错误

错误 1:在 WHERE 中直接使用

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

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

错误 2:忘记 ORDER BY

sql 复制代码
-- ❌ 错误:RANK 必须有排序规则
RANK() OVER (PARTITION BY dept_id)

-- ✅ 正确
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC)

错误 3:误解跳号逻辑

sql 复制代码
-- 假设有 3 个人并列第 1
SELECT 
    name,
    score,
    RANK() OVER (ORDER BY score DESC) AS rank
FROM students;

-- 结果:
-- name  | score | rank
-- ------+-------+------
-- 张三  | 100   | 1
-- 李四  | 100   | 1
-- 王五  | 100   | 1
-- 赵六  | 95    | 4    ← 不是 2!跳过了 2 和 3

七、RANK vs DENSE_RANK 选择指南

场景 推荐函数 原因
奥运会奖牌榜 RANK() 并列金牌,没有银牌
学生成绩排名 DENSE_RANK() 并列第一,下一个是第二
分页查询 ROW_NUMBER() 需要精确控制行数
前 N 名(含并列) RANK() 确保不遗漏并列者
紧凑排名 DENSE_RANK() 排名连续,无空缺

八、记忆口诀

复制代码
RANK 排名会跳号,并列之后空位跑
奥运奖牌最典型,金银铜牌不颠倒
若要排名更紧凑,DENSE_RANK 来效劳
分页去重用 ROW,场景不同选对宝

九、总结

核心要点

  1. RANK() = 并列排名,但会跳号(1, 1, 3, 4)
  2. 适用场景 = 排行榜、找并列第一、分段奖励
  3. 与 ROW_NUMBER 区别 = 相同值处理方式不同
  4. 与 DENSE_RANK 区别 = 是否跳号
  5. 使用时机 = 需要"公平排名"且允许空缺时

快速参考

sql 复制代码
-- 基本模板
SELECT * FROM (
    SELECT 
        字段列表,
        RANK() OVER (
            PARTITION BY 分组字段     -- 可选
            ORDER BY 排序字段 DESC    -- 必填
        ) AS rank
    FROM 表名
) t
WHERE rank <= N;  -- 前 N 名(含并列)