MySQL 高级函数之 ROW_NUMBER()
:数据分区排序排名应用
大家好,今天我们深入探讨 MySQL 中的一个强大的窗口函数:ROW_NUMBER()
。这个函数在数据分析、报表生成、以及需要对数据进行排序和排名的场景中非常有用。我们将从基础概念入手,逐步深入,并通过实际案例展示其强大的功能。
1. 窗口函数基础
在了解 ROW_NUMBER()
之前,我们需要理解什么是窗口函数。 简单来说,窗口函数允许我们在查询结果的“窗口”范围内执行计算,而这个窗口是基于OVER()
子句定义的。与聚合函数(如 COUNT()
, SUM()
, AVG()
)不同,窗口函数不会将多行数据合并成单行,而是为查询结果的每一行都返回一个值。
窗口函数的语法结构通常如下:
window_function(arguments) OVER (
[PARTITION BY column1, column2, ...]
[ORDER BY column1 [ASC | DESC], column2 [ASC | DESC], ...]
[frame_clause]
)
window_function()
: 需要使用的窗口函数,例如ROW_NUMBER()
,RANK()
,DENSE_RANK()
等。OVER()
: 定义窗口的子句。PARTITION BY
: 将数据划分为多个分区,窗口函数将分别在每个分区内执行。 如果没有PARTITION BY
子句,则整个结果集被视为一个分区。ORDER BY
: 定义每个分区内数据的排序方式。 窗口函数的计算是基于这个排序顺序进行的。frame_clause
: 定义窗口的范围(可选)。 例如,可以指定窗口包含当前行的前 N 行和后 M 行。
2. ROW_NUMBER()
函数详解
ROW_NUMBER()
函数的作用很简单:它为每个分区中的每一行分配一个唯一的序列号,从 1 开始。 它的语法如下:
ROW_NUMBER() OVER (
[PARTITION BY column1, column2, ...]
ORDER BY column1 [ASC | DESC], column2 [ASC | DESC], ...
)
ROW_NUMBER()
本身不接受任何参数。 关键在于 OVER()
子句中的 PARTITION BY
和 ORDER BY
子句。
PARTITION BY
(可选): 如前所述,它将结果集划分为多个分区。ROW_NUMBER()
会在每个分区内重新开始计数。 如果没有PARTITION BY
,则整个结果集被视为一个分区。ORDER BY
(必需): 它定义了每个分区内行的排序方式。ROW_NUMBER()
根据这个排序结果分配序列号。 如果ORDER BY
缺失,MySQL 会报错。
3. 示例数据库和表结构
为了更好地演示 ROW_NUMBER()
的用法,我们创建一个示例数据库 company_db
和一个表 employees
:
CREATE DATABASE IF NOT EXISTS company_db;
USE company_db;
CREATE TABLE IF NOT EXISTS employees (
employee_id INT PRIMARY KEY AUTO_INCREMENT,
department_id INT,
employee_name VARCHAR(255),
salary DECIMAL(10, 2)
);
INSERT INTO employees (department_id, employee_name, salary) VALUES
(1, 'Alice', 60000.00),
(1, 'Bob', 55000.00),
(1, 'Charlie', 70000.00),
(2, 'David', 50000.00),
(2, 'Eve', 65000.00),
(2, 'Frank', 65000.00),
(3, 'Grace', 80000.00),
(3, 'Henry', 75000.00),
(3, 'Ivy', 90000.00);
这个表包含员工ID、部门ID、员工姓名和薪水。
4. 简单示例:为所有员工分配排名
以下查询使用 ROW_NUMBER()
为所有员工分配一个基于薪水的排名,薪水最高的员工排名第一:
SELECT
employee_id,
employee_name,
salary,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS salary_rank
FROM
employees;
结果:
employee_id | employee_name | salary | salary_rank |
---|---|---|---|
9 | Ivy | 90000.00 | 1 |
7 | Grace | 80000.00 | 2 |
3 | Charlie | 70000.00 | 3 |
8 | Henry | 75000.00 | 4 |
5 | Eve | 65000.00 | 5 |
6 | Frank | 65000.00 | 6 |
1 | Alice | 60000.00 | 7 |
2 | Bob | 55000.00 | 8 |
4 | David | 50000.00 | 9 |
在这个例子中,OVER (ORDER BY salary DESC)
指定了根据薪水降序排列,并基于这个排序结果分配排名。即使 Eve 和 Frank 薪水相同,他们也会被分配不同的排名 (5 和 6)。 ROW_NUMBER()
保证了每个员工都有唯一的排名。
5. 分区排序:按部门分配排名
现在,我们使用 PARTITION BY
子句,按部门对员工进行排名。 以下查询为每个部门内的员工分配一个基于薪水的排名:
SELECT
employee_id,
department_id,
employee_name,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS department_salary_rank
FROM
employees;
结果:
employee_id | department_id | employee_name | salary | department_salary_rank |
---|---|---|---|---|
3 | 1 | Charlie | 70000.00 | 1 |
1 | 1 | Alice | 60000.00 | 2 |
2 | 1 | Bob | 55000.00 | 3 |
5 | 2 | Eve | 65000.00 | 1 |
6 | 2 | Frank | 65000.00 | 2 |
4 | 2 | David | 50000.00 | 3 |
9 | 3 | Ivy | 90000.00 | 1 |
7 | 3 | Grace | 80000.00 | 2 |
8 | 3 | Henry | 75000.00 | 3 |
在这个例子中,PARTITION BY department_id
将员工按部门划分成不同的分区。 ROW_NUMBER()
在每个部门内部根据薪水降序分配排名。可以看到,每个部门都有一个排名为 1 的员工。 注意,Eve 和 Frank 在同一部门,且薪水相同,但由于 ROW_NUMBER()
的特性,他们仍然被分配了不同的排名(1和2)。具体顺序取决于数据存储顺序。
6. 与 RANK()
和 DENSE_RANK()
的区别
ROW_NUMBER()
, RANK()
, 和 DENSE_RANK()
都是窗口函数,用于分配排名,但它们之间存在关键区别:
ROW_NUMBER()
: 为每个分区中的每一行分配一个唯一的序列号,即使多行具有相同的排序值。 不会跳过排名。RANK()
: 为每个分区中的每一行分配一个排名。 如果多行具有相同的排序值,则它们获得相同的排名,并且下一个排名将被跳过。DENSE_RANK()
: 与RANK()
类似,但不会跳过排名。 如果多行具有相同的排序值,则它们获得相同的排名,下一个排名是连续的。
为了更清晰地理解它们之间的区别,我们添加一些数据,使得在同一个部门有相同的工资:
INSERT INTO employees (department_id, employee_name, salary) VALUES
(1, 'Isaac', 70000.00), -- 与 Charlie 相同
(2, 'Olivia', 65000.00); -- 与 Eve 和 Frank 相同
现在,我们使用 ROW_NUMBER()
, RANK()
, 和 DENSE_RANK()
同时进行排名:
SELECT
employee_id,
department_id,
employee_name,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS row_number_rank,
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS rank_rank,
DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dense_rank_rank
FROM
employees;
结果:
employee_id | department_id | employee_name | salary | row_number_rank | rank_rank | dense_rank_rank |
---|---|---|---|---|---|---|
3 | 1 | Charlie | 70000.00 | 1 | 1 | 1 |
10 | 1 | Isaac | 70000.00 | 2 | 1 | 1 |
1 | 1 | Alice | 60000.00 | 3 | 3 | 2 |
2 | 1 | Bob | 55000.00 | 4 | 4 | 3 |
5 | 2 | Eve | 65000.00 | 1 | 1 | 1 |
6 | 2 | Frank | 65000.00 | 2 | 1 | 1 |
11 | 2 | Olivia | 65000.00 | 3 | 1 | 1 |
4 | 2 | David | 50000.00 | 4 | 4 | 2 |
9 | 3 | Ivy | 90000.00 | 1 | 1 | 1 |
7 | 3 | Grace | 80000.00 | 2 | 2 | 2 |
8 | 3 | Henry | 75000.00 | 3 | 3 | 3 |
观察结果:
- 在 department_id = 1 中,Charlie 和 Isaac 薪水相同。
ROW_NUMBER()
分别给他们分配了排名 1 和 2,而RANK()
和DENSE_RANK()
都给他们分配了排名 1。 接下来,Alice 的RANK()
是 3 (跳过了排名 2),而DENSE_RANK()
是 2 (没有跳过排名)。 - 在 department_id = 2 中,Eve, Frank, 和 Olivia 薪水相同.
ROW_NUMBER()
分别分配排名 1, 2, 3, 而RANK()
和DENSE_RANK()
都给他们分配了排名 1. 接下来, David 的RANK()
是 4 (跳过了排名 2 和 3), 而DENSE_RANK()
是 2 (没有跳过排名)。
7. 实际应用场景
ROW_NUMBER()
在许多实际场景中都非常有用。 以下是一些例子:
-
分页查询: 可以使用
ROW_NUMBER()
为查询结果添加行号,然后通过行号进行分页。SELECT employee_id, employee_name, salary FROM ( SELECT employee_id, employee_name, salary, ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num FROM employees ) AS ranked_employees WHERE row_num BETWEEN 1 AND 5; -- 获取薪水最高的 5 名员工
-
获取每个分组的前 N 条记录: 可以使用
PARTITION BY
和ROW_NUMBER()
来获取每个分组中满足特定条件的前 N 条记录。SELECT employee_id, department_id, employee_name, salary FROM ( SELECT employee_id, department_id, employee_name, salary, ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS department_rank FROM employees ) AS ranked_employees WHERE department_rank <= 2; -- 获取每个部门薪水最高的 2 名员工
-
删除重复数据: 可以使用
ROW_NUMBER()
标记重复的行,然后删除除了第一行之外的所有重复行。 (注意:删除操作需要谨慎,最好先备份数据。)-- 创建一个临时表,包含行号 CREATE TEMPORARY TABLE temp_employees AS SELECT employee_id, department_id, employee_name, salary, ROW_NUMBER() OVER (PARTITION BY department_id, employee_name, salary ORDER BY employee_id) AS row_num FROM employees; -- 删除临时表中行号大于 1 的记录 DELETE FROM employees WHERE employee_id IN (SELECT employee_id FROM temp_employees WHERE row_num > 1); -- 删除临时表 DROP TEMPORARY TABLE IF EXISTS temp_employees;
这个例子中,我们假设
department_id
,employee_name
, 和salary
相同的行是重复的。ROW_NUMBER()
根据这些字段进行分区,并根据employee_id
排序。 然后,我们删除所有row_num
大于 1 的行,只保留每个重复组的第一行。
8. 性能考虑
虽然 ROW_NUMBER()
功能强大,但在处理大量数据时需要注意性能。 窗口函数通常需要对数据进行排序,这可能会消耗大量的 CPU 和内存资源。 以下是一些优化建议:
- 确保有合适的索引: 在
PARTITION BY
和ORDER BY
子句中使用的列上创建索引可以显著提高查询性能。 - 避免在大型数据集上使用复杂的窗口范围 (frame clause): 复杂的窗口范围会增加计算的复杂度。
- 考虑预计算: 如果排名结果不需要实时更新,可以考虑定期预计算并将结果存储在单独的表中。
- 使用 EXPLAIN 分析查询计划: 使用
EXPLAIN
语句可以查看 MySQL 的查询计划,并找出潜在的性能瓶颈。
9. 兼容性
ROW_NUMBER()
函数是 SQL 标准的一部分,并且被大多数主流数据库系统支持,包括 MySQL (8.0 及以上版本), PostgreSQL, SQL Server, Oracle 等。 但是,不同数据库系统的语法和实现细节可能略有不同。 因此,在跨数据库系统迁移代码时,需要仔细检查并进行相应的调整。
函数使用技巧的总结
ROW_NUMBER()
函数是 MySQL 中一个非常实用的窗口函数,它能够在数据分区排序中发挥重要作用。通过灵活运用 PARTITION BY
和 ORDER BY
子句,我们可以轻松实现各种复杂的排名需求。 理解 ROW_NUMBER()
与 RANK()
和 DENSE_RANK()
的区别对于选择正确的排名函数至关重要。