Bootstrap

一文弄懂---窗口函数 (大量例子)

窗口函数用在你在不改变查询结果行数的情况下,对数据集中的某一部分(称为“窗口”)执行计算。与传统聚合函数(例如SUM、AVG等)不同。

这意味着你可以对某一行的数据进行分析,同时保留该行在结果中的位置。窗口函数常用于排序、排名、汇总和运行合计等操作。

基础语法

窗口函数名(列名) OVER (窗口定义)
  • 窗口函数名:指具体的函数名,如ROW_NUMBER()RANK()SUM()AVG()等。
  • 列名:进行计算的数据列。
  • 窗口定义:通过PARTITION BYORDER BY子句定义的窗口。PARTITION BY将结果集分成不同的分组,ORDER BY则定义了计算顺序。

示例

想要计算每个 员工在其 所在地区销售额排名

SELECT 
    员工ID,
    销售额,
    RANK() OVER (PARTITION BY 地区 ORDER BY 销售额 DESC) AS 销售排名
FROM 
    销售数据表;
  • PARTITION BY 地区:将数据按地区分组。
  • ORDER BY 销售额 DESC:在每个地区内按销售额从高到低排列。
  • RANK():用于给每个员工在其所在地区内的销售额排名。

常用窗口函数

分类函数名作用
聚合函数AVG()

计算窗口分区内值的平均值。

COUNT()

计算窗口分区内值的数量。

MAX()

计算窗口分区内值的最大值。

MIN()

计算窗口分区内值的最小值。

SUM()

计算窗口分区内值的总和。

排名函数CUME_DIST()

计算窗口分区内小于等于当前值的行数占窗口内总行数的比例。返回值范围为 (0,1]。

DENSE_RANK()

返回窗口分区内值的排名。相同值拥有相同的排名,排名是连续的。例如有两个相同值的排名为 1,则下一个值的排名为 2。

NTILE(n)

将窗口分区内数据按照顺序分成 n 组。

PERCENT_RANK()

计算窗口分区内各行的百分比排名。

RANK()

返回窗口分区内值的排名。相同值拥有相同的排名,排名不是连续的。例如有两个相同值的排名为 1,则下一个值的排名为 3。

ROW_NUMBER()

返回窗口分区内值的唯一排名,从 1 开始。即使值相同,排名也是连续的。例如三个相同值的排名为 1、2、3。

偏移函数FIRST_VALUE(x)

返回窗口分区内第一行的值 x。

LAST_VALUE(x)

返回窗口分区内最后一行的值 x。

LAG(x, offset, default_value)

返回窗口分区内位于当前行上方第 offset 行的值 x。如果不存在该行,则返回 default_value。

LEAD(x, offset, default_value)

返回窗口分区内位于当前行下方第 offset 行的值 x。如果不存在该行,则返回 default_value。

NTH_VALUE(x, offset)

返回窗口分区中第 offset 行的值 x。

函数示例

假设我们有一个销售订单表 orders,包含以下字段:

  • order_id (订单ID)
  • customer_id (客户ID)
  • order_date (订单日期)
  • order_amount (订单金额)

数据准备:

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    order_amount DECIMAL(10, 2)
);

INSERT INTO orders (order_id, customer_id, order_date, order_amount) VALUES
(1, 101, '2024-01-01', 100.00),
(2, 102, '2024-01-01', 150.00),
(3, 101, '2024-01-02', 200.00),
(4, 103, '2024-01-02', 100.00),
(5, 102, '2024-01-03', 250.00),
(6, 101, '2024-01-03', 200.00),
(7, 104, '2024-01-04', 300.00),
(8, 103, '2024-01-04', 100.00);

1. ROW_NUMBER()

场景: 为每个客户的订单按订单日期排序并分配一个唯一的行号。

SQL 语句:

SELECT order_id, customer_id, order_date, order_amount,
       ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date) AS rn
FROM orders;
order_idcustomer_idorder_dateorder_amountrn
11012024-01-01100.001
31012024-01-02200.002
61012024-01-03200.003
21022024-01-01150.001
51022024-01-03250.002
41032024-01-02100.001
81032024-01-04100.002
71042024-01-04300.001

2. RANK()

场景: 按订单金额降序排列所有订单,并分配排名。相同金额的订单排名相同,但下一个排名会跳过。

SQL 语句:

SELECT order_id, order_amount,
       RANK() OVER (ORDER BY order_amount DESC) AS rnk
FROM orders;
order_idorder_amountrnk
7300.001
5250.002
3200.003
6200.003
2150.005
1100.006
4100.006
8100.006

3.DENSE_RANK()

场景:RANK() 类似,但相同金额的订单排名相同,下一个排名不会跳过。

SQL 语句:

SELECT order_id, order_amount,
       DENSE_RANK() OVER (ORDER BY order_amount DESC) AS dense_rnk
FROM orders;
order_idorder_amountdense_rnk
7300.001
5250.002
3200.003
6200.003
2150.004
1100.005
4100.005
8100.005

4. NTILE(n)

原理:

  • 排序: 首先,NTILE() 函数需要对数据进行排序(通过 ORDER BY 子句)。
  • 分组: 然后,它尝试将排序后的数据平均分配到 n 个桶中。
  • 分配桶号: 最后,它为每一行分配一个桶号,从 1 开始,一直到 n

分桶步骤:

  1. 排序: 首先,使用 ORDER BY 子句对数据进行排序。这是最重要的一步,因为分桶是基于排序后的顺序进行的。

  2. 计算每桶的平均行数: 计算 总行数 / n。这将得到每个桶 理想情况下 应该包含的行数。

  3. 计算余数: 计算 总行数 % n。这将得到无法平均分配的剩余行数。

  4. 分配行到桶:

    • 余数 个桶,每个桶分配 平均行数 + 1 行。
    • 剩余的 n - 余数 个桶,每个桶分配 平均行数 行。

场景: 将订单按订单金额分成 4 组。

SQL 语句:

SELECT order_id, order_amount,
       NTILE(4) OVER (ORDER BY order_amount) AS ntile_group
FROM orders;

查询结果:

order_idorder_amountntile_group
1100.001
4100.001
8100.001
2150.002
3200.002
6200.003
5250.003
7300.004

5. LAG()

场景: 获取每个订单的前一个订单的金额。

SQL 语句:

SELECT order_id, order_date, order_amount,
       LAG(order_amount, 1, 0) OVER (ORDER BY order_date) AS previous_order_amount
FROM orders;

查询结果:

order_idorder_dateorder_amountprevious_order_amount
12024-01-01100.000.00
22024-01-01150.00100.00
32024-01-02200.00150.00
42024-01-02100.00200.00
52024-01-03250.00100.00
62024-01-03200.00250.00
72024-01-04300.00200.00
82024-01-04100.00300.00

6. LEAD()

场景: 获取每个订单的后一个订单的金额。

SQL 语句:

SELECT order_id, order_date, order_amount,
       LEAD(order_amount, 1, 0) OVER (ORDER BY order_date) AS next_order_amount
FROM orders;

查询结果:

order_idorder_dateorder_amountnext_order_amount
12024-01-01100.00150.00
22024-01-01150.00200.00
32024-01-02200.00100.00
42024-01-02100.00250.00
52024-01-03250.00200.00
62024-01-03200.00300.00
72024-01-04300.00100.00
82024-01-04100.000.00

7. FIRST_VALUE()

场景: 获取每个客户的第一个订单的金额。

SQL 语句:

SELECT order_id, customer_id, order_date, order_amount,
    FIRST_VALUE(order_amount) OVER
        (PARTITION BY customer_id ORDER BY order_date) AS first_order_amount
FROM orders;

查询结果:

order_idcustomer_idorder_dateorder_amountfirst_order_amount
11012024-01-01100.00100.00
31012024-01-02200.00100.00
61012024-01-03200.00100.00
21022024-01-01150.00150.00
51022024-01-03250.00150.00
41032024-01-02100.00100.00
81032024-01-04100.00100.00
71042024-01-04300.00300.00


 

PARTITION BY 与 GROUP BY

GROUP BY

  • 目的: 将数据行按照一个或多个列的值进行分组,然后对每个组进行聚合计算(例如 SUM(), AVG(), COUNT(), MAX(), MIN())。
  • 结果: 将多个行合并成一个摘要行,每个组对应一行。原始的详细数据行丢失
  • 使用场景: 统计分析、报表生成,例如计算每个部门的平均工资、每个产品的销售总额等。

PARTITION BY

  • 目的: 将数据行按照一个或多个列的值进行分区,但不会将行合并。它为窗口函数定义了计算的范围。
  • 结果: 保留原始的详细数据行,并为每一行计算窗口函数的结果。
  • 使用场景: 需要在每行上执行计算,同时又想保留与其他行的某种逻辑关系时使用,例如计算每个部门的员工工资排名、每个产品在其类别中的销售额占比等。

使用场景对比

有一个员工表 employees,包含以下字段:

  • department (部门)
  • employee_name (员工姓名)
  • salary (工资)

使用 GROUP BY 计算每个部门的平均工资:

SELECT department, AVG(salary) AS average_salary
FROM employees
GROUP BY department;

使用 PARTITION BY 计算每个员工在其部门内的工资排名: 

SELECT department, employee_name, salary, 
    RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS salary_rank
FROM employees;

如果你需要进行汇总统计,则使用 GROUP BY;如果你需要在每行上进行分析或比较,则使用 PARTITION BY

;