MySQL8新特性:窗口函数(最新超详细)(内含使用场景及示例)

本文将介绍MySQL8的新特性——窗口函数,从概念、语法、示例等各方面详细解析窗口函数,以及解析面试中窗口函数常考的题目和知识点。
窗口函数(Windows Function),也被称为分析函数(Analytic Functions),在Oracle中使用已久,但直到MySQL8才开始引入。窗口函数非常强大,在工作中应用非常广泛,专门用于处理复杂的统计分析问题,例如移动平均、累计总和、百分比排名等,它可直接添加新的聚合字段,而不必在数据库应用程序级别编写额外的代码。

环境:MySQL 8.0.32

下文语法中[]包括的内容表示可选项

以下示例均用下表以及下表数据进行演示:

-- 创建一个销售表(sales),其中包含销售日期(sale_date)、销售员ID(sale_id)和销售额(sale_amount)
CREATE TABLE sales (  
    id INT AUTO_INCREMENT PRIMARY KEY,  
    sale_date DATE,  
    sale_id INT,  
    sale_amount DECIMAL(10, 2)  
);  
  
-- 插入一些示例数据  
INSERT INTO sales (sale_date, sale_id, sale_amount) VALUES  
('2023-01-01', 1, 1000.00),  
('2023-01-02', 1, 1200.00),  
('2023-01-03', 2, 800.00),  
('2023-01-03', 1, 1000.00),  
('2023-01-05', 2, 900.00),  
('2023-01-01', 3, 700.00),  
('2023-01-02', 3, 750.00),  
('2023-01-03', 4, 600.00),  
('2023-01-04', 3, 800.00),  
('2023-01-05', 4, 650.00);

一、 概念

窗口函数会对数据进行分组,每个分组即为一个窗口。和分组聚合不同,窗口函数会对窗口中每一条记录进行应用,不会使记录变少(每一行返回一个结果),其重点是当前行与窗口以及窗口中其他行的联系。

二、语法

基本语法

SELECT 
	WINDOW_FUNCTION() OVER(
		[PARTITION BY `字段1`, `字段2`, ...]
		[ORDER BY `字段3`, `字段4`]
		[FRAME_CLAUSE]
	) [AS `别名`]
FROM `表名` ;

注意: 窗口函数可使用别名

语法解析

WINDOW_FUNCTION
MySQL8支持以下几类窗口函数:

  • 序号函数:用于为窗口内的每一行生成一个序号,例如 ROW_NUMBER(),RANK(),DENSE_RANK() 等。
  • 分布函数:用于计算窗口内的每一行在整个分区中的相对位置,例如 PERCENT_RANK(),CUME_DIST() 等。
  • 前后函数:用于获取窗口内的当前行的前后某一行的值,例如 LAG(),LEAD() 等。
  • 头尾函数:用于获取窗口内的第一行或最后一行的值,例如 FIRST_VALUE(),LAST_VALUE() 等。
  • 聚合函数:用于计算窗口内的某个字段的聚合值,例如 SUM(),AVG(),MIN(),MAX() 等。

OVER
OVER关键字用于标识是否使用窗口函数,有两种用法:

  • OVER(),常规用法,窗口规范放在括号中。如窗口规范为空,表示没有窗口划分,默认所有数据为一组。
  • OVER window_name:由FROM后的WINDOW子句定义窗口规范,可重复使用。下文进阶语法-命名窗口中会进行详细解析。

PARTITION BY
用于将记录划分为不同的分区,窗口函数在每个分区上分别执行。可以使用多个字段进行分区。

ORDER BY
用于将每个分区中的记录进行排序,窗口函数将按照排序后的顺序进行计算。可使用多个字段进行排序,默认ASC(升序)。

FRAME_CLAUSE
框架子句,用于指定每个分区的数据范围。下文进进阶语法-框架中会进行详细解析

进阶语法

命名窗口WINDOW AS

有时候多次重复定义或引用相同的窗口,很繁琐。为了避免重复定义,减少语句的繁琐,可以在WINDOW子句中定义并命名窗口,并在OVER中通过窗口名进行引用。一次定义,多次引用。
语法定义如下:

SELECT  
	WINDOW_FUNCTION() OVER `窗口名`|(`窗口名` ...)
FROM `表名` 
WINDOW `窗口名` AS ()

说明:

  • WINDOW子句中的括号内的部分就是原OVER子句后的窗口定义。使用OVER关键字调用窗口时,可直接引用窗口名或者可对窗口进行进一步的加工,使用括号括起来并在其中使用其他的窗口规范。
  • 一个命名窗口的定义本身也可以以一个窗口名开头。这样可以实现窗口之间的引用,但不能形成循环。

示例:

-- 查询每个销售员最早和最晚的销售日期
SELECT 
	DISTINCT `sale_id`,
	FIRST_VALUE(`sale_date`) OVER (w ORDER BY `sale_date` DESC) first_date_desc,
	FIRST_VALUE(`sale_date`) OVER w first_date_asc
FROM `salse`
WINDOW w AS (PARTITION BY `sale_id`); 
-- 定义了三个命名窗口,其中第二个和第三个都引用了第一个,这样每个窗口就可以根据不同的范围来计算累计和
SELECT 
	sale_amount,
	SUM(`sale_amount`) OVER w1 AS sum_amount1,
	SUM(`sale_amount`) OVER w2 AS sum_amount2,
	SUM(`sale_amount`) OVER w3 AS sum_amount3
FROM `salse`
WINDOW 
	w1 AS (ORDER BY `sale_amount`),
	w2 AS (w1 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),
	w3 AS (w1 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW);

框架FRAME_CLAUSE

框架是窗口的一个子集,通过框架子句可以显式定义框架的范围,指定每个分区内应用窗口函数的一小部分数据的区域。可根据当前行动态的变化,或者选定一个静态的范围。
frame_clause由两个子句组成,语法定义如下:

frame_units frame_extent

解析:
frame_units(框架单位)指定框架使用的单位,有两种:

  • ROWS:基于行数,通过起始行和结束行来划定框架的范围,边界是明确的一行。

  • RANGE:基于值的大小,通过具有相同值的行来划定框架的范围,边界是一个范围,具有相同值的行作为一个整体看待。

frame_extend(框架范围)指定窗口范围的起始位置和结束位置,可通过BETWEEN frame_start AND frame_end子句定义,frame_start和frame_end有以下几种形式:

  • CURRENT ROW:当前行
  • UNBOUNDED PRECEDING:分区中第一行
  • UNBOUNDED FOLLOWING:分区中最后一行
  • N PRECEDING:当框架单位是ROWS时, 边界是当前行的前n行。当框架单位是RANGE时,边界是值和"当前行的值减去N"相等的行,可能有多个,统一视为一个边界。
  • M FOLLOWING:当框架单位是ROWS时, 边界是当前行的后m行。当框架单位是RANGE时,边界是值和"当前行的值加上M"相等的行,可能有多个,统一视为一个边界。

以下为框架范围图示:
图示

注意:

  • 含有ORDER BY子句,框架从组内第一行到当前行
  • 不含ORDER BY子句,框架从组内第1行到最后一行(组内所有行)
  • 如果没有指定框架,则默认为RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,即从分区开始到与当前行值相同的行。
  • 如果没有定义框架的终点,则默认终点为CURRENT ROW当前行

示例:

SELECT 
	`sale_id`, 
	`sale_amount`,
	SUM(`sale_amount`) OVER (PARTITION BY `sale_id` ORDER BY `sale_amount` ROWS 1 PRECEDING) AS s1,
	SUM(`sale_amount`) OVER (PARTITION BY `sale_id` ORDER BY `sale_amount` RANGE 1 PRECEDING) AS s2,
	SUM(`sale_amount`) OVER (PARTITION BY `sale_id` ORDER BY `sale_amount` RANGE 100 PRECEDING) AS s3
FROM `sales`;

结果如下:

在这里插入图片描述

以上示例中:

  • s1框架单位为ROWS,框架范围为1 PRECEDING,起点边界为当前行的前一行,终点默认为当前行。
  • s2框架范围为RANGE,框架范围为1 PRECEDING,起点边界为“当前行的值减去1”,终点默认为当前行。但是值差距都比1大,起点都无法触及之前的行,所以每行的框架都只包含当前行。
  • s3框架范围为RANGE,框架范围为100 PRECEDING,起点边界为“当前行的值减去100”,终点默认为当前行,即取每个分区中值的范围在区间[当前行值-100,当前行值]内的所有行
    • sale_id为1的分组中,第2行的sale_amount为1000,因此框架包含值在[1000-100, 1000]范围内的所有行,即第1,2行,sum求和结果为2000。其他分组同理。
    • sale_id为1的分组中,第3行的sale_amount为1200,因此框架包含值在[1200-100, 1200]范围内的所有行,只有第3行,则sum求和结果为其本身的值。

三、窗口函数

聚合函数在窗口中使用与GROUP BY中相同,不做详细解析。以下介绍的专用窗口函数必须搭配OVER关键字使用

ROW_NUMBER

无参数,返回当前行在分组内的序号,从1开始,每一行都有唯一的序号。

注意: 如无ORDER BY子句,行的编号不确定

RANK

无参数,返回当前行在分组内的序号,排序带间隙(序号不连续),值相同,序号相同。
可使用ORDER BY子句指定按某列进行排序。

注意: 如无ORDER BY子句,则所有行的序号都为1

DENSE_RANK

无参数,返回当前行在分组内的排序,排序不带间隙(序号连续),值相同,序号相同。

示例:

SELECT 
	`sale_id`, 
	`sale_amount`,
	ROW_NUMBER() OVER (PARTITION BY `sale_id` ORDER BY `sale_amount`) AS row_number排序,
	RANK() OVER (PARTITION BY `sale_id` ORDER BY `sale_amount`) AS rank排序,
	DENSE_RANK() OVER (PARTITION BY `sale_id` ORDER BY `sale_amount`) AS dense_rank排序
FROM `sales`;

结果如下:

在这里插入图片描述

PERCENT_RANK

百分比排序,返回当前行在分组内的百分比位置,返回值范围为[0, 1],第1行的百分比位置是0%。
计算公式:(rank-1)/(rows-1),rank为RANK函数返回的序号,rows为组内总行数

示例:

SELECT 
	`sale_id`,
	`sale_amount`,
	RANK() OVER (PARTITION BY `sale_id` ORDER BY `sale_amount`) rank排序
	PERCENT_RANK() OVER (PARTITION BY `sale_id` ORDER BY `sale_amount`) percent_rank排序
FROM `sales`;

结果如下:

在这里插入图片描述

以上示例中: sale_id为1的分组中,第3行RANK排序为3,组内总行数为3,所以PRECENT_RANK为(3-1)/(3-1)=1。其他同理。

CUME_DIST

累积分布(cumulative distribution),返回分组中值的累积分布,数据分布从0到1。
计算公式:(值小于等于当前行的行数)/分组内总行数。

示例:

SELECT
	`sale_id`,
	`sale_amount`,
	CUME_DIST() OVER (PARTITION BY `sale_id` ORDER BY `sale_amount`) 累积分布
FROM `sales`;

结果如下:

在这里插入图片描述

以上示例中:

  • sale_id为1的分组中,小于等于第1行值的行为第1、2行,所以2/3=0.666…。第2行同理。
  • sale_id为2的分组中,小于等于第4行值的行只有第4行,所以1/2=0.5。而小于等于第5行的值的行有第4、5行,所以2/2=1。其他分组同理。

FIRST_VALUE与LAST_VALUE

FIRST_VALUE(offset):返回当前框架对应参数第一行的值,参数expr为要检索的列或表达式,根据排序定义不同而变化。
LAST_VALUE(offset):返回当前框架对应参数最后一行的值,参数expr为要检索的列或表达式,根据排序定义不同而变化。

NTH_VALUE

NTH_VALUE(offset, N):返回当前框架第N行对应参数的值,没有时返回NULL。参数expr为要检索的列或表达式,参数offset为第几行。
用得较少,可以根据以上示例自己尝试练习。

LAG与LEAD

LAG(expr [, offset [, default]]):返回当前框架内当前行之前的某一行的值。参数expr为要检索的列或表达式,参数offset为要返回的行数,如果不指定,默认为1,即上一行。参数default为当指定的行数超出结果集范围时,返回的默认值。
LEAD(expr [, offset [, default]]):返回当前框架内当前行之后的某一行的值。参数expr为要检索的列或表达式,参数offset为要返回的行数,如果不指定,默认为1,即下一行。参数default为当指定的行数超出结果集范围时,返回的默认值。

示例1:

SELECT
	`sale_id`,
	`sale_amount`,
	LAG(`sale_amount`*2) OVER w 当前行前1行乘2,
	LAG(`sale_amount`,1,'不存在') OVER w 当前行前1行带默认值,
	`sale_amount`-LAG(`sale_amount`,2) OVER w 当前值与前2行的差
FROM `sales`
	WINDOW w AS (PARTITION BY `sale_id` ORDER BY `sale_amount`);

结果如下:

在这里插入图片描述

示例2:

SELECT
	`sale_id`,
	`sale_amount`,
	LEAD(`sale_amount`*2) OVER w 当前行后1行乘2,
	LEAD(`sale_amount`,1,'不存在') OVER w 当前行后1行带默认值,
	`sale_amount`-LEAD(`sale_amount`,1) OVER w 当前值与后1行的差
FROM `sales`
	WINDOW w AS (PARTITION BY `sale_id` ORDER BY `sale_amount`);

结果如下:

在这里插入图片描述

NTILE

NTILE(N):分区内有序数据根据不同等级分为N个桶,记录桶编号。参数为子分区数量。
用得较少,可以根据以上示例自己尝试练习。

四、窗口函数使用事项

  • 窗口函数可以在不改变原表行数的情况下,对每个分区内的查询行进行聚合、排序、排名等操作,提高了数据分析的灵活性和效率。
  • 窗口函数可以使用滑动窗口来处理动态的数据范围。
  • 窗口函数可以与普通聚合函数、子查询等结合使用,实现更复杂的查询逻辑。
  • 窗口函数的语法较为复杂,需要注意OVER子句中的各个参数的含义和作用。
  • 窗口函数的执行效率可能不如普通聚合函数,因为它需要对每个分区内的每个查询行进行计算,而不是折叠为单个输出行。但一般来说,窗口函数的性能优于使用子查询或连接的方法,因为窗口函数只需要扫描一次数据,而子查询或连接可能需要多次扫描或连接。
  • 窗口函数只能在SELECT列表和ORDER BY子句中使用,不能用于WHERE、GROUP BY、HAVING等子句中。

五、窗口函数优化方法

  • 选择合适的窗口函数,避免使用复杂或重复的窗口函数。
  • 使用Named Windows来定义和引用窗口,避免在多个OVER子句中重复定义相同的窗口。
  • 尽量减少分区和排序的代价,使用索引或物化视图来加速分区和排序。
  • 尽量减少窗口的大小,使用合适的frame_clause来限制窗口内的数据范围。
  • 尽量使用并行处理来加速窗口函数的计算,利用多核或分布式系统来提高效率。

六、面试常问(持续更新)

1.序号函数:row_number(),rank(),dense_rank()的区别

  • ROW_NUMBER,顺序排序——1、2、3
  • RANK,并列排序,跳过重复序号——1、1、3
  • DENSE_RANK,并列排序,不跳过重复序号——1、1、2

2.求连续登录3天的用户

  • 解题思路:
    ①同一用户不可能在同一时间登录,不可能出现相同的登录时间,所以可以使用窗口函数根据用户进行分组,再根据登录时间进行排序,窗口函数可使用任一排序函数。
    ②如果连续登录,那么登录时间减去序号所获得的日期相同。
    ③统计获得的相同日期的个数,等于3的即为所求

  • 解题过程(使用以上数据集):
    根据sale_id分区,根据sale_date排序,DATA_SUB函数在sale_date日期上减去序号的天数获得data_flag。
    感谢评论区指出源数据已去重的问题,如果对源数据进行去重可在窗口函数对应语句中使用嵌套查询:(SELECT DISTINCT sale_date, sale_id FROM sales) tmp_sales

SELECT 
	`sale_id`,
	`sale_date`,
	`rn`,
	DATE_SUB(`sale_date`, INTERVAL `rn` DAY) AS `date_flag`
FROM (
	SELECT 
		*,
		ROW_NUMBER() OVER (PARTITION BY `sale_id` ORDER BY `sale_date` ASC) AS `rn`
	FROM (SELECT DISTINCT `sale_date`, `sale_id` FROM `sales`) tmp_sales
) tmp

在这里插入图片描述

  • 最终答案(使用以上数据集):
    根据sale_id分区排序后,分组统计登录时间减去序号获得的相同日期的个数,等于3即为连续登录3天的用户。
SELECT 
	`sale_id`,
	COUNT(*) AS`days_count`
FROM (
	SELECT 
		*,
		ROW_NUMBER() OVER (PARTITION BY `sale_id` ORDER BY `sale_date` ASC) AS `rn`
	FROM (SELECT DISTINCT `sale_date`, `sale_id` FROM `sales`) tmp_sales
) tmp
GROUP BY `sale_id`, DATE_SUB(`sale_date`, INTERVAL `rn` DAY)
HAVING days_count=3

在这里插入图片描述


如有问题和建议,可私信或评论,非常感谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bumerang�

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值