Window Functions:复杂查询中的应用与执行流程
大家好,今天我们来深入探讨一下窗口函数(Window Functions)。窗口函数是SQL中一个非常强大的特性,它允许我们在一个查询中对一组相关的行进行计算,而不需要像使用GROUP BY那样对行进行分组。这为我们解决很多复杂的查询问题提供了极大的便利。
什么是窗口函数?
简单来说,窗口函数允许你访问当前行的“窗口”中的数据。这个窗口是查询结果集中的一部分行,它与当前行相关。窗口函数可以执行诸如计算平均值、总和、排名等操作,并且结果会添加到查询结果的每一行中,而不会减少结果集的行数。这与聚合函数(如SUM、AVG)不同,聚合函数会将多行聚合成一行。
窗口函数的基本语法
窗口函数的基本语法如下:
function_name(arguments) OVER (
[PARTITION BY column_list]
[ORDER BY column_list [ASC | DESC]]
[ROWS | RANGE frame_extent]
)
让我们分解一下这个语法:
function_name(arguments)
: 这是你要使用的窗口函数,例如AVG()
,SUM()
,RANK()
,ROW_NUMBER()
等。OVER()
:OVER
关键字表示这是一个窗口函数。括号内定义了窗口的范围和排序规则。PARTITION BY column_list
:PARTITION BY
子句将结果集划分为多个分区。窗口函数会在每个分区内独立计算。类似于GROUP BY
,但不会聚合行。ORDER BY column_list [ASC | DESC]
:ORDER BY
子句定义了每个分区内行的排序方式。这对于某些窗口函数(如RANK()
和LAG()
)非常重要,因为它们依赖于行的顺序。ROWS | RANGE frame_extent
:frame_extent
定义了窗口的帧(frame)。帧是当前行周围的一组行,窗口函数将在这些行上进行计算。ROWS
和RANGE
指定了如何定义帧。
常用的窗口函数
以下是一些常用的窗口函数及其说明:
窗口函数 | 描述 |
---|---|
ROW_NUMBER() |
为结果集中的每一行分配一个唯一的序列号。从1开始,按ORDER BY 子句指定的顺序递增。 |
RANK() |
根据ORDER BY 子句对结果集中的行进行排名。如果存在并列(相同的值),则会跳过后续的排名。例如,如果两行并列第一,则下一行的排名将是3。 |
DENSE_RANK() |
类似于RANK() ,但不会跳过排名。如果存在并列,则后续的排名将紧随其后。例如,如果两行并列第一,则下一行的排名将是2。 |
NTILE(n) |
将结果集划分为n个大小大致相等的桶(bucket),并为每一行分配一个桶号。 |
LAG(column, offset, default) |
访问结果集中当前行之前的行。column 是要访问的列,offset 是要访问的行数(相对于当前行),default 是如果前一行不存在时返回的默认值。 |
LEAD(column, offset, default) |
访问结果集中当前行之后的行。column 是要访问的列,offset 是要访问的行数(相对于当前行),default 是如果后一行不存在时返回的默认值。 |
AVG() , SUM() , MIN() , MAX() |
这些聚合函数也可以用作窗口函数。当用作窗口函数时,它们会在当前行的窗口内计算平均值、总和、最小值或最大值。 |
FIRST_VALUE(column) |
返回窗口中第一行的指定列的值。 |
LAST_VALUE(column) |
返回窗口中最后一行的指定列的值。 |
应用案例
让我们通过一些具体的例子来了解窗口函数的应用。
案例1:计算每个部门的员工薪水排名
假设我们有一个employees
表,包含以下字段:
employee_id
: 员工IDemployee_name
: 员工姓名department_id
: 部门IDsalary
: 薪水
我们想计算每个部门内员工的薪水排名。可以使用RANK()
窗口函数来实现:
SELECT
employee_id,
employee_name,
department_id,
salary,
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
FROM
employees;
在这个查询中:
PARTITION BY department_id
将结果集按部门ID进行分区。ORDER BY salary DESC
按薪水降序排列每个分区内的员工。RANK() OVER (...)
计算每个部门内员工的薪水排名。
案例2:计算每个用户的移动平均订单数量
假设我们有一个orders
表,包含以下字段:
order_id
: 订单IDuser_id
: 用户IDorder_date
: 订单日期order_amount
: 订单金额
我们想计算每个用户过去三个月的移动平均订单数量。可以使用AVG()
窗口函数和ROWS BETWEEN
子句来实现:
SELECT
user_id,
order_date,
COUNT(*) AS order_count,
AVG(COUNT(*)) OVER (PARTITION BY user_id ORDER BY order_date ASC ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_average
FROM
orders
GROUP BY
user_id,
order_date
ORDER BY
user_id,
order_date;
在这个查询中:
- 我们首先使用
GROUP BY
按用户ID和订单日期对订单进行分组,并计算每天的订单数量。 PARTITION BY user_id
将结果集按用户ID进行分区。ORDER BY order_date ASC
按订单日期升序排列每个分区内的订单。ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
定义了一个包含当前行和前两行的窗口帧。AVG(COUNT(*)) OVER (...)
计算每个用户过去三个月的平均订单数量。
案例3:查找每个部门薪水最高的员工
使用RANK()
或者 ROW_NUMBER()
来完成
-- 使用RANK()
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
FROM
RankedEmployees
WHERE
salary_rank = 1;
-- 使用ROW_NUMBER()
WITH RankedEmployees AS (
SELECT
employee_id,
employee_name,
department_id,
salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
FROM
employees
)
SELECT
employee_id,
employee_name,
department_id,
salary
FROM
RankedEmployees
WHERE
salary_rank = 1;
这两个查询都实现了相同的功能。 使用了公共表表达式 (CTE) RankedEmployees
来计算每个部门内员工的薪水排名。 然后,在外部查询中,我们选择排名为1的员工,即每个部门薪水最高的员工。
窗口函数的执行流程
理解窗口函数的执行流程对于优化查询性能至关重要。一般来说,窗口函数的执行流程如下:
- FROM 和 WHERE 子句执行: 首先,
FROM
子句确定要查询的表,WHERE
子句过滤掉不满足条件的行。 - GROUP BY 子句执行: 如果存在
GROUP BY
子句,则对结果集进行分组。 - 窗口函数计算: 窗口函数在分组后的结果集上进行计算。
- 分区 (Partitioning): 如果存在
PARTITION BY
子句,则将结果集划分为多个分区。 - 排序 (Ordering): 如果存在
ORDER BY
子句,则对每个分区内的行进行排序。 - 帧定义 (Frame Definition): 如果存在
ROWS
或RANGE
子句,则定义每个窗口的帧。 - 函数应用 (Function Application): 窗口函数应用于每个窗口,计算结果添加到每一行中。
- 分区 (Partitioning): 如果存在
- SELECT 子句执行: 最后,
SELECT
子句选择要返回的列。 - ORDER BY 子句执行 (可选): 如果存在
ORDER BY
子句(在最外层查询中),则对最终结果集进行排序。
窗口帧的定义
ROWS
和RANGE
子句用于定义窗口的帧,也就是当前行周围的一组行,窗口函数将在这些行上进行计算。
ROWS
:ROWS
子句根据行的物理位置定义帧。它使用行数作为单位。RANGE
:RANGE
子句根据行的逻辑值定义帧。它使用ORDER BY
子句指定的列的值作为单位。
常用的帧定义选项包括:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
: 包括当前行和所有前面的行。ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
: 包括当前行和所有后面的行。ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
: 包括所有行(整个分区)。ROWS BETWEEN n PRECEDING AND CURRENT ROW
: 包括当前行和前面n行。ROWS BETWEEN CURRENT ROW AND n FOLLOWING
: 包括当前行和后面n行。ROWS BETWEEN n PRECEDING AND n FOLLOWING
: 包括当前行,前面n行和后面n行。
RANGE
子句的用法类似,但它基于ORDER BY
子句指定的列的值来定义帧。例如:
RANGE BETWEEN 10 PRECEDING AND CURRENT ROW
: 包括ORDER BY
列的值在当前行值之前10个单位的行,以及当前行。
注意事项和性能优化
- 理解窗口函数的语义: 在使用窗口函数之前,务必理解其语义和行为。特别是对于
RANK()
、DENSE_RANK()
和ROW_NUMBER()
等排名函数,它们的细微差别可能会导致不同的结果。 - 正确使用
PARTITION BY
和ORDER BY
:PARTITION BY
和ORDER BY
子句对于窗口函数的计算至关重要。确保它们的定义符合你的需求。 - 避免不必要的排序:
ORDER BY
子句会增加查询的开销。如果不需要排序,请避免使用它。 - 索引优化: 在
PARTITION BY
和ORDER BY
子句中使用的列上创建索引可以提高查询性能。 - 考虑使用物化视图: 对于复杂的窗口函数查询,可以考虑使用物化视图来预先计算结果,从而提高查询性能。
- 注意数据类型:
RANGE
子句只能用于数值类型或日期类型的列。
窗口函数 vs. 子查询
窗口函数可以用来解决很多子查询可以解决的问题,但窗口函数通常更简洁、更高效。例如,以下两个查询都实现了查找每个部门薪水最高的员工的功能:
使用子查询:
SELECT
e.employee_id,
e.employee_name,
e.department_id,
e.salary
FROM
employees e
WHERE
e.salary = (SELECT MAX(salary) FROM employees WHERE department_id = e.department_id);
使用窗口函数:
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
FROM
RankedEmployees
WHERE
salary_rank = 1;
在大多数情况下,使用窗口函数的查询会比使用子查询的查询更高效,因为窗口函数只需要扫描一次表,而子查询可能需要多次扫描表。
总结,窗口函数在复杂查询中极具价值
窗口函数是SQL中一个强大的工具,它允许我们在一个查询中对一组相关的行进行计算,而无需像使用GROUP BY
那样对行进行分组。这为我们解决很多复杂的查询问题提供了极大的便利。通过理解窗口函数的基本语法、常用函数和执行流程,我们可以编写更简洁、更高效的SQL查询。
理解窗口函数的语法和实际应用,可以帮助我们写出更简洁高效的SQL查询语句。