1-2 必知必会的窗口函数
顺序编程的复活
本节将介绍窗口函数。窗口函数出现于 20 世纪 90 年代后半期,在 21 世纪初得到了 Oracle、DB2 和SQLServer 等 DBMS 的支持。随着 2017 年 MySQL也开始支持窗口函数,现在的主流 DBMS 中就都可以使用窗口函数了。如果熟练掌握了窗口函数,从某种意义上来说,我们就可以像使用面向过程语言那样操作数据。窗口函数能够大幅扩展SQL编程的可能性,是一个非常重要的工具。通过本节,我们能够加深自己对窗口函数的理解。
虽然窗口函数被SQL引入的时间比较晚,但它非常重要。可以说没有窗口函数,就没有现代SQL编程。在 2008 年编写本书的第 1 版时,因为有些 DBMS 还不支持窗口函数,所以笔者当时并未对其进行详细介绍,但在本书中,窗口函数成了重点内容。
窗口函数的应用方法有很多,特别是在进行行间比较时必须依赖相关子查询的情况下,通过使用窗口函数,我们可以去掉相关的子查询,让SQL语句变得更加优雅(相关内容将在 1-7 节中详细介绍)。为了帮助大家熟练掌握便捷的窗口函数,本节将重点介绍窗口函数的基本操作。把介绍的重点放在基本操作上还有一个原因,那就是窗口函数乍一看很难理解,常常让SQL用户感到困惑。传统的SQL编码基于面向集合的思维方式,对熟悉这种编码的数据库工程师来说,窗口函数很难理解,因为它使用了“行的顺序”这一面向过程语言的概念,而这个概念很早之前就脱离关系数据库和SQL了。对初次接触SQL的新手来说,若一个函数中包含诸多功能,便很难想象它是怎么工作的。
不过,如果因为这样的理由就对窗口函数敬而远之,那就太可惜了。本节,笔者会通过一些示例来讲解窗口函数。这里以读者大致掌握窗口函数的基本语法为前提,如果读者完全没有见过或使用过窗口函数,那么请先阅读书末参考文献中列举的面向新手的有关窗口函数的图书,这样理解起来会更容易些。另外,不记得详细的可选项语法并不会影响理解本节的内容,请大家放心阅读。
什么是窗口
首先,初次见到窗口函数的人会觉得这个名字有些不可思议 9。大家可能会想象这个函数是使用了某种类似于“窗口”的东西,但看到语法示例时,却发现并没有哪句代码表明了“这是窗口”,只看到那些使用了 PARTITION BY
子句或 ORDER BY
子句的查询示例。
9 窗口函数出现于 20 世纪 90 年代到 21 世纪初,当时它也被称为“OLAP 函数”。人们打算把它用于 OLAP(联机分析处理,online analytical processing),所以才取了这个名称,但现在已经不怎么使用该名称了。笔者认为现在这个“窗口函数”的名称要好一些,因为它能清楚地描述动作特征。
我们来看一下窗口函数的典型用例——计算移动平均值的语法的示例。这里不需要具体的数据,所以省略了表的定义。
■ 匿名窗口
SELECT products_id, products_name, sale_price,
AVG (sale_price) OVER (ORDER BY products_id
ROWS BETWEEN 2 PRECEDING
AND CURRENT ROW) AS moving_avg
FROM Products;
上面的代码按商品 ID 的升序来排列商品表,计算包含当前 ID 之前的两个商品的价格移动平均值。虽然出现了 AVG
、OVER
、ROWS BETWEEN
、CURRENT ROW
等窗口函数的关键字,但我们看不到窗口本身的定义。
然而,这并不是说该查询并未使用窗口。实际上,该语法中也定义了窗口,只是操作是悄悄进行的,乍一看会让人以为没有窗口。
显式定义窗口的语法如下所示。
■ 有名称的窗口
SELECT products_id, products_name, sale_price,
AVG(sale_price) OVER W AS moving_avg
FROM Products
WINDOW W AS (ORDER BY products_id
ROWS BETWEEN 2 PRECEDING
AND CURRENT ROW);
这里显式定义了窗口,并对其应用了 AVG
函数。这里所说的窗口,就是针对通过 FROM
子句选择的记录集,使用 ORDER BY
排序和使用 ROWS BETWEEN
定义帧之后所形成的数据集。窗口会通过各种可选项对记录集进行数据加工,这就是它和记录集的不同之处 10。
10 窗口(window)在英语中原本就有“范围”“宽度”的意思,在系统开发领域也存在“批处理窗口”“维护窗口”这样的术语。在这种情况下,“窗口”与普通意义上的窗口并无直接关系。
用这个词表示时间段含义时也是如此。一般来说,该术语不仅用作将集合分割开的子集,还用作默认内含某种顺序性的“范围”。笔者知道的其他示例还有世界一级方程式锦标赛(F1 比赛)中使用的术语“Pit Stop 窗口”,其含义是赛车进行 Pit Stop(进入维修站换台及加油)的合适的时间间隔。这也是在确定某个时间段时使用的词语。
通过比较这两种语法可以知道,我们常用的窗口函数的语法,是默认使用“匿名窗口”的简略版语法(这与匿名存储过程或匿名函数是一样的)。其优点是内容简练,而带名称的窗口的优点是窗口可以重复使用,能避免编辑错误。这与通过公用表表达式(CTE)重复使用视图,以及在存储过程中定义有名称的存储过程的效果是一样的。
-- 有名称的窗口可以被重复使用
SELECT products_id, products_name, sale_price,
AVG(sale_price) OVER W AS moving_avg,
SUM(sale_price) OVER W AS moving_sum,
COUNT(sale_price) OVER W AS moving_count,
MAX(sale_price) OVER W AS moving_max
FROM Products
WINDOW W AS (ORDER BY products_id
ROWS BETWEEN 2 PRECEDING
AND CURRENT ROW);
匿名窗口和有名称的窗口各有优势,要根据具体情况进行选择,但有一点必须注意,即有的 DBMS 不支持有名称的窗口,一旦使用就会发生错误 11。人们通常认为有名称的窗口是“正式”的语法,但实际情况恰好相反,被普遍使用的是匿名窗口。
11 例如,有名称的窗口函数可以用在 PostgreSQL和 MySQL中,但在 Oracle 中使用就会发生错误。
这种语法上的不兼容会给 DBMS 之间的迁移带来风险,因此原则上要使用匿名窗口(在理解了有名称的窗口定义之后),这种做法或许更稳妥一些。这和“爬完梯子要将其扔掉”12 是一个道理。
12 出自哲学家路德维希 · 维特根斯坦。——编者注
一张图看懂窗口函数
前面介绍了窗口函数的定义,下面我们来看一下窗口函数的功能(图 1.2.1)
图 1.2.1 一张图看懂窗口函数13
13 这张图参考自论文“Efficient Processing of Window Functions in AnalyticalSQLQueries”(分析型SQL查询中窗口函数的高效处理)。
窗口函数让人难以理解的原因之一是 1 个窗口函数中包含多个操作,而如果像图 1.2.1 那样从整体来看,窗口函数实际上只包含下面 3 个功能(或许仍有人认为包含 3 个功能已经很复杂了,这里我们暂且抛开这个问题)。
01.使用 PARTITION BY
子句分割记录集合。
02.使用 ORDER BY
子句对记录排序。
03.使用帧子句定义以当前记录为中心的子集。
其中,第 1 个功能和第 2 个功能因为与现有的 GROUP BY
和 ORDER BY
的功能几乎一样,所以对于已经掌握SQL基本语法的人来说都很容易理解 14。窗口函数真正特有的功能是上面列出的第 3 个功能。传统的SQL编程中并没有显式地使用“当前记录”的概念。另外,使用关系数据库构建过系统的人应该能立刻注意到,这个“当前记录”源自“游标”(cursor)的引入——关系数据库一直使用游标向面向过程语言传递数据(图 1.2.2)。
14 PARTITION BY
子句只用来分割窗口,并不会像 GROUP BY
子句那样对记录进行聚合,因此在应用窗口函数时,记录的行数不会发生改变,这一点与 GROUP BY
子句的功能并不完全一样。大家记住“PARTITION BY = GROUP BY
- 聚合”,就能更容易理解这个功能了。关于二者的不同,请参考本书 2-6 节的内容。
图 1.2.2 帧子句的原理是“游标”
之所以需要游标,是因为关系数据库的表中的记录是无序的,操作的基本单位是记录的集合,也就是一次一集合(set at a time)的操作方式,而面向过程语言的记录是有序的,操作的基本单位是一行记录,也就是是一次一记录(record at a time)的操作方式,我们需要用游标来填补二者之间的差异。
在面向过程语言中,根据键对记录集合进行排序,通过 for
语句或 while
语句循环记录集合,一行一行地移动当前记录进行处理,这种操作方法至今都没有变过。即使在引入地址隐藏和面向对象后,也没有发生改变。在这一点上,窗口函数可以说是将面向过程语言的思想引入到了SQL中 15。
15 可能有人会认为“在传统的SQL中,ORDER BY
子句也会定义记录的顺序”,但实际上 ORDER BY
子句并不是SQL,而是游标定义的一部分。关于关系数据库(的设计者)去掉编程中非常重要的记录顺序这一概念的原因,请参考本书的 2-1 节;关于记录顺序的概念去掉又恢复的原委,请参考本书的 2-5 节。
使用帧子句将其他行移至当前行
帧子句的作用是能通过SQL简单计算出移动平均值等以当前记录为基准计算的统计指标。除此之外,帧子句还有很广泛的用途。直观来讲,帧子句可以将其他行移至当前行。之前使用SQL进行行间比较很困难,现在则变得很自如。
求过去最临近的值
我们先来思考一下基本的时间序列分析。当比较时间序列中的数据时,SQL基本上是沿着时间序列,一行一行地向前追溯或向后推进。作为示例,我们来看一张记录了服务器各个时点的负载量的表 LoadSample(这里选用了较为合适的数值作为负载量,大家不用关注其具体含义)。由于采样是不定期的,所以存储的日期并不连续,间隔随机。
首先计算各行的“过去最临近的日期”,也就是计算“上一行”的日期。
SELECT sample_date AS cur_date,
MIN(sample_date)
OVER (ORDER BY sample_date ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_date
FROM LoadSample;
■ 执行结果
cur_date latest_date
------------ ------------
2018-02-01
2018-02-02 2018-02-01
2018-02-05 2018-02-02
2018-02-07 2018-02-05
2018-02-08 2018-02-07
2018-02-12 2018-02-08
由于该表中并没有 2 月 1 日之前的数据,所以 2 月 1 日这行之前的日期是 NULL
。这一点应该不难理解。从 2 月 2 日起,每行日期的过去最临近的日期都存在于表中,它们保存在 latest
列中。该查询的重点是通过 ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING
将帧子句的范围限定在按 sample_date
排序后的上一行。一般来说,BETWEEN
大多用来指定多行的范围,而这里用来将范围限定为一行,自然就不会发生错误。
可以说,这里的帧子句以游标位于“当前行”为前提,创建了范围是“上一行”的记录集合。
除日期之外,计算与日期相对应的负载量也很简单。当前记录的负载量可以直接用 load
列计算出来。在相同的窗口定义下仅将列修改为 load
列,就可以计算出上一行的负载量。
SELECT sample_date AS cur_date,
load_val AS cur_load,
MIN(sample_date)
OVER (ORDER BY sample_date ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_date,
MIN(load_val)
OVER (ORDER BY sample_date ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_load
FROM LoadSample;
■ 执行结果
cur_date cur_load latest_date latest_load
------------ --------- ----------- -----------
2018-02-01 1024
2018-02-02 2366 2018-02-01 1024
2018-02-05 2366 2018-02-02 2366
2018-02-07 985 2018-02-05 2366
2018-02-08 780 2018-02-07 985
2018-02-12 1000 2018-02-08 780
大家注意到了吗?这段代码中出现了两次相同的窗口定义。如果使用有名称的窗口语法,就可以像下面这样将窗口函数汇总为一个(结果是一样的)。
SELECT sample_date AS cur_date,
load_val AS cur_load,
MIN(sample_date) OVER W AS latest_date,
MIN(load_val) OVER W AS latest_load
FROM LoadSample
WINDOW W AS (ORDER BY sample_date ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING);
这是使用帧子句进行行间移动的基本内容。这里,笔者来回答一些常见的疑问。
Q 1 除向前移动之外,帧还可以向“后”移动吗
可以。这时要使用 FOLLOWING
关键字。例如,在过去最临近的值的查询中,将帧的范围向后移动一行。
SELECT sample_date AS cur_date,
load_val AS cur_load,
MIN(sample_date) OVER W AS next_date,
MIN(load_val) OVER W AS next_load
FROM LoadSample
WINDOW W AS (ORDER BY sample_date ASC
ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING);
■ 执行结果
cur_date cur_load next_date next_load
------------ ---------- ----------- -----------
2018-02-01 1024 2018-02-02 2366
2018-02-02 2366 2018-02-05 2366
2018-02-05 2366 2018-02-07 985
2018-02-07 985 2018-02-08 780
2018-02-08 780 2018-02-12 1000
2018-02-12 1000
这次在 next_date
和 next_load
列中显示将来最临近的日期的记录值。
另外,同时使用 PRECEDING
和 FOLLOWING
,将当前记录夹在中间,还可以设置范围为“前后各 n 行”的帧。
Q 2 这里使用了 MIN 函数,请问它有什么含义呢
如果是像示例这样将帧的范围限定为一行,那么 MIN
并没有什么特别的含义。即使使用的是 MAX
、AVG
或 SUM
,结果也是一样的,因为这相当于对一行应用聚合函数。如果帧的范围是多行,就需要应用相应的聚合函数了。
-- 执行结果与使用 MIN 函数时相同
SELECT sample_date AS cur_date,
load_val AS cur_load,
MAX(sample_date) OVER W AS latest_date,
MAX(load_val) OVER W AS latest_load
FROM LoadSample
WINDOW W AS (ORDER BY sample_date ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING);
Q 3 可以设置“一天前”或“两天前”这样基于列值(而不是行)的帧吗
可以。这时要使用 RANGE
关键字来代替 ROWS
16。
16 sample_date
是日期类型,所以 interval '1' day
是根据日期类型指定日期间隔的语法。如果使用 RANGE
,就需要注意列的数据类型了(笔者认为它只能用于数值、日期和时间)。
SELECT sample_date AS cur_date,
load_val AS cur_load,
MIN(sample_date)
OVER (ORDER BY sample_date ASC
RANGE BETWEEN interval '1' day PRECEDING
AND interval '1' day PRECEDING
) AS day1_before,
MIN(load_val)
OVER (ORDER BY sample_date ASC
RANGE BETWEEN interval '1' day PRECEDING
AND interval '1' day PRECEDING
) AS load_day1_before
FROM LoadSample;
■ 执行结果
cur_date cur_load day1_before load_day1_before
-------- --------- ----------- ----------------
18-02-01 1024
18-02-02 2366 18-02-01 1024
18-02-05 2366
18-02-07 985
18-02-08 780 18-02-07 985
18-02-12 1000
表 LoadSample 中的数据不是连续的,如果没有一天前的数据,day1_before
列和 load_day1_before
列中就会显示 NULL
。这样看起来比较直观。
下面是帧子句中可以使用的选项,大家可以参考。
● ROWS:按行设置移动单位
● RANGE:按列值设置移动单位。使用 ORDER BY 子句来指定基准列
● n PRECEDING:仅向前(行号较小的方向)移动 n 行。n 为正整数
● n FOLLOWING:仅向后(行号较大的方向)移动 n 行。n 为正整数
● UNBOUNDED PRECEDING:一直移动到最前面
● UNBOUNDED FOLLOWING:一直移动到最后面
● CURRENT ROW:当前行
行间比较的一般化
现在这样已经可以求出过去最临近的日期了,但在实际工作中,人们还可能希望将比较范围再扩大一些,比如将某个日期与其“过去最临近的日期”或“过去第二临近的日期”进行比较,甚至与“前面 n 行的日期”进行比较。这就是行间比较的一般化。
为了满足该需求,我们首先要思考如何以某个日期为起点开始依次追溯之前的日期。假设我们先追溯前面三个临近的日期,那么结果会像下页这样呈阶梯形。之所以会呈现出这种形状,是因为当追溯的日期数据不存在时,数据为 NULL
。
■ 设想的执行结果
cur_date latest_1 latest_2 latest_3
-------- -------- -------- --------
2018-02-01
2018-02-02 2018-02-01
2018-02-05 2018-02-02 2018-02-01
2018-02-07 2018-02-05 2018-02-02 2018-02-01
2018-02-08 2018-02-07 2018-02-05 2018-02-02
2018-02-12 2018-02-08 2018-02-07 2018-02-05
求解的窗口函数如下所示。
SELECT sample_date AS cur_date,
MIN(sample_date)
OVER (ORDER BY sample_date ASC
ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS latest_1,
MIN(sample_date)
OVER (ORDER BY sample_date ASC
ROWS BETWEEN 2 PRECEDING AND 2 PRECEDING) AS latest_2,
MIN(sample_date)
OVER (ORDER BY sample_date ASC
ROWS BETWEEN 3 PRECEDING AND 3 PRECEDING) AS latest_3
FROM LoadSample;
这里只是将 BETWEEN
的指定行修改为“前一行”“前两行”“前三行”……实现起来非常简单。不管是前几行,我们都可以使用相同的方法进行扩展。在这种情况下,可能有人认为可以使用有名称的窗口来汇总定义,但是很遗憾,由于帧的定义不一样,这一点无法实现。
使用窗口函数进行行间比较的应用方式有很多种,我们将在 1-7 节中详细了解。
窗口函数的内部动作
前面介绍过,窗口函数拥有下面 3 个功能。
01.使用 PARTITION BY
子句分割记录集合。
02.使用 ORDER BY
子句对记录排序。
03.使用帧子句定义以当前记录为中心的子集。
看起来这是将复杂的功能集中在一个函数中,但实际上这些功能在SQL内部是怎样实现的呢?本节要讲解的就是这个问题。
查看SQL语句内部动作的手段通常是查看“执行计划”(execution plan)。所谓执行计划,其实就是一份由数据库提供的计划书,以帮助我们确定 DBMS 在执行SQL语句时,以什么样的访问路径获取数据、执行什么样的计算是最高效的。可以说,它就像是一份用来判断登山路线的参考书。
虽然执行计划的格式会随 DBMS 的不同而发生改变,但只要是经过一定培训的人,应该就能看懂。因此,当SQL语句执行较慢时,我们就要输出并解析执行计划,查明原因并进行优化(SQL语句越复杂,执行计划就越复杂,解析也就越辛苦)。
本书的目的不是教大家读懂执行计划,所以书中并未讲解执行计划的细节内容,而窗口函数的执行计划很简单,即使是第一次看到的人也能明白其含义。请试着看一下本节开头介绍的查询移动平均值的执行计划。
SELECT products_id, products_name, sale_price,
AVG (sale_price) OVER (ORDER BY products_id
ROWS BETWEEN 2 PRECEDING
AND CURRENT ROW) AS moving_avg
FROM Products;
■ 执行结果 PostgreSQL
QUERY PLAN
----------------------------------------------------------------------
WindowAgg (cost=20.76..24.61 rows=220 width=274)
-> Sort (cost=20.76..21.31 rows=220 width=242)
Sort Key: products_id
-> Seq Scan on products (cost=0.00..12.20 rows=220 width=242)
■ 执行结果 MySQL
该示例展示了 PostgreSQL和 MySQL的执行计划。MySQL的执行计划是横向布局,一页放不下,所以这里只摘录了重点内容。
这两个执行计划的含义都是扫描(读取)表 Products 的数据,并对读取的数据进行排序。PostgreSQL的执行计划中出现了 SORT
关键字,MySQL的执行计划中出现了 Using filesort
关键字,它们都表示排序。
窗口函数的本质是排序
由刚才讲解的内容可知,窗口函数在内部对记录集合进行了排序。在笔者编写本书时(2018 年),所有的 DBMS 都是如此。之所以要在窗口函数中进行排序,是出于使用 PARTITION BY
子句进行分组和使用 ORDER BY
子句对记录排序时的需要。在关系数据库中,表的记录并不一定是物理排序的,因此一般来说,如果要基于键值对记录排序,就需要先对记录集合进行排序 17。
17 有时,如果数据已经排完序,通过扫描索引能够很好地使用它们,那么也可以省略掉窗口函数的排序,实现快速查询。
所谓“进行排序”,就是执行使用 for
语句或 while
语句的循环。虽然我们无法根据执行计划确定使用的是什么排序算法,但不管是快速排序,还是归并排序,在面向过程语言中通常都是通过循环来实现的。实际上,如果大家不使用SQL而使用面向过程语言,对 CSV 或文本文件等适当形式的数据执行与窗口函数同样的计算,使用循环进行排序也可以解决问题。
散列和排序
不过,排序作为窗口函数的实现方法在性能方面是否最优,也存在不同的意见。在图 1.2.1 的边注所提到的论文中,实际的测试结果显示从原理上来说,某些情况下使用散列来计算 PARTITION BY
子句的性能会更好。
对于输入行数 n,如果分割数是 O(n),那么散列就是 O(n),最优排序也得是 O(n log n)。
——出自论文 4.2 节“Determining the Window Frame Bounds”
(笔者摘译)
散列函数拥有这样的特性:若输入值不同,输出值基本上也不会一样(值不会重复)。该输出值称为散列值。图 1.2.3 展示了“30
”→“cdae7jh02
”的转换。成对的输入值和散列值称为散列表。使用散列表进行分组,就可以在不进行排序的情况下执行聚合操作(虽然输入值不转换为散列值也可以进行分组,但散列值的优点是无须在意列数或数据类型,即可使用各种需要输入散列值的函数)。
图 1.2.3 散列分组的示意图
实际上,GROUP BY
子句的功能与 PARTITION BY
子句的功能几乎是一样的。在 Oracle 或 PostgreSQL中,GROUP BY
子句除排序之外,还可以使用散列进行计算。不过,前述论文中也指出,散列要想发挥优势是有几个前提的,并不是说它在任何情况下都有优势。或许,窗口函数早晚有一天会像 GROUP BY
子句那样能使用散列进行计算。
本节小结
我们来回顾一下本节要点。
01.窗口函数中的“窗口”(原则上是有序的)是“范围”的意思。
02.窗口函数在语法上是通过 PARTITION BY
子句和 ORDER BY
子句被赋予某种特征的记录集合。由于较为常用的是窗口函数的简略形式,所以我们很难注意到窗口。
03.PARTITION BY
子句去掉了 GROUP BY
子句的聚合功能,只保留了分割功能,而 ORDER BY
子句用于对记录排序。
04.帧子句通过将游标功能引入SQL中来定义以“当前记录”为中心的记录集合的范围。
05.通过帧子句,我们可以将不同行的数据移至同一行,轻松地进行行间比较。
06.目前,窗口函数的内部动作是对记录进行排序,将来或许会采用散列来处理。
专栏 为何是 OVER,而不是 ON
在窗口函数中,定义了使用 PARTITION BY
子句进行分割、使用 ORDER BY
子句进行排序这些操作的SQL语句,使用的关键字是 OVER
。如果没有这些SQL语句,AVG
或 SUM
就不算是窗口函数,只能作为聚合函数来执行操作。因此,OVER
可以说是窗口函数的标记。
众所周知,OVER 在英语中是表示“在(某个对象的)上面”的介词。这里的“对象”当然就是记录集合。不过,ON 也有“在上面”的意思,那么窗口函数为什么不使用 ON
呢?
由于SQL中已经将 ON
用作指定连接条件的关键字,所以不使用 ON
的直接原因或许是为了避免混淆。不过笔者认为,窗口函数中之所以使用 OVER
,是因为 ON
与 OVER
的语感稍有不同,OVER
有更积极的含义。下面就来聊一聊笔者的推测。
大家在英语课上学过,ON 与 OVER 的语感存在细微的差别。ON 给人的印象是在某个对象上处于静止(贴合)状态,而 OVER 包含在上面穿过的动作或移动的含义(图 1.2.4)。
图 1.2.4 ON 与 OVER 的区别
不管是对于人还是物,OVER 这个词都拥有“从一端移动到另一端”这样的语感,请看下面的例句。
The airplane is flying over the sea. (飞机在海上飞行)
The ball flew over the pond. 〔(高尔夫)球越过池塘〕
当表示这种移动时,如果使用 ON,会让人感觉有点奇怪吧?
正如本节介绍的那样,窗口函数按顺序扫描多个记录,并进行计算。为此,窗口函数内部会进行排序。笔者认为,单词 OVER 与该动作的形象一致,所以窗口函数中才使用了 OVER
一词。虽然没有确凿的证据证明是这样的,但应该不外乎如此吧。
练习题
● 练习题 1-2-1:窗口函数的结果预测 (1)
本节中使用了记录了服务器负载量的表 LoadSample,假设我们要像下面这样将其扩展为记录了多台服务器数据的表。
请大家猜测一下对该表执行下述 SELECT
语句的结果。
SELECT server, sample_date,
SUM(load_val) OVER () AS sum_load
FROM ServerLoadSample;
这里的窗口函数非常简单,其中并未定义 PARTITION BY
子句、ORDER BY
子句和帧子句。
这些子句都是可选的,因此该SQL语句并不会发生语法错误,会正确地返回结果,那么它会返回什么样的结果呢?也请大家猜测一下,并思考其原因。
● 练习题 1-2-2:窗口函数的结果预测 (2)
对上一题中的表 ServerLoadSample 执行下述 SELECT
语句,并猜测一下结果。
SELECT server, sample_date,
SUM(load_val) OVER (PARTITION BY server) AS sum_load
FROM ServerLoadSample;
这次添加了 PARTITION BY
子句,结果会发生什么变化呢?也请大家猜测一下,并思考其原因。
这两道题可能让人觉得自己是在做有关窗口函数详细规范的竞赛题。其实,这些规范在一些情境下使用起来非常便利。我们将在 1-7 节中了解详细内容。