深入浅出 SQL 窗口函数

133 阅读7分钟

在数据处理和分析中,我们经常会遇到「分组排序」「取每组 Top N」「标记组内最新记录」等需求。比如 “按部门分组,给每个部门的员工按薪资排序”“获取每个商品的最新订单”,这类场景如果用传统的 GROUP BY 很难实现,而 SQL 中的窗口函数 ROW_NUMBER() OVER (PARTITION BY ...) 正是解决这类问题的 “利器”。

本文将从原理、语法、实战三个维度,带你彻底搞懂 PARTITION BY 的核心逻辑,让你在实际工作中灵活运用窗口函数。

一、先明确:ROW_NUMBER () OVER (PARTITION BY) 是什么?

ROW_NUMBER() OVER (PARTITION BY ...) 是 SQL 标准中的窗口函数(也叫分析函数),核心作用是:

  1. 按指定字段(PARTITION BY 后的字段)对数据进行「分组」,把数据拆分成多个独立的 “窗口”;
  2. 在每个窗口内,按指定规则(ORDER BY 后的字段)排序;
  3. 给每个窗口内的每条记录分配一个唯一的连续序号(从 1 开始,不重复)。

简单说:PARTITION BY 负责 “分组”,ORDER BY 负责 “组内排序”,ROW_NUMBER() 负责 “给组内记录编序号”。

核心语法结构

ROW_NUMBER() OVER (
    PARTITION BY 分组字段1, 分组字段2  -- 可选:按1个或多个字段分组,组内独立计算
    ORDER BY 排序字段1 DESC/ASC, 排序字段2  -- 必选:组内按1个或多个字段排序(决定序号顺序)
) AS 序号列名

二、PARTITION BY 核心原理:“分组独立计算,序号各自归零”

很多人会把 PARTITION BY 和 GROUP BY 混淆,但二者的核心逻辑完全不同:

  • GROUP BY:是「聚合」,会把同一组的多条记录合并成一条(比如 SUM() 求和、MAX() 取最大值);
  • PARTITION BY:是「分组不聚合」,只是把数据按规则拆分成多个 “独立窗口”,每个窗口内的记录依然保留,序号仅在窗口内有效。

原理拆解:3 个关键步骤

  1. 数据分组:根据 PARTITION BY 后的字段,将整个数据集拆分成 N 个互不重叠的 “窗口”。比如按 部门ID 分组,每个部门就是一个独立窗口;
  2. 组内排序:对每个窗口内的记录,按 ORDER BY 后的字段排序(升序 ASC 或降序 DESC);
  3. 序号分配:在每个窗口内,从 1 开始给排序后的记录分配连续序号,每个窗口的序号独立归零,且序号不重复(即使排序字段值相同,也会分配不同序号)。

直观对比:PARTITION BY vs GROUP BY

假设我们有一张 employee 表:

员工 ID (emp_id)部门 (department)薪资 (salary)入职时间 (hire_date)
1技术部150002023-01-10
2技术部120002023-03-15
3市场部100002023-02-20
4市场部130002023-01-25

用 GROUP BY 按部门查最高薪资:

SELECT department, MAX(salary) AS max_salary FROM employee GROUP BY department;

结果(聚合后每组只剩 1 条):

departmentmax_salary
技术部15000
市场部13000

用 PARTITION BY 按部门分组排序:

SELECT
    emp_id,
    department,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_salary_rank
FROM employee;

结果(分组不聚合,保留所有记录,组内序号独立):

emp_iddepartmentsalarydept_salary_rank
1技术部150001
2技术部120002
4市场部130001
3市场部100002

从结果能清晰看到:PARTITION BY 没有合并记录,只是给每个部门内部的员工按薪资排序编序号,两个部门的序号都从 1 开始。

三、3 个实战案例:覆盖 90% 工作场景

理论结合实践才是掌握的关键,下面通过 3 个高频场景,带你吃透 PARTITION BY 的用法。

案例 1:取每组的最新记录(最常用)

需求:有一张 order 订单表,获取每个商品(product_id)的最新订单记录(按 create_time 倒序)。

订单表 order 数据:

订单 ID (order_id)商品 ID (product_id)订单金额 (amount)创建时间 (create_time)
10011012992025-11-20 14:30:00
10021013992025-11-21 10:15:00
10031021992025-11-20 09:20:00
10041022992025-11-21 15:40:00

SQL 实现:

WITH product_order_rank AS (
    SELECT
        order_id,
        product_id,
        amount,
        create_time,
        -- 按商品ID分组,组内按创建时间倒序排序(最新的排前面)
        ROW_NUMBER() OVER (
            PARTITION BY product_id 
            ORDER BY create_time DESC
        ) AS order_rank
    FROM `order`
)
-- 取每组序号为1的记录(即最新订单)
SELECT * FROM product_order_rank WHERE order_rank = 1;

结果:

order_idproduct_idamountcreate_timeorder_rank
10021013992025-11-21 10:15:001
10041022992025-11-21 15:40:001

案例 2:取每组的 Top N 记录

需求:按「学科」分组,取每个学科成绩前 2 名的学生(按 score 倒序)。

学生成绩表 student_score 数据:

学生 ID (student_id)姓名 (name)学科 (subject)成绩 (score)
1张三数学95
2李四数学92
3王五数学88
4赵六语文93
5孙七语文90
6周八语文85

SQL 实现:

WITH subject_score_rank AS (
    SELECT
        student_id,
        name,
        subject,
        score,
        -- 按学科分组,组内按成绩降序排序
        ROW_NUMBER() OVER (
            PARTITION BY subject 
            ORDER BY score DESC
        ) AS score_rank
    FROM student_score
)
-- 取每个学科前2名(score_rank <= 2)
SELECT * FROM subject_score_rank WHERE score_rank <= 2;

结果:

student_idnamesubjectscorescore_rank
1张三数学951
2李四数学922
4赵六语文931
5孙七语文902

案例 3:多字段分组 + 多字段排序

需求:按「部门 + 岗位」分组,每个分组内按「薪资降序→入职时间升序」排序,给员工编序号(薪资相同则入职早的排前面)。

员工表 employee 扩展数据:

员工 ID (emp_id)部门 (department)岗位 (position)薪资 (salary)入职时间 (hire_date)
1技术部后端开发150002023-01-10
2技术部后端开发150002022-12-05
3技术部前端开发130002023-03-15
4市场部运营100002023-02-20
5市场部运营110002023-01-25

SQL 实现:

SELECT
    emp_id,
    department,
    position,
    salary,
    hire_date,
    -- 多字段分组(部门+岗位),多字段排序(薪资降序→入职时间升序)
    ROW_NUMBER() OVER (
        PARTITION BY department, position 
        ORDER BY salary DESC, hire_date ASC
    ) AS pos_salary_rank
FROM employee;

结果:

emp_iddepartmentpositionsalaryhire_datepos_salary_rank
2技术部后端开发150002022-12-051
1技术部后端开发150002023-01-102
3技术部前端开发130002023-03-151
5市场部运营110002023-01-251
4市场部运营100002023-02-202

👉 关键说明:员工 1 和 2 薪资相同,但员工 2 入职更早,所以序号为 1,体现了多字段排序的优先级。

四、常见误区与注意事项

  1. ORDER BY 是必选的ROW_NUMBER() 必须配合 ORDER BY 使用,否则序号分配无意义(数据库会随机分配序号);
  2. 序号不重复且连续:即使组内排序字段值相同,ROW_NUMBER() 也会分配不同序号(如果需要相同值序号相同,可改用 RANK() 或 DENSE_RANK());
  3. 多字段分组 / 排序PARTITION BY 和 ORDER BY 都支持多个字段,用逗号分隔,按顺序生效;
  4. 与聚合函数的区别PARTITION BY 是 “分组不聚合”,保留原始记录;GROUP BY 是 “分组聚合”,合并记录,二者不可混淆。

五、总结

ROW_NUMBER() OVER (PARTITION BY ...) 的核心价值在于「分组排序不聚合」,它解决了传统 GROUP BY 无法实现的 “组内精细排序” 需求。

记住三个关键词:

  • 分组:PARTITION BY 把数据拆分成独立窗口;
  • 排序:ORDER BY 决定窗口内记录的顺序;
  • 编序:ROW_NUMBER() 给窗口内记录分配唯一连续序号。