窗口函数用在你在不改变查询结果行数的情况下,对数据集中的某一部分(称为“窗口”)执行计算。与传统聚合函数(例如SUM、AVG等)不同。
这意味着你可以对某一行的数据进行分析,同时保留该行在结果中的位置。窗口函数常用于排序、排名、汇总和运行合计等操作。
基础语法
窗口函数名(列名) OVER (窗口定义)
- 窗口函数名:指具体的函数名,如
ROW_NUMBER()
、RANK()
、SUM()
、AVG()
等。 - 列名:进行计算的数据列。
- 窗口定义:通过
PARTITION BY
和ORDER 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_id | customer_id | order_date | order_amount | rn |
---|---|---|---|---|
1 | 101 | 2024-01-01 | 100.00 | 1 |
3 | 101 | 2024-01-02 | 200.00 | 2 |
6 | 101 | 2024-01-03 | 200.00 | 3 |
2 | 102 | 2024-01-01 | 150.00 | 1 |
5 | 102 | 2024-01-03 | 250.00 | 2 |
4 | 103 | 2024-01-02 | 100.00 | 1 |
8 | 103 | 2024-01-04 | 100.00 | 2 |
7 | 104 | 2024-01-04 | 300.00 | 1 |
2. RANK()
场景: 按订单金额降序排列所有订单,并分配排名。相同金额的订单排名相同,但下一个排名会跳过。
SQL 语句:
SELECT order_id, order_amount,
RANK() OVER (ORDER BY order_amount DESC) AS rnk
FROM orders;
order_id | order_amount | rnk |
---|---|---|
7 | 300.00 | 1 |
5 | 250.00 | 2 |
3 | 200.00 | 3 |
6 | 200.00 | 3 |
2 | 150.00 | 5 |
1 | 100.00 | 6 |
4 | 100.00 | 6 |
8 | 100.00 | 6 |
3.DENSE_RANK()
场景: 与 RANK()
类似,但相同金额的订单排名相同,下一个排名不会跳过。
SQL 语句:
SELECT order_id, order_amount,
DENSE_RANK() OVER (ORDER BY order_amount DESC) AS dense_rnk
FROM orders;
order_id | order_amount | dense_rnk |
---|---|---|
7 | 300.00 | 1 |
5 | 250.00 | 2 |
3 | 200.00 | 3 |
6 | 200.00 | 3 |
2 | 150.00 | 4 |
1 | 100.00 | 5 |
4 | 100.00 | 5 |
8 | 100.00 | 5 |
4. NTILE(n)
原理:
- 排序: 首先,
NTILE()
函数需要对数据进行排序(通过ORDER BY
子句)。 - 分组: 然后,它尝试将排序后的数据平均分配到 n 个桶中。
- 分配桶号: 最后,它为每一行分配一个桶号,从 1 开始,一直到 n。
分桶步骤:
-
排序: 首先,使用
ORDER BY
子句对数据进行排序。这是最重要的一步,因为分桶是基于排序后的顺序进行的。 -
计算每桶的平均行数: 计算
总行数 / n
。这将得到每个桶 理想情况下 应该包含的行数。 -
计算余数: 计算
总行数 % n
。这将得到无法平均分配的剩余行数。 -
分配行到桶:
- 前
余数
个桶,每个桶分配平均行数 + 1
行。 - 剩余的
n - 余数
个桶,每个桶分配平均行数
行。
- 前
场景: 将订单按订单金额分成 4 组。
SQL 语句:
SELECT order_id, order_amount,
NTILE(4) OVER (ORDER BY order_amount) AS ntile_group
FROM orders;
查询结果:
order_id | order_amount | ntile_group |
---|---|---|
1 | 100.00 | 1 |
4 | 100.00 | 1 |
8 | 100.00 | 1 |
2 | 150.00 | 2 |
3 | 200.00 | 2 |
6 | 200.00 | 3 |
5 | 250.00 | 3 |
7 | 300.00 | 4 |
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_id | order_date | order_amount | previous_order_amount |
---|---|---|---|
1 | 2024-01-01 | 100.00 | 0.00 |
2 | 2024-01-01 | 150.00 | 100.00 |
3 | 2024-01-02 | 200.00 | 150.00 |
4 | 2024-01-02 | 100.00 | 200.00 |
5 | 2024-01-03 | 250.00 | 100.00 |
6 | 2024-01-03 | 200.00 | 250.00 |
7 | 2024-01-04 | 300.00 | 200.00 |
8 | 2024-01-04 | 100.00 | 300.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_id | order_date | order_amount | next_order_amount |
---|---|---|---|
1 | 2024-01-01 | 100.00 | 150.00 |
2 | 2024-01-01 | 150.00 | 200.00 |
3 | 2024-01-02 | 200.00 | 100.00 |
4 | 2024-01-02 | 100.00 | 250.00 |
5 | 2024-01-03 | 250.00 | 200.00 |
6 | 2024-01-03 | 200.00 | 300.00 |
7 | 2024-01-04 | 300.00 | 100.00 |
8 | 2024-01-04 | 100.00 | 0.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_id | customer_id | order_date | order_amount | first_order_amount |
---|---|---|---|---|
1 | 101 | 2024-01-01 | 100.00 | 100.00 |
3 | 101 | 2024-01-02 | 200.00 | 100.00 |
6 | 101 | 2024-01-03 | 200.00 | 100.00 |
2 | 102 | 2024-01-01 | 150.00 | 150.00 |
5 | 102 | 2024-01-03 | 250.00 | 150.00 |
4 | 103 | 2024-01-02 | 100.00 | 100.00 |
8 | 103 | 2024-01-04 | 100.00 | 100.00 |
7 | 104 | 2024-01-04 | 300.00 | 300.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
。