MySQL高级功能:窗口函数
MySQL高级功能:窗口函数
-
窗口函数简介
核心概念:窗口函数对一组与当前行相关的行执行计算。关键点在于,它不会像
GROUP BY
那样将多行合并为单个输出行,而是为每一行返回一个值,同时保留原始行的所有细节。你可以把它想象成:为每一行数据都开一个“窗口”,这个“窗口”定义了计算所基于的数据集(如同行、前几行、后几行、整个分区等),然后在这个窗口上进行计算,并将结果直接附加到当前行上。
-
窗口函数基本语法
SELECTcolumn1,column2,window_function(column3) OVER ([PARTITION BY partition_expression][ORDER BY order_expression [ASC | DESC]][frame_clause]) AS alias_name FROM table_name;
核心组成部分解析:
window_function
: 要使用的窗口函数名称(例如ROW_NUMBER
,SUM
,RANK
等)。OVER
子句: 定义窗口的规则。这是窗口函数的灵魂。PARTITION BY
: 类似于GROUP BY
,它将结果集划分为不同的分区(组)。窗口函数会独立地应用于每个分区。如果省略,整个结果集就是一个分区。ORDER BY
: 定义分区内数据的排序方式。对于排名函数(如RANK
)和计算累计和的函数(如SUM
)至关重要。它还会影响frame_clause
的默认行为。frame_clause
: 定义当前行所在分区中的一个子集(窗口帧)。语法通常是ROWS BETWEEN start AND end
。start
/end
可以是:UNBOUNDED PRECEDING
:分区的第一行UNBOUNDED FOLLOWING
:分区的最后一行n PRECEDING
:当前行之前的第 n 行n FOLLOWING
:当前行之后的第 n 行CURRENT ROW
:当前行
- 示例:
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
表示窗口包含当前行、前一行和后一行。
-
常用窗口函数分类及示例
-
序号函数 (Ranking Functions):为每一行分配一个唯一的序号或排名。
-
ROW_NUMBER()
: 为分区内的每一行分配一个唯一的连续序号(无论值是否相同)。-- 为每个部门的员工按销售额排名(同分也不同名) SELECT *,ROW_NUMBER() OVER (PARTITION BY department ORDER BY amount DESC) AS dept_sale_rank FROM sales;
-
RANK()
: 分配排名,值相同时排名相同,但会留下空位(例如:1, 2, 2, 4)。-- 排名,同分则同名,下一个排名会跳号 SELECT *,RANK() OVER (ORDER BY amount DESC) AS overall_rank FROM sales;
-
DENSE_RANK()
: 分配排名,值相同时排名相同,且不会留下空位(例如:1, 2, 2, 3)。-- 排名,同分则同名,下一个排名不跳号 SELECT *,DENSE_RANK() OVER (ORDER BY amount DESC) AS `dense_rank` FROM sales;
-
-
分布函数 (Distribution Functions): 计算每行在其分区中的百分比排名(公式:
(rank - 1) / (rows - 1)
)。-
NTILE(n)
: 将分区内的数据分成n
个大致相等的组,并分配组号。-- 将总销售额按金额分为4个桶 SELECT *,NTILE(4) OVER (ORDER BY amount DESC) AS quartile FROM sales;
-
-
前后值函数 (Value Functions):访问同一分区中其他行的值。
-
LAG(column, offset, default_value)
: 返回当前行之前offset
行的值,若不存在则为default_value
。-- 查看每位员工上一次的销售额 SELECT employee, sale_date, amount,LAG(amount, 1, 0) OVER (PARTITION BY employee ORDER BY sale_date) AS prev_sale_amount FROM sales;
-
LEAD(column, offset, default_value)
: 返回当前行之后offset
行的值,若不存在则为default_value
。-- 查看每位员工下一次的销售额 SELECT employee, sale_date, amount,LEAD(amount, 1, 0) OVER (PARTITION BY employee ORDER BY sale_date) AS next_sale_amount FROM sales;
-
FIRST_VALUE(column)
/LAST_VALUE(column)
: 返回窗口帧内的第一个/最后一个值。-- 查看每个部门内的最高销售额(显示在每一行) SELECT *,FIRST_VALUE(amount) OVER (PARTITION BY department ORDER BY amount DESC) AS dept_highest_sale FROM sales;
-
-
聚合函数 (Aggregate Functions as Window Functions):所有标准聚合函数(如
SUM
,AVG
,COUNT
,MAX
,MIN
)都可以用作窗口函数。-
SUM()
用于计算累计和:-- 计算每位员工的累计销售额 SELECT employee, sale_date, amount,SUM(amount) OVER (PARTITION BY employee ORDER BY sale_date) AS running_total FROM sales;
-
AVG()
用于计算移动平均:-- 计算每位员工最近3天的平均销售额(包括当天) SELECT employee, sale_date, amount,AVG(amount) OVER (PARTITION BY employee ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg FROM sales;
-
-
-
注意事项
- 版本要求:窗口函数从 MySQL 8.0 版本开始才被支持。如果你使用的是旧版本,将无法利用这些功能。
LAST_VALUE()
的陷阱:直接使用LAST_VALUE()
可能无法得到分区最后一行,因为其默认窗口范围是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
。要获取最后一行,通常需要显式指定框架:LAST_VALUE(expr) OVER (ORDER BY cols RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
。
-
附录
假设我们有一张
salse
表:有以下数据-- 创建 sales 表 CREATE TABLE sales (sale_id INT PRIMARY KEY AUTO_INCREMENT,employee VARCHAR(50) NOT NULL,department VARCHAR(50) NOT NULL,sale_date DATE NOT NULL,amount DECIMAL(10, 2) NOT NULL );-- 插入示例数据 INSERT INTO sales (employee, department, sale_date, amount) VALUES -- 销售部门数据 ('Alice', 'Sales', '2023-10-01', 100.00), ('Bob', 'Sales', '2023-10-01', 150.00), ('Alice', 'Sales', '2023-10-02', 110.00), ('Bob', 'Sales', '2023-10-02', 120.00), ('Charlie', 'Sales', '2023-10-02', 200.00), -- 新员工 ('Alice', 'Sales', '2023-10-03', 130.00), ('Bob', 'Sales', '2023-10-03', 140.00), ('Charlie', 'Sales', '2023-10-03', 180.00), ('Alice', 'Sales', '2023-10-04', 120.00), -- 销售额下降 ('Bob', 'Sales', '2023-10-04', 160.00), ('Charlie', 'Sales', '2023-10-04', 220.00), -- 最高销售额-- 市场部门数据 ('David', 'Marketing', '2023-10-01', 300.00), -- 高价值销售 ('Eva', 'Marketing', '2023-10-01', 250.00), ('David', 'Marketing', '2023-10-02', 280.00), ('Eva', 'Marketing', '2023-10-02', 270.00), ('Frank', 'Marketing', '2023-10-02', 320.00), -- 新员工,高销售额 ('David', 'Marketing', '2023-10-03', 310.00), ('Eva', 'Marketing', '2023-10-03', 260.00), ('Frank', 'Marketing', '2023-10-03', 330.00), ('David', 'Marketing', '2023-10-04', 290.00), -- 销售额下降 ('Eva', 'Marketing', '2023-10-04', 280.00), ('Frank', 'Marketing', '2023-10-04', 350.00), -- 最高销售额-- IT部门数据(较少销售记录) ('Grace', 'IT', '2023-10-02', 500.00), -- 非常高价值的销售 ('Henry', 'IT', '2023-10-03', 450.00), ('Grace', 'IT', '2023-10-04', 550.00), -- 最高销售额-- 添加一些相同销售额的记录,用于测试排名函数 ('Ivy', 'Sales', '2023-10-05', 200.00), -- 与Charlie某天销售额相同 ('Jack', 'Sales', '2023-10-05', 200.00); -- 与Charlie某天销售额相同
现在你可以使用这个数据集测试各种窗口函数:
- 部门内销售额排名
SELECT *,ROW_NUMBER() OVER (PARTITION BY department ORDER BY amount DESC) AS `row_num`,RANK() OVER (PARTITION BY department ORDER BY amount DESC) AS `rank`,DENSE_RANK() OVER (PARTITION BY department ORDER BY amount DESC) AS `dense_rank` FROM sales ORDER BY department, amount DESC;
-
员工销售额累计
SELECT employee, sale_date, amount,SUM(amount) OVER (PARTITION BY employee ORDER BY sale_date) AS running_total FROM sales ORDER BY employee, sale_date;
-
部门内移动平均(3天内
amount
的平均值)SELECT department, sale_date, amount,AVG(amount) OVER (PARTITION BY department ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg FROM sales ORDER BY department, sale_date;
-
访问前后行数据
SELECT employee, sale_date, amount,LAG(amount, 1) OVER (PARTITION BY employee ORDER BY sale_date) AS prev_day_sale,LEAD(amount, 1) OVER (PARTITION BY employee ORDER BY sale_date) AS next_day_sale,amount - LAG(amount, 1) OVER (PARTITION BY employee ORDER BY sale_date) AS day_over_day_change FROM sales ORDER BY employee, sale_date;
-
部门内销售额占比
SELECT employee, department, sale_date, amount,amount / SUM(amount) OVER (PARTITION BY department, sale_date) * 100 AS pct_of_daily_dept_sales FROM sales ORDER BY department, sale_date, amount DESC;
- 还有许多应用场景…