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

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

"并列排名,不跳号,最紧凑"

就像学校成绩排名:如果两个人并列第一,下一个就是第二,不会跳过。

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

DENSE = 密集的、紧凑的,所以叫"密集排名"。


二、三大排名函数对比

函数 相同值处理 示例 特点 适用场景
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,DENSE 不跳
-- 赵六  | 90    | 4       | 4    | 3
-- 钱七  | 90    | 5       | 4    | 3      ← 又一对并列
-- 孙八  | 85    | 6       | 6    | 4      ← RANK 跳过 5,DENSE 只到 4

三、8 个实用场景

场景 1:学生成绩排名(最经典)

需求: 给全班学生排名,相同分数排名相同

sql 复制代码
SELECT 
    student_id,
    name,
    score,
    DENSE_RANK() OVER (ORDER BY score DESC) AS rank
FROM exam_scores
ORDER BY rank;

为什么用 DENSE_RANK?

  • 如果用 ROW_NUMBER():两个 100 分的学生,一个第 1,一个第 2,不公平
  • 如果用 RANK():两个 100 分后,下一个 95 分变成第 3 名,学生家长会问"第 2 名去哪了?"
  • DENSE_RANK():100 分都是第 1,95 分是第 2,合情合理

场景 2:等级划分(ABCDE 级)

需求: 根据销售额划分等级:前 10% A 级,10-30% B 级,30-60% C 级...

sql 复制代码
SELECT 
    emp_name,
    sales_amount,
    DENSE_RANK() OVER (ORDER BY sales_amount DESC) AS rank,
    COUNT(*) OVER () AS total_count,
    CASE 
        WHEN DENSE_RANK() OVER (ORDER BY sales_amount DESC)::DECIMAL / COUNT(*) OVER () <= 0.1 THEN 'A 级'
        WHEN DENSE_RANK() OVER (ORDER BY sales_amount DESC)::DECIMAL / COUNT(*) OVER () <= 0.3 THEN 'B 级'
        WHEN DENSE_RANK() OVER (ORDER BY sales_amount DESC)::DECIMAL / COUNT(*) OVER () <= 0.6 THEN 'C 级'
        ELSE 'D 级'
    END AS grade
FROM sales_performance;

优势: 比固定阈值更灵活,自动适应数据分布


场景 3:找出前 N 个不同值

需求: 找出销售额最高的 5 个不同金额(可能有多个员工对应同一金额)

sql 复制代码
SELECT DISTINCT sales_amount
FROM (
    SELECT 
        sales_amount,
        DENSE_RANK() OVER (ORDER BY sales_amount DESC) AS rank
    FROM sales_performance
) t
WHERE rank <= 5;

与 ROW_NUMBER 的区别:

  • ROW_NUMBER():返回前 5 条记录(可能只有 3 个不同金额)
  • DENSE_RANK():返回前 5 个不同金额(可能有 10 条记录)

场景 4:分组排名(每个部门内部)

需求: 每个部门内按业绩排名

sql 复制代码
SELECT 
    dept_name,
    emp_name,
    sales_amount,
    DENSE_RANK() OVER (PARTITION BY dept_name ORDER BY sales_amount DESC) AS dept_rank
FROM employees
ORDER BY dept_name, dept_rank;

结果示例:

复制代码
dept_name | emp_name | sales_amount | dept_rank
----------+----------+--------------+----------
销售部    | 张三     | 100000       | 1
销售部    | 李四     | 100000       | 1      ← 并列
销售部    | 王五     | 95000        | 2
技术部    | 赵六     | 80000        | 1
技术部    | 钱七     | 75000        | 2

场景 5:计算百分位排名

需求: 计算每个员工的业绩在公司的百分位

sql 复制代码
SELECT 
    emp_name,
    sales_amount,
    DENSE_RANK() OVER (ORDER BY sales_amount DESC) AS rank,
    COUNT(DISTINCT sales_amount) OVER () AS unique_count,
    ROUND(
        (DENSE_RANK() OVER (ORDER BY sales_amount DESC) - 1)::DECIMAL / 
        NULLIF(COUNT(DISTINCT sales_amount) OVER (), 0) * 100, 
        2
    ) AS percentile
FROM sales_performance;

解读:

  • percentile = 0:最高业绩(第 1 名)
  • percentile = 50:中等水平
  • percentile = 99:接近最低

场景 6:检测数据分布

需求: 分析销售金额的分布情况

sql 复制代码
SELECT 
    DENSE_RANK() OVER (ORDER BY sales_amount DESC) AS rank,
    COUNT(*) AS emp_count,
    AVG(sales_amount) AS avg_sales,
    MIN(sales_amount) AS min_sales,
    MAX(sales_amount) AS max_sales
FROM (
    SELECT 
        emp_name,
        sales_amount,
        DENSE_RANK() OVER (ORDER BY sales_amount DESC) AS rank
    FROM sales_performance
) t
GROUP BY rank
ORDER BY rank
LIMIT 10;  -- 看前 10 个档次的分布

用途: 快速了解业绩集中度,是否有断层


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

需求: 删除重复订单,但金额和时间完全相同的订单都保留

sql 复制代码
-- 使用 DENSE_RANK 标记
SELECT 
    id,
    order_no,
    amount,
    created_at,
    DENSE_RANK() OVER (
        PARTITION BY order_no, amount, created_at 
        ORDER BY id
    ) AS rn
FROM orders;

-- 所有 rn=1 的都是唯一或并列的记录
-- rn>1 的是真正的重复(完全相同的字段)
DELETE FROM orders
WHERE id IN (
    SELECT id FROM (
        SELECT 
            id,
            DENSE_RANK() OVER (
                PARTITION BY order_no, amount, created_at 
                ORDER BY id
            ) AS rn
        FROM orders
    ) t
    WHERE rn > 1
);

场景 8:生成连续序号用于报表

需求: 报表需要连续的序号列,即使数据有并列

sql 复制代码
SELECT 
    DENSE_RANK() OVER (ORDER BY category, sub_category) AS item_no,
    category,
    sub_category,
    product_name,
    sales_amount
FROM products
ORDER BY item_no;

优势: 序号连续美观,适合打印报表


四、核心语法

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

关键点:

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

五、性能优化

1. 避免多次调用

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

-- ✅ 快:用子查询或 CTE
WITH ranked AS (
    SELECT 
        emp_name,
        sales,
        DENSE_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_employees_sales ON employees (sales_amount DESC);

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

3. 减少窗口范围

sql 复制代码
--  如果不需要分组,不要加 PARTITION BY
DENSE_RANK() OVER (PARTITION BY 1 ORDER BY id)  -- 多余!

-- ✅ 直接全局排名
DENSE_RANK() OVER (ORDER BY id)

六、常见错误

错误 1:在 WHERE 中直接使用

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

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

错误 2:忘记 ORDER BY

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

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

错误 3:混淆 RANK 和 DENSE_RANK

sql 复制代码
-- 场景:找前 10 名员工
-- 用 RANK:如果有并列,可能返回超过 10 人
SELECT * FROM (
    SELECT *, RANK() OVER (ORDER BY sales DESC) AS rank
    FROM employees
) t WHERE rank <= 10;  -- 可能返回 12 人(如果有并列)

-- 用 DENSE_RANK:保证最多 10 个不同档次
SELECT * FROM (
    SELECT *, DENSE_RANK() OVER (ORDER BY sales DESC) AS rank
    FROM employees
) t WHERE rank <= 10;  -- 最多 10 个档次,但人数可能很多

-- 用 ROW_NUMBER:严格 10 人
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY sales DESC) AS rn
    FROM employees
) t WHERE rn <= 10;  -- 严格 10 人

七、选择指南:什么时候用 DENSE_RANK?

场景 推荐函数 原因
学生成绩排名 DENSE_RANK() 家长能理解"第 2 名"
员工绩效考核 DENSE_RANK() 等级划分清晰
奥运奖牌榜 RANK() 并列金牌,没有银牌
分页查询 ROW_NUMBER() 需要精确行数
找前 N 个不同值 DENSE_RANK() 关注档次而非人数
生成报表序号 DENSE_RANK() 序号连续美观

八、记忆口诀

复制代码
DENSE_RANK 不跳号,并列之后接着跑
成绩排名最合适,等级划分也巧妙
若要严格控人数,ROW_NUMBER 来效劳
奥运奖牌要跳号,RANK 函数不能少

九、总结

核心要点

  1. DENSE_RANK() = 并列排名,不跳号(1, 1, 2, 3)
  2. 适用场景 = 成绩排名、等级划分、前 N 个不同值
  3. 最大优势 = 排名连续,符合人类直觉
  4. 与 RANK 区别 = 是否跳号
  5. 使用时机 = 需要"公平且紧凑"的排名时

快速参考

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

实战速查

sql 复制代码
-- 1. 全班排名
SELECT name, score, DENSE_RANK() OVER (ORDER BY score DESC) AS rank
FROM students;

-- 2. 部门内排名
SELECT dept, name, sales, 
       DENSE_RANK() OVER (PARTITION BY dept ORDER BY sales DESC) AS dept_rank
FROM employees;

-- 3. 前 10% 等级
SELECT name, sales,
       CASE WHEN DENSE_RANK() OVER (ORDER BY sales DESC)::DECIMAL / 
                 COUNT(*) OVER () <= 0.1 THEN 'A' ELSE 'B' END AS grade
FROM employees;

-- 4. 找前 5 个不同金额
SELECT DISTINCT amount FROM (
    SELECT amount, DENSE_RANK() OVER (ORDER BY amount DESC) AS rn
    FROM orders
) t WHERE rn <= 5;