`Window Functions`:`窗口`函数`在`复杂`查询`中的`应用`与`执行`流程`。

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)。帧是当前行周围的一组行,窗口函数将在这些行上进行计算。ROWSRANGE指定了如何定义帧。

常用的窗口函数

以下是一些常用的窗口函数及其说明:

窗口函数 描述
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: 员工ID
  • employee_name: 员工姓名
  • department_id: 部门ID
  • salary: 薪水

我们想计算每个部门内员工的薪水排名。可以使用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: 订单ID
  • user_id: 用户ID
  • order_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的员工,即每个部门薪水最高的员工。

窗口函数的执行流程

理解窗口函数的执行流程对于优化查询性能至关重要。一般来说,窗口函数的执行流程如下:

  1. FROM 和 WHERE 子句执行: 首先,FROM子句确定要查询的表,WHERE子句过滤掉不满足条件的行。
  2. GROUP BY 子句执行: 如果存在GROUP BY子句,则对结果集进行分组。
  3. 窗口函数计算: 窗口函数在分组后的结果集上进行计算。
    • 分区 (Partitioning): 如果存在PARTITION BY子句,则将结果集划分为多个分区。
    • 排序 (Ordering): 如果存在ORDER BY子句,则对每个分区内的行进行排序。
    • 帧定义 (Frame Definition): 如果存在ROWSRANGE子句,则定义每个窗口的帧。
    • 函数应用 (Function Application): 窗口函数应用于每个窗口,计算结果添加到每一行中。
  4. SELECT 子句执行: 最后,SELECT子句选择要返回的列。
  5. ORDER BY 子句执行 (可选): 如果存在ORDER BY 子句(在最外层查询中),则对最终结果集进行排序。

窗口帧的定义

ROWSRANGE子句用于定义窗口的帧,也就是当前行周围的一组行,窗口函数将在这些行上进行计算。

  • 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 BYORDER BY: PARTITION BYORDER BY子句对于窗口函数的计算至关重要。确保它们的定义符合你的需求。
  • 避免不必要的排序: ORDER BY子句会增加查询的开销。如果不需要排序,请避免使用它。
  • 索引优化: 在PARTITION BYORDER 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查询语句。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注