在数据处理和分析中,我们经常会遇到「分组排序」「取每组 Top N」「标记组内最新记录」等需求。比如 “按部门分组,给每个部门的员工按薪资排序”“获取每个商品的最新订单”,这类场景如果用传统的 GROUP BY 很难实现,而 SQL 中的窗口函数 ROW_NUMBER() OVER (PARTITION BY ...) 正是解决这类问题的 “利器”。
本文将从原理、语法、实战三个维度,带你彻底搞懂 PARTITION BY 的核心逻辑,让你在实际工作中灵活运用窗口函数。
一、先明确:ROW_NUMBER () OVER (PARTITION BY) 是什么?
ROW_NUMBER() OVER (PARTITION BY ...) 是 SQL 标准中的窗口函数(也叫分析函数),核心作用是:
- 按指定字段(
PARTITION BY后的字段)对数据进行「分组」,把数据拆分成多个独立的 “窗口”; - 在每个窗口内,按指定规则(
ORDER BY后的字段)排序; - 给每个窗口内的每条记录分配一个唯一的连续序号(从 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 个关键步骤
- 数据分组:根据
PARTITION BY后的字段,将整个数据集拆分成 N 个互不重叠的 “窗口”。比如按部门ID分组,每个部门就是一个独立窗口; - 组内排序:对每个窗口内的记录,按
ORDER BY后的字段排序(升序ASC或降序DESC); - 序号分配:在每个窗口内,从 1 开始给排序后的记录分配连续序号,每个窗口的序号独立归零,且序号不重复(即使排序字段值相同,也会分配不同序号)。
直观对比:PARTITION BY vs GROUP BY
假设我们有一张 employee 表:
| 员工 ID (emp_id) | 部门 (department) | 薪资 (salary) | 入职时间 (hire_date) |
|---|---|---|---|
| 1 | 技术部 | 15000 | 2023-01-10 |
| 2 | 技术部 | 12000 | 2023-03-15 |
| 3 | 市场部 | 10000 | 2023-02-20 |
| 4 | 市场部 | 13000 | 2023-01-25 |
用 GROUP BY 按部门查最高薪资:
SELECT department, MAX(salary) AS max_salary FROM employee GROUP BY department;
结果(聚合后每组只剩 1 条):
| department | max_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_id | department | salary | dept_salary_rank |
|---|---|---|---|
| 1 | 技术部 | 15000 | 1 |
| 2 | 技术部 | 12000 | 2 |
| 4 | 市场部 | 13000 | 1 |
| 3 | 市场部 | 10000 | 2 |
从结果能清晰看到:PARTITION BY 没有合并记录,只是给每个部门内部的员工按薪资排序编序号,两个部门的序号都从 1 开始。
三、3 个实战案例:覆盖 90% 工作场景
理论结合实践才是掌握的关键,下面通过 3 个高频场景,带你吃透 PARTITION BY 的用法。
案例 1:取每组的最新记录(最常用)
需求:有一张 order 订单表,获取每个商品(product_id)的最新订单记录(按 create_time 倒序)。
订单表 order 数据:
| 订单 ID (order_id) | 商品 ID (product_id) | 订单金额 (amount) | 创建时间 (create_time) |
|---|---|---|---|
| 1001 | 101 | 299 | 2025-11-20 14:30:00 |
| 1002 | 101 | 399 | 2025-11-21 10:15:00 |
| 1003 | 102 | 199 | 2025-11-20 09:20:00 |
| 1004 | 102 | 299 | 2025-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_id | product_id | amount | create_time | order_rank |
|---|---|---|---|---|
| 1002 | 101 | 399 | 2025-11-21 10:15:00 | 1 |
| 1004 | 102 | 299 | 2025-11-21 15:40:00 | 1 |
案例 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_id | name | subject | score | score_rank |
|---|---|---|---|---|
| 1 | 张三 | 数学 | 95 | 1 |
| 2 | 李四 | 数学 | 92 | 2 |
| 4 | 赵六 | 语文 | 93 | 1 |
| 5 | 孙七 | 语文 | 90 | 2 |
案例 3:多字段分组 + 多字段排序
需求:按「部门 + 岗位」分组,每个分组内按「薪资降序→入职时间升序」排序,给员工编序号(薪资相同则入职早的排前面)。
员工表 employee 扩展数据:
| 员工 ID (emp_id) | 部门 (department) | 岗位 (position) | 薪资 (salary) | 入职时间 (hire_date) |
|---|---|---|---|---|
| 1 | 技术部 | 后端开发 | 15000 | 2023-01-10 |
| 2 | 技术部 | 后端开发 | 15000 | 2022-12-05 |
| 3 | 技术部 | 前端开发 | 13000 | 2023-03-15 |
| 4 | 市场部 | 运营 | 10000 | 2023-02-20 |
| 5 | 市场部 | 运营 | 11000 | 2023-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_id | department | position | salary | hire_date | pos_salary_rank |
|---|---|---|---|---|---|
| 2 | 技术部 | 后端开发 | 15000 | 2022-12-05 | 1 |
| 1 | 技术部 | 后端开发 | 15000 | 2023-01-10 | 2 |
| 3 | 技术部 | 前端开发 | 13000 | 2023-03-15 | 1 |
| 5 | 市场部 | 运营 | 11000 | 2023-01-25 | 1 |
| 4 | 市场部 | 运营 | 10000 | 2023-02-20 | 2 |
👉 关键说明:员工 1 和 2 薪资相同,但员工 2 入职更早,所以序号为 1,体现了多字段排序的优先级。
四、常见误区与注意事项
ORDER BY是必选的:ROW_NUMBER()必须配合ORDER BY使用,否则序号分配无意义(数据库会随机分配序号);- 序号不重复且连续:即使组内排序字段值相同,
ROW_NUMBER()也会分配不同序号(如果需要相同值序号相同,可改用RANK()或DENSE_RANK()); - 多字段分组 / 排序:
PARTITION BY和ORDER BY都支持多个字段,用逗号分隔,按顺序生效; - 与聚合函数的区别:
PARTITION BY是 “分组不聚合”,保留原始记录;GROUP BY是 “分组聚合”,合并记录,二者不可混淆。
五、总结
ROW_NUMBER() OVER (PARTITION BY ...) 的核心价值在于「分组排序不聚合」,它解决了传统 GROUP BY 无法实现的 “组内精细排序” 需求。
记住三个关键词:
- 分组:
PARTITION BY把数据拆分成独立窗口; - 排序:
ORDER BY决定窗口内记录的顺序; - 编序:
ROW_NUMBER()给窗口内记录分配唯一连续序号。