使用 PARTITION BY 和 RANK/DENSE_RANK 查询部门内薪资 Top 2

使用 PARTITION BY 和 RANK/DENSE_RANK 查询部门内薪资 Top 2

在 SQL 查询中,获取每个分组内的前 N 条记录(如每个部门内薪资最高的员工)是一项常见需求。本文将讲解如何使用 PARTITION BY 结合 RANK()DENSE_RANK() 窗口函数,查询部门内薪资排名前 2 的员工。

PARTITION BY 和 RANK()/DENSE_RANK() 详解

什么是 PARTITION BY?

PARTITION BY 是 SQL 窗口函数的一部分,用于将数据分成多个分区(类似于 GROUP BY,但不会聚合数据)。每个分区独立执行窗口函数的计算,保留原始行。

  • 作用 :将数据按指定列分组,例如按 department_id 分区,每个部门的数据形成一个独立的分组。

  • 语法

    sql 复制代码
    OVER (PARTITION BY column_name ORDER BY another_column)
    • PARTITION BY column_name:指定分区的列。
    • ORDER BY another_column:指定分区内的排序规则。

RANK() 和 DENSE_RANK()

RANK()DENSE_RANK() 是窗口函数,用于为每行生成排名,结合 PARTITION BY 可在每个分区内计算排名。

  • RANK()

    • 为每行分配排名,相同值的记录排名相同。

    • 排名会跳跃,例如:1,1,3(如果有两个第一名,下一名是第三)。

    • 示例:

      sql 复制代码
      RANK() OVER (PARTITION BY department_id ORDER BY salary DESC)

      按部门分区,薪资从高到低排序,生成排名。

  • DENSE_RANK()

    • RANK() 类似,但排名不跳跃,例如:1,1,2(两个第一名后,下一名是第二)。

    • 示例:

      sql 复制代码
      DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC)
  • 区别

    • 如果数据中有重复值,RANK() 可能导致排名"空缺",而 DENSE_RANK() 保持连续。
    • 选择哪种函数取决于业务需求:RANK() 适合严格按顺序排名,DENSE_RANK() 适合更紧凑的排名。

窗口函数的优势

与传统的 GROUP BY 不同,PARTITION BY 结合 RANK()DENSE_RANK()

  • 保留所有行数据,而不是聚合为一行。
  • 可以在每个分区内动态计算排名,适合复杂的排序需求。

解决方案分析

问题背景

假设有一个员工表 employees,包含以下字段:

  • employee_id:员工 ID
  • employee_name:员工姓名
  • department_id:部门 ID
  • salary:薪资

目标:查询每个部门内薪资排名前 2 的员工信息。

表结构与示例数据

employees 表数据如下:

employee_id employee_name department_id salary
1 Alice 101 5000
2 Bob 101 6000
3 Charlie 101 5500
4 David 102 7000
5 Eve 102 6500
6 Frank 103 4500

SQL 查询

使用 PARTITION BYRANK() 实现:

sql 复制代码
WITH RankedEmployees AS (
    SELECT
        employee_id,
        employee_name,
        department_id,
        salary,
        RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
    FROM
        employees
)
SELECT
    employee_id,
    employee_name,
    department_id,
    salary,
    salary_rank
FROM
    RankedEmployees
WHERE
    salary_rank <= 2;

查询结果

结果如下:

employee_id employee_name department_id salary salary_rank
2 Bob 101 6000 1
3 Charlie 101 5500 2
4 David 102 7000 1
5 Eve 102 6500 2
6 Frank 103 4500 1

步骤解析

  1. PARTITION BY department_id
    • 将数据按 department_id 分区,例如部门 101、102、103 各为一个分区。
  2. ORDER BY salary DESC
    • 在每个分区内按薪资从高到低排序。
  3. RANK()
    • 为每行生成排名,部门 101 中 Bob(6000)排名 1,Charlie(5500)排名 2。
  4. CTE(WITH 子句)
    • 因为 RANK() 不能直接在 WHERE 中使用,需通过 CTE 或子查询存储临时结果。
  5. WHERE salary_rank <= 2
    • 筛选每个部门排名 1 和 2 的记录。

使用 DENSE_RANK()

如果改用 DENSE_RANK()

sql 复制代码
WITH RankedEmployees AS (
    SELECT
        employee_id,
        employee_name,
        department_id,
        salary,
        DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
    FROM
        employees
)
SELECT
    employee_id,
    employee_name,
    department_id,
    salary,
    salary_rank
FROM
    RankedEmployees
WHERE
    salary_rank <= 2;

结果与 RANK() 相同(因示例数据无重复薪资)。若有重复薪资,DENSE_RANK() 可能包含更多记录。

注意事项

  • 性能 :大数据量下,窗口函数可能较慢,建议为 department_idsalary 添加索引。
  • 重复薪资 :若部门内薪资重复,RANK()DENSE_RANK() 可能返回超过 2 条记录。需额外条件(如 employee_id)控制。
  • 空分区:无员工的部门不会出现在结果中。
  • RANK() vs DENSE_RANK():选择取决于是否需要连续排名。

总结

PARTITION BY 结合 RANK()DENSE_RANK() 是查询分组内 Top N 的高效方法。通过分区和排名,我们可以灵活处理复杂的排序需求。本例展示了如何查询部门内薪资 Top 2,适用于类似场景。希望这篇文章帮你深入理解 SQL 窗口函数!

相关推荐
Asthenia041214 分钟前
为什么说MVCC无法彻底解决幻读的问题?
后端
Asthenia041215 分钟前
面试官问我:三级缓存可以解决循环依赖的问题,那两级缓存可以解决Spring的循环依赖问题么?是不是无法解决代理对象的问题?
后端
Asthenia041216 分钟前
面试复盘:使用 perf top 和火焰图分析程序 CPU 占用率过高
后端
Asthenia041217 分钟前
面试复盘:varchar vs char 以及 InnoDB 表大小的性能分析
后端
Asthenia041217 分钟前
面试问题解析:InnoDB中NULL值是如何记录和存储的?
后端
Asthenia041243 分钟前
面试官问我:TCP发送到IP存在但端口不存在的报文会发生什么?
后端
Asthenia041244 分钟前
HTTP 相比 TCP 的好处是什么?
后端
Asthenia04121 小时前
MySQL count(*) 哪个存储引擎更快?为什么 MyISAM 更快?
后端
Asthenia04121 小时前
面试官问我:UDP发送到IP存在但端口不存在的报文会发生什么?
后端
Asthenia04121 小时前
深入理解 TCP backlog 参数:意义、应用场景与 Netty 中的应用
后端