窗口函数
本文章基于MySQL
数据库进行测试, 其他数据库可能存在差异。
1 什么是窗口函数
1.1介绍
窗口函数(Window Function)是一种在数据库中执行实时分析处理的函数。它可以用于排序并生成序列号,对一组相关的数据行(称为窗口)进行计算,并生成每个窗口内的聚合值。窗口函数允许用户对数据进行更灵活的分析和处理,例如计算排名、分组和计算移动平均等。
窗口函数的使用需要使用OVER
子句来定义窗口的规则,并可以结合聚合函数(如sum、avg、count、max、min等)对数据进行计算。常见的窗口函数包括ROW_NUMBER()
、RANK()
、DENSE_RANK()
、NTILE()
、LAG()
和LEAD()
等。
1.2 语法
窗口函数的语法通常由以下几部分组成:
- 函数名:指定要使用的窗口函数的名称。
- OVER 子句:定义窗口的规则,包括分区(PARTITION BY)和排序规则(ORDER BY)。
- 参数列表:指定窗口函数的参数,例如聚合函数中的参数。
下面是一个示例窗口函数的语法:
<窗口函数名称> OVER ([PARTITION BY <用于分组的列名>] [ORDER BY <用于排序的列名>])
其中,<窗口函数名称>
可以是任何有效的窗口函数名称,例如ROW_NUMBER()
、RANK()
、DENSE_RANK()
等。PARTITION BY
子句用于指定分组规则,将数据分为多个窗口,而ORDER BY
子句则用于指定每个窗口内的排序规则。
[]
中的部分是可以省略的。在有的窗口函数中,PARTITION BY
子句和ORDER BY
子句都是可选的。
-
ROW_NUMBER()
函数在省略PARTITION BY和ORDER BY子句时,会为结果集中的每一行分配一个唯一的序号。而如果在PARTITION BY子句中指定了分组列,则会在每个分组内为每一行分配一个序号。 -
省略PARTITION BY子句将导致在整个结果集中进行计算,而省略ORDER BY子句将导致按照默认顺序(通常是按照行的物理顺序)进行计算。
2 案例数据
-- 员工信息表
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50),
salary DECIMAL(10, 2),
department VARCHAR(50)
);
INSERT INTO employees (id, name, salary, department)
VALUES
(1, 'John Doe', 5000, 'Sales'),
(2, 'Jane Smith', 5500, 'Marketing'),
(3, 'Bob Johnson', 4500, 'HR'),
(4, 'Alice Brown', 5400, 'Sales'),
(5, 'Mike Davis', 4800, 'Marketing'),
(6, 'Sarah Harris', 5400, 'HR'),
(7, 'Chris Thompson', 5300, 'Sales'),
(8, 'Emily Davis', 5400, 'Marketing'),
(9, 'Daniel Wilson', 4900, 'HR'),
(10, 'Olivia Martinez', 5400, 'Sales');
-- 每月实际工资表
CREATE TABLE salary (
id INT PRIMARY KEY COMMENT 'ID',
actual_salary DECIMAL(10, 2) COMMENT '实际工资',
month INT COMMENT '月份',
employee_id INT COMMENT '员工ID'
);
INSERT INTO Salary (id, actual_salary, month, employee_id)
VALUES
(1, 5000, 5, 1),
(2, 5500, 5, 2),
(3, 6000, 5, 3),
(4, 6000, 6, 1),
(5, 6500, 6, 2),
(6, 7000, 6, 3),
(7, 7000, 7, 1),
(8, 7500, 7, 2),
(9, 8000, 7, 3);
id | name | salary | department |
---|---|---|---|
1 | John Doe | 5000 | Sales |
2 | Jane Smith | 5500 | Marketing |
3 | Bob Johnson | 4500 | HR |
4 | Alice Brown | 5400 | Sales |
5 | Mike Davis | 4800 | Marketing |
6 | Sarah Harris | 5400 | HR |
7 | Chris Thompson | 5300 | Sales |
8 | Emily Davis | 5400 | Marketing |
9 | Daniel Wilson | 4900 | HR |
10 | Olivia Martinez | 5400 | Sales |
3 案例
3.1 ROW_NUMBER
ROW_NUMBER
用于为结果集中的每一行分配一个唯一的序号。它通常与OVER
子句一起使用,以指定排序顺序和分区方式。
3.1.1 每个部门的员工根据薪资排序
SELECT
id,
NAME,
salary,
department,
ROW_NUMBER() OVER ( PARTITION BY department ORDER BY salary DESC ) AS row_num
FROM
employees;
id | name | salary | department |
---|---|---|---|
6 | Sarah Harris | 5400.00 | HR |
9 | Daniel Wilson | 4900.00 | HR |
3 | Bob Johnson | 4500.00 | HR |
2 | Jane Smith | 5500.00 | Marketing |
8 | Emily Davis | 5400.00 | Marketing |
5 | Mike Davis | 4800.00 | Marketing |
4 | Alice Brown | 5400.00 | Sales |
10 | Olivia Martinez | 5400.00 | Sales |
7 | Chris Thompson | 5300.00 | Sales |
1 | John Doe | 5000.00 | Sales |
3.1.2 对所有员工的按照薪资进行排序
如果去掉PARTITION BY department
,查询将不再按照部门进行分区,而是对整个结果集进行排序,变成为对所有员工根据薪资进行排序。
SELECT
id,
NAME,
salary,
department,
ROW_NUMBER() OVER ( ORDER BY salary DESC ) AS row_num
FROM
employees;
id | name | salary | department |
---|---|---|---|
2 | Jane Smith | 5500 | Marketing |
4 | Alice Brown | 5400 | Sales |
6 | Sarah Harris | 5400 | HR |
8 | Emily Davis | 5400 | Marketing |
10 | Olivia Martinez | 5400 | Sales |
7 | Chris Thompson | 5300 | Sales |
1 | John Doe | 5000 | Sales |
9 | Daniel Wilson | 4900 | HR |
5 | Mike Davis | 4800 | Marketing |
3 | Bob Johnson | 4500 | HR |
3.1.2 去除ORDER BY salary DESC
虽然不知道这个查询是否意义,但是还是展示一下查询结果
SELECT
id,
NAME,
salary,
department,
ROW_NUMBER() OVER ( PARTITION BY department ) AS row_num
FROM
employees;
id | name | salary | department |
---|---|---|---|
3 | Bob Johnson | 4500 | HR |
6 | Sarah Harris | 5400 | HR |
9 | Daniel Wilson | 4900 | HR |
2 | Jane Smith | 5500 | Marketing |
5 | Mike Davis | 4800 | Marketing |
8 | Emily Davis | 5400 | Marketing |
1 | John Doe | 5000 | Sales |
4 | Alice Brown | 5400 | Sales |
7 | Chris Thompson | 5300 | Sales |
10 | Olivia Martinez | 5400 | Sales |
3.2 RANK和DENSE_RANK
3.2.1 RANK()函数:
RANK()
函数返回结果集中每一行的排名,按照指定的排序顺序进行排名。默认情况下,RANK()
函数使用升序排序,并返回每个值的位置。如果存在相同的值,则并列排名,下一个排名会跳过并列的数量。
SELECT
id,
NAME,
salary,
department,
RANK() OVER ( ORDER BY salary DESC ) AS rn
FROM
employees;
id | name | salary | department | rn |
---|---|---|---|---|
2 | Jane Smith | 5500 | Marketing | 1 |
4 | Alice Brown | 5400 | Sales | 2 |
6 | Sarah Harris | 5400 | HR | 2 |
8 | Emily Davis | 5400 | Marketing | 2 |
10 | Olivia Martinez | 5400 | Sales | 2 |
7 | Chris Thompson | 5300 | Sales | 6 |
1 | John Doe | 5000 | Sales | 7 |
9 | Daniel Wilson | 4900 | HR | 8 |
5 | Mike Davis | 4800 | Marketing | 9 |
3 | Bob Johnson | 4500 | HR | 10 |
3.2.2 DENSE_RANK()函数:
DENSE_RANK()
函数与RANK()函数类似,都用于对结果集进行排名。但是,DENSE_RANK()
函数不会跳过并列排名的数量。即使存在相同的值,下一个排名仍然是连续的。
SELECT
id,
NAME,
salary,
department,
DENSE_RANK() OVER ( ORDER BY salary DESC ) AS rn
FROM
employees;
id | name | salary | department | rn |
---|---|---|---|---|
2 | Jane Smith | 5500 | Marketing | 1 |
4 | Alice Brown | 5400 | Sales | 2 |
6 | Sarah Harris | 5400 | HR | 2 |
8 | Emily Davis | 5400 | Marketing | 2 |
10 | Olivia Martinez | 5400 | Sales | 2 |
7 | Chris Thompson | 5300 | Sales | 3 |
1 | John Doe | 5000 | Sales | 4 |
9 | Daniel Wilson | 4900 | HR | 5 |
5 | Mike Davis | 4800 | Marketing | 6 |
3 | Bob Johnson | 4500 | HR | 7 |
3.3 NTILE
NTILE()
用于将数据集分为n个相等的百分位数,每个百分位包含相同数量的数据, 计算方法是将数据集中的数据按照指定的百分位数进行分组,每个组包含相同数量的数据。每个组对应一个百分位数,因此总共有n个百分位数,每个百分位数包含的数据数量相同。
SELECT
id,
NAME,
salary,
department,
NTILE( 4 ) OVER ( ORDER BY salary ) AS quartile
FROM
Employees;
在这个查询中,我们使用了NTILE
函数将员工按照工资水平分为四个组。每个组对应一个百分位数,即前25%、25%-50%、50%-75%和75%-100%。每个员工所属的组由NTILE
函数计算得到,并使用quartile
列表示。
id | name | salary | department | quartile |
---|---|---|---|---|
3 | Bob Johnson | 4500 | HR | 1 |
5 | Mike Davis | 4800 | Marketing | 1 |
9 | Daniel Wilson | 4900 | HR | 1 |
1 | John Doe | 5000 | Sales | 2 |
7 | Chris Thompson | 5300 | Sales | 2 |
4 | Alice Brown | 5400 | Sales | 2 |
6 | Sarah Harris | 5400 | HR | 3 |
8 | Emily Davis | 5400 | Marketing | 3 |
10 | Olivia Martinez | 5400 | Sales | 4 |
2 | Jane Smith | 5500 | Marketing | 4 |
3.4 LAG和LEAD
LAG
和LEAD
是窗口函数,用于获取当前行之前或之后的行的值,常用于时间序列数据或需要计算移动平均线等场合。
lead(EXPR,,)
lag(EXPR,,)
EXPR
:一般是列名,指定要获取之前或之后值的列;也可以是从其他行返回的表达式。OFFSET
:这是一个整数,指定了要偏移的行数。它指定了要获取之前或之后多少行的值。在函数中,OFFSET通常为1,表示获取当前行之前的1行的值。DEFAULT
:这是一个可选参数,指定当没有更早的行可用时返回的默认值。如果没有更早的行可用,将返回默认值。
3.4.1 LAG函数
LAG
函数用于获取当前行之前的指定列的值, 第一行显示null
。它需要指定要获取值的列和偏移量。偏移量指定了要获取之前多少行的值。
3.4.1.1 查询员工当月以及上月实际工资是多少
SELECT
employee_id,
MONTH,
actual_salary,
LAG( actual_salary ) OVER ( PARTITION BY employee_id ORDER BY MONTH ) AS previous_month_salary
FROM
salary;
employee_id | month | actual_salary | previous_month_salary |
---|---|---|---|
1 | 5 | 5000 | |
1 | 6 | 6000 | 5000 |
1 | 7 | 7000 | 6000 |
2 | 5 | 5500 | |
2 | 6 | 6500 | 5500 |
2 | 7 | 7500 | 6500 |
3 | 5 | 6000 | |
3 | 6 | 7000 | 6000 |
3 | 7 | 8000 | 7000 |
3.4.1.2 查询员工当月以及两个月前的实际工资是多少
SELECT
employee_id,
MONTH,
actual_salary,
LAG( actual_salary, 2 ) OVER ( PARTITION BY employee_id ORDER BY MONTH ) AS previous_month_salary
FROM
salary;
employee_id | month | actual_salary | previous_month_salary |
---|---|---|---|
1 | 5 | 5000 | |
1 | 6 | 6000 | |
1 | 7 | 7000 | 5000 |
2 | 5 | 5500 | |
2 | 6 | 6500 | |
2 | 7 | 7500 | 5500 |
3 | 5 | 6000 | |
3 | 6 | 7000 | |
3 | 7 | 8000 | 6000 |
3.4.1.3 查询员工当月以及上月实际工资是多少,第一个月返回0
SELECT
employee_id,
MONTH,
actual_salary,
LAG( actual_salary, 1, 0 ) OVER ( PARTITION BY employee_id ORDER BY MONTH ) AS previous_month_salary
FROM
salary;
employee_id | month | actual_salary | previous_month_salary |
---|---|---|---|
1 | 5 | 5000 | 0 |
1 | 6 | 6000 | 5000 |
1 | 7 | 7000 | 6000 |
2 | 5 | 5500 | 0 |
2 | 6 | 6500 | 5500 |
2 | 7 | 7500 | 6500 |
3 | 5 | 6000 | 0 |
3 | 6 | 7000 | 6000 |
3 | 7 | 8000 | 7000 |
3.4.2 LEAD函数
LEAD
函数用于获取当前行之后的指定列的值,最后一行显示null
。它同样需要指定要获取值的列和偏移量。偏移量指定了要获取之后多少行的值。
比如,查询员工当月以及下个月实际工资是多少
SELECT
employee_id,
MONTH,
actual_salary,
LEAD( actual_salary ) OVER ( PARTITION BY employee_id ORDER BY MONTH ) AS previous_month_salary
FROM
salary;
employee_id | month | actual_salary | previous_month_salary |
---|---|---|---|
1 | 5 | 5000 | 6000 |
1 | 6 | 6000 | 7000 |
1 | 7 | 7000 | |
2 | 5 | 5500 | 6500 |
2 | 6 | 6500 | 7500 |
2 | 7 | 7500 | |
3 | 5 | 6000 | 7000 |
3 | 6 | 7000 | 8000 |
3 | 7 | 8000 |
关于LEAD()
其他参数不再举例,详情请见LAG()
函数
3.5 FIRST_VALUE与LAST_VALUE
FIRST_VALUE
和LAST_VALUE
用于获取查询结果集中每行的第一个值或最后一个值。它们通常在排序的窗口中使用,以便在每组中进行聚合操作。
FIRST_VALUE (EXPR)
LAST_VALUE (EXPR)
EXPR
:一般是列名,指定要获取之前或之后值的列;也可以是从其他行返回的表达式。
3.5.1 FIRST_VALUE
FIRST_VALUE
函数返回指定列的第一个值。
比如,获取每个员工每月的工资以及首月工资
SELECT
employee_id,
MONTH,
actual_salary,
FIRST_VALUE( actual_salary ) OVER ( PARTITION BY employee_id ORDER BY MONTH ) AS first_salary
FROM
salary;
employee_id | month | actual_salary | first_salary |
---|---|---|---|
1 | 5 | 5000 | 5000 |
1 | 6 | 6000 | 5000 |
1 | 7 | 7000 | 5000 |
2 | 5 | 5500 | 5500 |
2 | 6 | 6500 | 5500 |
2 | 7 | 7500 | 5500 |
3 | 5 | 6000 | 6000 |
3 | 6 | 7000 | 6000 |
3 | 7 | 8000 | 6000 |
3.5.2 LAST_VALUE
LAST_VALUE
函数返回指定列的最后一个值,。
比如,获取每个员工每月的工资以及最后一月工资
SELECT
employee_id,
MONTH,
actual_salary,
LAST_VALUE( actual_salary ) OVER ( PARTITION BY employee_id ORDER BY MONTH ) AS last_salary
FROM
salary;
employee_id | month | actual_salary | last_salary |
---|---|---|---|
1 | 5 | 5000 | 5000 |
1 | 6 | 6000 | 6000 |
1 | 7 | 7000 | 7000 |
2 | 5 | 5500 | 5500 |
2 | 6 | 6500 | 6500 |
2 | 7 | 7500 | 7500 |
3 | 5 | 6000 | 6000 |
3 | 6 | 7000 | 7000 |
3 | 7 | 8000 | 8000 |
看到这里,你可能会好奇,没啥没达到理想的效果,请看下面这句sql
SELECT
employee_id,
month,
actual_salary,
LAST_VALUE(actual_salary) OVER (PARTITION BY employee_id ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_salary
FROM salary;
employee_id | month | actual_salary | last_salary |
---|---|---|---|
1 | 5 | 5000 | 7000 |
1 | 6 | 6000 | 7000 |
1 | 7 | 7000 | 7000 |
2 | 5 | 5500 | 7500 |
2 | 6 | 6500 | 7500 |
2 | 7 | 7500 | 7500 |
3 | 5 | 6000 | 8000 |
3 | 6 | 7000 | 8000 |
3 | 7 | 8000 | 8000 |
为啥加上ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
, 这就涉及到一个叫**窗口帧(window frame
)**的东西;
这里就不展开叙述,请看Window Function Frame Specification。
3.6 统计函数(MAX、MIN、AVG、SUM、COUNT)
这里简单举几个例子;
3.6.1 MAX
SELECT
employee_id,
month,
actual_salary,
MAX(actual_salary) OVER (PARTITION BY employee_id) AS max_salary
FROM
salary;
employee_id | month | actual_salary | max_salary |
---|---|---|---|
1 | 5 | 5000 | 7000 |
1 | 6 | 6000 | 7000 |
1 | 7 | 7000 | 7000 |
2 | 5 | 5500 | 7500 |
2 | 6 | 6500 | 7500 |
2 | 7 | 7500 | 7500 |
3 | 5 | 6000 | 8000 |
3 | 6 | 7000 | 8000 |
3 | 7 | 8000 | 8000 |
3.6.2 MIN
SELECT
employee_id,
month,
actual_salary,
MIN(actual_salary) OVER (PARTITION BY employee_id) AS min_salary
FROM
salary;
employee_id | month | actual_salary | min_salary |
---|---|---|---|
1 | 5 | 5000 | 5000 |
1 | 6 | 6000 | 5000 |
1 | 7 | 7000 | 5000 |
2 | 5 | 5500 | 5500 |
2 | 6 | 6500 | 5500 |
2 | 7 | 7500 | 5500 |
3 | 5 | 6000 | 6000 |
3 | 6 | 7000 | 6000 |
3 | 7 | 8000 | 6000 |
3.6.3 AVG
SELECT
employee_id,
month,
actual_salary,
AVG(actual_salary) OVER (PARTITION BY employee_id) AS avg_salary
FROM
salary;
employee_id | month | actual_salary | avg_salary |
---|---|---|---|
1 | 5 | 5000 | 6000 |
1 | 6 | 6000 | 6000 |
1 | 7 | 7000 | 6000 |
2 | 5 | 5500 | 6500 |
2 | 6 | 6500 | 6500 |
2 | 7 | 7500 | 6500 |
3 | 5 | 6000 | 7000 |
3 | 6 | 7000 | 7000 |
3 | 7 | 8000 | 7000 |
3.6.4 SUM
SELECT
employee_id,
month,
actual_salary,
SUM(actual_salary) OVER (PARTITION BY employee_id) AS sum_salary
FROM
salary;
employee_id | month | actual_salary | sum_salary |
---|---|---|---|
1 | 5 | 5000 | 18000 |
1 | 6 | 6000 | 18000 |
1 | 7 | 7000 | 18000 |
2 | 5 | 5500 | 19500 |
2 | 6 | 6500 | 19500 |
2 | 7 | 7500 | 19500 |
3 | 5 | 6000 | 21000 |
3 | 6 | 7000 | 21000 |
3 | 7 | 8000 | 21000 |
3.6.5 COUNT
SELECT
employee_id,
month,
actual_salary,
COUNT(actual_salary) OVER (PARTITION BY employee_id) AS count_salary
FROM
salary;
employee_id | month | actual_salary | count_salary |
---|---|---|---|
1 | 5 | 5000 | 3 |
1 | 6 | 6000 | 3 |
1 | 7 | 7000 | 3 |
2 | 5 | 5500 | 3 |
2 | 6 | 6500 | 3 |
2 | 7 | 7500 | 3 |
3 | 5 | 6000 | 3 |
3 | 6 | 7000 | 3 |
3 | 7 | 8000 | 3 |