SQL进阶——窗口函数进行行间比较

窗口函数进行行间比较

使用SQL对同一行数据进行列间比较很简单,只需要在WHERE子句里写上类似于col_1 = col_2的比较条件就可以了。但是,对不同行的数据进行列间比较就没那么简单了。这并不是说我们不能用SQL进行行与行之间的比较。使用SQL进行这种行间比较时,发挥主要作用的技术是窗口函数,它在SQL:2003中实现了标准化。

在过去,如果要用SQL进行行间比较,通常使用的方法是关联子查询。但是,关联子查询的代码很复杂,内部处理过程不易理解,性能方面也很容易出问题,这些都让数据库工程师困扰不已。像行间比较这样的处理,与使用SQL相比,人们更常用的方法是将整张表的结果集传给应用程序,然后使用面向过程语言来处理。

不过,在窗口函数出现后,我们就能使用非常简洁的SQL语句来进行行间比较了。

1、增加、减少、没有变化

需要对数据做行间比较的典型业务场景,是使用记录了时间序列数据的表进行时间序列分析。假设有这样一张记录了某家公司每年营业额的表Sales(相应的趋势图见图)​。

sql 复制代码
CREATE TABLE Sales (
	year INTEGER NOT NULL , 
	sale INTEGER NOT NULL ,
	PRIMARY KEY (year)
);

INSERT INTO Sales VALUES (1990, 50);
INSERT INTO Sales VALUES (1991, 51);
INSERT INTO Sales VALUES (1992, 52);
INSERT INTO Sales VALUES (1993, 52);
INSERT INTO Sales VALUES (1994, 50);
INSERT INTO Sales VALUES (1995, 50);
INSERT INTO Sales VALUES (1996, 49);
INSERT INTO Sales VALUES (1997, 55);

请根据这张表里的数据,使用SQL输出"营业额与上一年相比是增加了还是减少了,或是没有变化"的结果。我们先试着求出"没有变化"的年份。从表里可以看到,需要求出的是1993年和1995年。如果用面向过程语言来解决,方法很简单,如下所示。

  1. 按年份递增的顺序排序。
  2. 循环地将当前行的sale列与前一行的sale列进行比较。

使用SQL时的思路跟这个不太一样,因为SQL中没有循环和变量,所以无法实现同样的操作。在SQL中,过去的做法通常是在表Sales的基础上,再加一张存储了上一年数据的表(S2),然后使用关联子查询进行比较​。S2的数据也来自物理表Sales。

sql 复制代码
-- 求与上一年营业额一样的年份(1):使用关联子查询
SELECT year,sale
FROM Sales S1
WHERE sale = (
	SELECT sale
	FROM Sales S2
	WHERE S2.year = S1.year - 1)
ORDER BY year;

+------+------+
| year | sale |
+------+------+
| 1993 |   52 |
| 1995 |   50 |
+------+------+

也就是说,关联子查询通过把要比较的数据偏移一行,来代替面向过程语言中的循环(关联子查询也因此被称为循环查询)​。

不过,在现在的SQL中,我们可以像下面这样使用窗口函数来实现。

sql 复制代码
-- 求与上一年营业额一样的年份(2):使用窗口函数
SELECT year, current_sale
FROM (
	SELECT year,
         sale AS current_sale,
         SUM(sale) OVER (ORDER BY year RANGE BETWEEN 1 PRECEDING AND 1 PRECEDING) AS pre_sale
	FROM Sales
) TMP
WHERE current_sale = pre_sale
ORDER BY year;

-- 直接使用log函数选取上一行
SELECT year,
			 sale AS current_sale,
			 lag(sale) OVER (ORDER BY year ) AS pre_sale
FROM Sales


+------+--------------+
| year | current_sale |
+------+--------------+
| 1993 |           52 |
| 1995 |           50 |
+------+--------------+

执行结果与使用关联子查询时是一样的,是1993年和1995年。这里的关键就是在子查询内部使用的那个窗口函数。我们来单独执行子查询,就能理解窗口函数的处理过程了。

sql 复制代码
SELECT year,
			 sale AS current_sale,
			 SUM(sale) OVER (ORDER BY year RANGE BETWEEN 1 PRECEDING AND 1 PRECEDING) AS pre_sale
FROM Sales

             ->  AS pre_sale
+------+--------------+----------+
| year | current_sale | pre_sale |
+------+--------------+----------+
| 1990 |           50 |   <null> |
| 1991 |           51 |       50 |
| 1992 |           52 |       51 |
| 1993 |           52 |       52 |
| 1994 |           50 |       52 |
| 1995 |           50 |       50 |
| 1996 |           49 |       50 |
| 1997 |           55 |       49 |
+------+--------------+----------+

这里的关键是使用窗口函数生成的pre_sale列。这里显示的恰好是"向前"偏移了一年的sale列。帧子句中RANGE BETWEEN 1 PRECEDING AND 1 PRECEDING条件的含义是"限定为当前行年份的上一年"​。帧子句是以当前行为起点,限制统计对象的记录范围的窗口函数功能。如果想把条件限定为"当前行年份的下一年"​,将PRECEDING改为FOLLOWING就可以了。

在这里,窗口函数的意义是可以在不修改原始表(这里指表Sales)的情况下,在结果中显示新的列(pre_sale列)​。这可以说是一种保持信息完整或非破损的处理。虽然这里的SUM(sale)使用了SUM函数,但那其实只是表象,它并没有像聚合函数SUM那样缩减(聚合)表的记录个数。这样一来,我们就可以在子查询的外部比较使用窗口函数生成的虚拟列(pre_sale列)和原始表中的列。

接下来请将这个例子扩展一下,求出各年份与上一年相比,营业额是增加了还是减少了,或是没有变化。在音乐和电影的每周排行榜中,我们经常会见到这样的图表。

使用关联子查询的代码如下所示。

sql 复制代码
-- 求出是增加了还是减少了,或是没有变化(1):使用关联子查询
SELECT year, current_sale AS sale,
       CASE WHEN current_sale = pre_sale
            THEN '→'
            WHEN current_sale > pre_sale
            THEN '↑'
            WHEN current_sale < pre_sale
            THEN '↓'
       ELSE '-' END AS var
FROM (
	SELECT year,
			 sale AS current_sale,
			 (SELECT sale
				FROM Sales S2
				WHERE S2.year = S1.year - 1) AS pre_sale
	FROM Sales S1) TMP
 ORDER BY year;

+------+------+-----+
| year | sale | var |
+------+------+-----+
| 1990 |   50 | -   |
| 1991 |   51 | ↑   |
| 1992 |   52 | ↑   |
| 1993 |   52 | →   |
| 1994 |   50 | ↓   |
| 1995 |   50 | →   |
| 1996 |   49 | ↓   |
| 1997 |   55 | ↑   |
+------+------+-----+

这条SQL语句将关联子查询的逻辑移到了SELECT子句里,保存为pre_sale列,使用CASE表达式将其与这一年的current_sale列进行比较。因为这里没有1990年之前的数据,所以执行结果里显示的是"---"​。

这里可以将关联子查询的部分替换为窗口函数,具体如下所示。

sql 复制代码
-- 求出是增加了还是减少了,或是没有变化(2):使用窗口函数
SELECT year, current_sale AS sale,
       CASE WHEN current_sale = pre_sale
            THEN '→'
            WHEN current_sale > pre_sale
            THEN '↑'
            WHEN current_sale < pre_sale
            THEN '↓'
       ELSE '-' END AS var
FROM (
	SELECT year,
					sale AS current_sale,
					SUM(sale)OVER (ORDER BY year
											 RANGE BETWEEN 1 PRECEDING
																 AND 1 PRECEDING) AS pre_sale
		FROM Sales) TMP
 ORDER BY year;

像这样,我们可以使用窗口函数来替换关联子查询。

2、时间轴有间断时:和过去最临近的时间进行比较

我们再来看一下其他场景。在上一部分的例题里,各年份的数据都没有间断,然而现实中肯定有财务人员做事马虎的公司,比如这家公司就丢失了过去个别年份的数据(表Sales2)​。

sql 复制代码
CREATE TABLE Sales2 (
	year INTEGER NOT NULL , 
	sale INTEGER NOT NULL , 
	PRIMARY KEY (year)
);

INSERT INTO Sales2 VALUES (1990, 50);
INSERT INTO Sales2 VALUES (1992, 50);
INSERT INTO Sales2 VALUES (1993, 52);
INSERT INTO Sales2 VALUES (1994, 55);
INSERT INTO Sales2 VALUES (1997, 55);

这样一来,​"年份 -1"这个条件就不能用了。我们需要把它扩展成更普遍的情况,用某一年的数据和它过去最临近的年份进行比较。具体点说,我们需要写出一条能让1992年和1990年、1997年和1994年进行比较的SQL语句。通过这个示例,我们可以实际感受到窗口函数的威力。

首先按关联子查询的思路,对某一年来说,​"过去最临近的年份"需要满足两个条件。

  1. 与该年份相比是过去的年份。
  2. 在满足条件1的年份中,年份最早的一个。

如果按这两个条件改写SQL语句,那么应该像下面这样写。

sql 复制代码
-- 查询与过去最临近的年份营业额相同的年份(1):使用关联子查询
SELECT year, sale
FROM Sales2 S1
WHERE sale = (
	SELECT sale
	FROM Sales2 S2
	WHERE S2.year = (
		SELECT MAX(year)-- 条件 2 :在满足条件 1 的年份中,年份最早的一个
		FROM Sales2 S3
    WHERE S1.year > S3.year
	)
) -- 条件1 :与该年份相比是过去的年份
ORDER BY year;

+------+------+
| year | sale |
+------+------+
| 1992 |   50 |
| 1997 |   55 |
+------+------+

这条SQL语句能与过去与其(这里指某一年)最近的年份进行比较,因此即使年份有缺失也没有关系。而且,不只是数值,这条SQL语句还可以应用于字符类型、日期类型等具有顺序的列,通用性比较高。但是,关联子查询的嵌套会变深,性能会变差。

在使用窗口函数的情况下,则无须担心这些问题。通过将帧子句中的RANGE换成ROWS,我们就可以按物理层的记录顺序来设置统计范围了。

sql 复制代码
-- 查询与过去最临近的年份营业额相同的年份(2):使用窗口函数
SELECT year, current_sale
FROM (
	SELECT year,
				sale AS current_sale,
				SUM(sale) OVER (ORDER BY year
												ROWS BETWEEN 1 PRECEDING
																 AND 1 PRECEDING) AS pre_sale
	FROM Sales2) TMP
 WHERE current_sale = pre_sale
 ORDER BY year;

+------+--------------+
| year | current_sale |
+------+--------------+
| 1992 |           50 |
| 1997 |           55 |
+------+--------------+

3、窗口函数与关联子查询

使用关联子查询的代码,和使用窗口函数的代码有如下区别。

  • 虽然使用窗口函数的代码中也使用子查询,但这个子查询并不是"关联"子查询。因此,子查询本身也可以单独执行,代码具有很高的可读性,操作也容易理解。通过仅执行子查询,我们还可以轻松进行调试
  • 使用窗口函数的代码仅对表扫描一次就可以了,性能会更好

总而言之,使用窗口函数的代码读/写起来更简单、性能更好,我们没有什么理由不使用它。

造成这种差异的原因是,关联子查询总是连接多张表(即使自连接中的表在物理层面上是同一张表)​,然后使用SQL的旧功能来进行行间比较,而窗口函数是基于"行的顺序"进行操作的,直接将面向过程语言的循环操作引入到SQL中。

4、为什么可以用窗口函数替换关联子查询

大家已经知道了可以使用窗口函数来替换关联子查询,那么我们再考虑一下缘由,为什么可以使用窗口函数来替换关联子查询呢?它们的语法完全不一样。

因为试着替换了一下,发现结果是一样的------这么说也不是不可以,但实际上,比较它们的操作后,我们就会发现它们的功能其实很相似。这里的关键字就是集合(表)的"分割"​。

我们使用一张记录了商品名称和价格的表Products来确认一下。

我们思考一下如何从表Products中,按照商品种类查询销售单价高于平均销售单价的商品。例如,​"厨房用具"的平均销售单价是2795日元,那么查询结果就是菜刀和高压锅。

这是以前使用关联子查询求解的典型问题,查询代码如下所示。

sql 复制代码
SELECT products_type, products_name, sale_price
FROM Products P1
WHERE sale_price > (
	SELECT AVG(sale_price)
	FROM Products P2
	WHERE P1.products_type = P2.products_type
	GROUP BY products_type
);

 products_type    products_name     sale_price
---------------  ---------------  --------------
 办公用品        打孔器                      500
 衣服            运动 T 恤                  4000
 厨房用具        菜刀                       3000
 厨房用具        高压锅                     6800

这里,我们很难立刻明白关联子查询的操作,但能看出重点是针对集合P1和集合P2的过滤条件P1.products_type = P2. products_type。根据这个条件,我们将查询范围限定为P1和P2两张表中商品种类相同的记录集合,然后逐行比较该集合的平均销售单价与各条记录的销售单价。

下面的SELECT语句中平均销售单价会随商品种类而发生变化,该关联子查询的操作与逐行循环执行这些语句是一样的。

sql 复制代码
SELECT 衣服,     T 恤,          1000 FROM Products WHERE 1000 > 2500;
SELECT 衣服,     运动 T 恤,     1000 FROM Products WHERE 4000 > 2500;
----------------------------------------------------------------------
SELECT 厨房用具, 菜刀,          3000 FROM Products WHERE 3000 > 2795;
SELECT 厨房用具, 高压锅,        6800 FROM Products WHERE 6800 > 2795;
SELECT 厨房用具, 叉子,           500 FROM Products WHERE  500 > 2795;
SELECT 厨房用具, 擦菜板,         880 FROM Products WHERE  880 > 2795;
----------------------------------------------------------------------
SELECT 办公用品, 圆珠笔,         100 FROM Products WHERE  100 >  300;
SELECT 办公用品, 打孔器,         500 FROM Products WHERE  500 >  300;

也就是说,关联子查询和窗口函数实现的功能是一样的,都是分割集合,以记录为单位进行循环。

实现相同操作的窗口函数如下所示。

sql 复制代码
SELECT products_name, products_type, sale_price
FROM (
	SELECT products_name, products_type, sale_price,
			AVG(sale_price) OVER(PARTITION BY products_type) AS avg_price
	FROM Products) TMP
WHERE sale_price > avg_price;

根据子查询中的窗口函数,我们可以计算出各商品种类的平均销售单价(avg_price),并将其存储在avg_price列中。

这个执行结果的好处(或者说方便之处)在于,尽管函数计算出了商品种类的平均销售单价,但并未聚合记录,只是将结果列直接添加到原始表中,确保了信息的完整性。因为表Products中的数据是按原样存储的,所以接下来只要在每一行中简单地写上条件sale_price > avg_price,就可以比较平均销售单价和销售单价了。虽然平均销售单价集合的性质与销售单价集合的元素的性质属于不同层级的信息,但它们可以出现在相同的层级中。

5、查询重叠的时间区间

假设有下面这样一张表Reservations,记录了酒店或者旅馆的预订情况。

sql 复制代码
CREATE TABLE Reservations (
	reserver VARCHAR(30) PRIMARY KEY,
	start_date DATE NOT NULL,
	end_date DATE NOT NULL
);

INSERT INTO Reservations VALUES('木村', '2018-10-26', '2018-10-27');
INSERT INTO Reservations VALUES('荒木', '2018-10-28', '2018-10-31');
INSERT INTO Reservations VALUES('堀',   '2018-10-31', '2018-11-01');
INSERT INTO Reservations VALUES('山本', '2018-11-03', '2018-11-04');
INSERT INTO Reservations VALUES('内田', '2018-11-03', '2018-11-05');
INSERT INTO Reservations VALUES('水谷', '2018-11-06', '2018-11-06');

这张表里没有房间编号,请把表中数据当成是某一房间在某段期间内的预订情况。那么,正常情况下,每天只能有一组客人在该房间住宿。从表中数据可以看出,这里存在重叠的预订日期。为了让大家看得更清晰,我们把表中数据转换成了柱状图​。

显然,这样会有问题,必须马上重新分配房间。我们面对的问题是如何查出住宿日期重叠的客人并列表显示。

与前面一样,我们先考虑使用关联子查询的思路。我们给重叠的住宿日期分类,可知一共有下面3种类型,如图所示。

例如,在堀看来,荒木是属于类型 (1)的;相反,在荒木看来,堀是属于类型 (2)的。而山本的住宿期间完全在内田的住宿期间内,所以在山本看来,内田是属于类型 (3)的。因此,我们只要找出满足这3种类型中任意一种的客人就可以了。但是,稍微考虑一下我们就会发现,其实类型 (3)的情况忽略也没有问题。原因是,满足 (3)和同时满足 (1)、(2)是等价的。

因此,充要条件是满足类型 (1)和类型 (2)中至少一个条件,解答如下所示。

sql 复制代码
-- 求重叠的住宿时间(1):使用关联子查询
SELECT reserver, start_date, end_date
FROM Reservations R1
WHERE EXISTS (
	SELECT *
	FROM Reservations R2
	WHERE R1.reserver <> R2.reserver  -- 与自己以外的客人进行比较
	AND ( R1.start_date BETWEEN R2.start_date AND R2.end_date
	-- 条件(1):自己的入住日期在他人的住宿期间内
	OR R1.end_date BETWEEN R2.start_date AND R2.end_date)
	-- 条件(2):自己的离店日期在他人的住宿期间内
);


+----------+------------+------------+
| reserver | start_date | end_date   |
+----------+------------+------------+
| 内田     | 2018-11-03 | 2018-11-05 |
| 堀       | 2018-10-31 | 2018-11-01 |
| 山本     | 2018-11-03 | 2018-11-04 |
| 荒木     | 2018-10-28 | 2018-10-31 |
+----------+------------+------------+

请注意,因为自己和自己在住宿期间上肯定是重叠的,所以如果没有R1.reserver <> R2.reserver这个条件,所有人都会出现在结果列表里。相反,如果想求"与任何住宿期间都不重叠的日期"​,我们只需要把EXISTS谓词改写成NOT EXISTS谓词就可以了。

接下来,我们看看窗口函数。窗口函数的优势是,无论列的数据类型是否是数值,只要有顺序,就都可以进行排序。这里,如果按入住日期的列对记录进行升序排列,那么记录恰好是按图的甘特图的顺序来排列的。这样一来,在当前行的入住客人之后入住的,就是下一行记录的客人。我们只要检查下一位入住客人的入住日期是否与当前行客人的住宿时间重复就可以了,也就是说,可以使用BETWEEN谓词。使用窗口函数的查询如下所示。

sql 复制代码
-- 求重叠的住宿期间(2):使用窗口函数
SELECT reserver, next_reserver
FROM (
	SELECT reserver,
         start_date,
         end_date,
         MAX(start_date)
					 OVER (ORDER BY start_date
							ROWS BETWEEN 1 FOLLOWING
								AND 1 FOLLOWING) AS next_start_date,
				 MAX(reserver)
					 OVER (ORDER BY start_date
							ROWS BETWEEN 1 FOLLOWING
                AND 1 FOLLOWING) AS next_reserver
	FROM Reservations) TMP
 WHERE next_start_date BETWEEN start_date AND end_date;

+----------+---------------+
| reserver | next_reserver |
+----------+---------------+
| 荒木     | 堀            |
| 内田     | 山本          |
+----------+---------------+

另外,这里虽然使用了MAX窗口函数,但它并不是要获取最大值。由于SUM和AVG只能用于数值,所以这里才用了能用于日期和字符串的MAX函数,反正帧子句ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING已经将范围限制在一行,所以获取最大值也没有意义。因此,这里还可以使用MIN函数。

窗口函数解法的优势,在于重叠的客人能以成对的形式输出到一行中,因此我们一眼就能看出可以将谁与谁调换。这种格式的便利之处体现在有3人以上出现时间区间重叠的情景。下面,我们将水谷的入住日期提前至11月4日。

sql 复制代码
DELETE FROM Reservations WHERE reserver = '山本';
INSERT INTO Reservations VALUES('山本', '2018-11-03', '2018-11-04');
DELETE FROM Reservations WHERE reserver = '水谷';
INSERT INTO Reservations VALUES('水谷', '2018-11-04', '2018-11-06');

对该数据使用关联子查询和窗口函数都能够查询到水谷,但二者之间的含义稍有不同。

关联子查询的结果只表明存在重叠这一实际情况,而窗口函数的结果则表明水谷和内田的住宿时间是重叠的(实际上水谷和山本的住宿时间也是重叠的,但由于结果只显示2列,所以并未显示出来。不过,该结果已经充分满足业务需求了)​。

另外,如果山本的入住日期不是11月3日,而是推迟了一天,即11月4日,那么关联子查询的结果里将不会出现内田。这是因为内田的入住日期和离店日期都不再与任何人重叠。换句话说,像内田这种自己的住宿时间完全包含了他人的住宿时间的情况会被排除。如果也想输出这样的住宿时间,我们需要添加条件(由于关联子查询中并不是一定要编写这么复杂的代码,所以这里就省略了。但在窗口函数的情况下,结果仍是一样的。在这一点上,窗口函数的通用性更高一些。

6、移动平均值

统计指标------移动平均值。买卖股票或外汇的人对此可能比较熟悉,它其实就是偏移记录来求某个窗口范围的平均值,由于窗口看起来像是移动的,所以被称为移动平均值。

例如,现在有一张银行账户存取款历史记录表Accounts,我们将当前行及其之前两行的记录作为窗口来求移动平均值。对于前两行记录不全的情况,有两种处理方式:一种是处理为"无结果"​,另一种是根据既有数据求平均值。这里我们采用后一种处理方式,这也是窗口函数默认的操作。

sql 复制代码
CREATE TABLE Accounts (
	prc_date DATE NOT NULL , 
	prc_amt INTEGER NOT NULL , 
	PRIMARY KEY (prc_date)
) ;

DELETE FROM Accounts;
INSERT INTO Accounts VALUES ('2018-10-26',  12000 );
INSERT INTO Accounts VALUES ('2018-10-28',   2500 );
INSERT INTO Accounts VALUES ('2018-10-31', -15000 );
INSERT INTO Accounts VALUES ('2018-11-03',  34000 );
INSERT INTO Accounts VALUES ('2018-11-04',  -5000 );
INSERT INTO Accounts VALUES ('2018-11-06',   7200 );
INSERT INTO Accounts VALUES ('2018-11-11',  11000 );

使用窗口函数的帧子句可以轻松地求移动平均值。

sql 复制代码
-- 使用窗口函数求移动平均值
SELECT prc_date, 
			 prc_amt,
       AVG(prc_amt) OVER(ORDER BY prc_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) as avg
FROM Accounts;


+------------+---------+------------+
| prc_date   | prc_amt |        avg |
+------------+---------+------------+
| 2018-10-26 |   12000 | 12000.0000 |
| 2018-10-28 |    2500 |  7250.0000 |
| 2018-10-31 |  -15000 |  -166.6667 |
| 2018-11-03 |   34000 |  7166.6667 |
| 2018-11-04 |   -5000 |  4666.6667 |
| 2018-11-06 |    7200 | 12066.6667 |
| 2018-11-11 |   11000 |  4400.0000 |
+------------+---------+------------+

关联子查询的方法实现:

sql 复制代码
-- 使用关联子查询求移动平均值
SELECT prc_date, A1.prc_amt, (
	SELECT AVG(prc_amt)
	FROM Accounts A2
	WHERE A1.prc_date >= A2.prc_date
	AND (
		SELECT COUNT(*)
		FROM Accounts A3
    WHERE A3.prc_date BETWEEN A2.prc_date AND A1.prc_date 
	) <= 3 
) AS mvg_sum
FROM Accounts A1
ORDER BY prc_date;

+------------+---------+------------+
| prc_date   | prc_amt |    mvg_sum |
+------------+---------+------------+
| 2018-10-26 |   12000 | 12000.0000 |
| 2018-10-28 |    2500 |  7250.0000 |
| 2018-10-31 |  -15000 |  -166.6667 |
| 2018-11-03 |   34000 |  7166.6667 |
| 2018-11-04 |   -5000 |  4666.6667 |
| 2018-11-06 |    7200 | 12066.6667 |
| 2018-11-11 |   11000 |  4400.0000 |
+------------+---------+------------+

在上一道练习题中,即使记录少于3行也能求移动平均值。请思考一下,如何将记录少于3行时的处理结果改为NULL。使用窗口函数实现的人,也试着思考一下如何用关联子查询来实现。

sql 复制代码
-- 窗口函数
SELECT prc_date, 
			 prc_amt,
       CASE WHEN cnt < 3 THEN NULL
            ELSE mvg_avg END AS mvg_avg
FROM (
	SELECT prc_date, prc_amt,
         AVG(prc_amt) OVER(ORDER BY prc_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) mvg_avg,
         COUNT(*) OVER (ORDER BY prc_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS cnt
  FROM Accounts
) TMP;

+------------+---------+------------+
| prc_date   | prc_amt |    mvg_avg |
+------------+---------+------------+
| 2018-10-26 |   12000 |     <null> |
| 2018-10-28 |    2500 |     <null> |
| 2018-10-31 |  -15000 |  -166.6667 |
| 2018-11-03 |   34000 |  7166.6667 |
| 2018-11-04 |   -5000 |  4666.6667 |
| 2018-11-06 |    7200 | 12066.6667 |
| 2018-11-11 |   11000 |  4400.0000 |
+------------+---------+------------+


-- 关联子查询
SELECT prc_date, A1.prc_amt, (
	SELECT AVG(prc_amt)
  FROM Accounts A2
  WHERE A1.prc_date >= A2.prc_date
  AND (
		SELECT COUNT(*)
    FROM Accounts A3
    WHERE A3.prc_date BETWEEN A2.prc_date AND A1.prc_date 
	) <= 3
	HAVING COUNT(*) = 3
) AS mvg_sum -- 不满3行数据就不显示
FROM Accounts A1
ORDER BY prc_date;


+------------+---------+------------+
| prc_date   | prc_amt |    mvg_sum |
+------------+---------+------------+
| 2018-10-26 |   12000 |     <null> |
| 2018-10-28 |    2500 |     <null> |
| 2018-10-31 |  -15000 |  -166.6667 |
| 2018-11-03 |   34000 |  7166.6667 |
| 2018-11-04 |   -5000 |  4666.6667 |
| 2018-11-06 |    7200 | 12066.6667 |
| 2018-11-11 |   11000 |  4400.0000 |
+------------+---------+------------+
相关推荐
杨云龙UP2 小时前
Windows SQL Server 备份无法传输至异地共享目录排查处理
运维·服务器·数据库·windows·共享·smb·异地备份传输
落寞的电源2 小时前
Delegate = Object + MethodInfo
开发语言·数据库·c#
要开心吖ZSH2 小时前
处方物流信息同步优化:从 36 秒到亚秒级的踩坑记录
java·数据库·mysql·性能优化
隐形的萝莉2 小时前
再回到技术面,研究 T-SQL 的 UNION、EXISTS、EXCEPT、INTERSECT 运算符。
数据库·sql
Nturmoils3 小时前
JOIN 不是拼上就完事,多表查询这几个坑先跑一遍
数据库·后端
yoothey3 小时前
MySQL 数据库连接池调优实战
数据库·mysql
水无痕simon3 小时前
5 多表操作
java·开发语言·数据库
云絮.3 小时前
计算机相关工作原理
服务器·前端·数据库
Tian_Hang4 小时前
Eclipse Mosquitto 安装及介绍
java·运维·服务器·ide·sql·mysql·eclipse