使用窗口函数

窗口函数在窗口上运行,窗口是多组相关的行(例如,按日期或位置分组)。本主题介绍如何使用 Snowflake 支持的不同类型的窗口函数,包括:

  • 常规窗口函数。

  • 计算排名(例如最高、第二高等)的窗口函数。

  • 支持累积窗口框架和滑动窗口框架的窗口函数。

本文档面向尚未精通窗口函数的读者。已经精通这些函数的读者阅读参考资料就已足够:

本主题内容:

窗口概念

窗口 是一组行。一个窗口可以包含 0 行、1 行或多行。但是,为了简单起见,我们通常只说一个窗口包含“多行”。

窗口中的所有行都以某种方式相关,例如按位置(例如,所有行都来自同一个城市)或按时间(例如,所有行都来自同一财年)。

使用窗口的函数是 窗口函数

归类为窗口函数的函数有助于回答与标量函数不同类型的问题:

  • 使用标量函数的查询仅使用该行中的数据来回答有关该行的问题。

  • 使用窗口函数的查询可以回答有关一行与同一窗口中 其他行 的关系的问题。

例如,假设您管理着五家连锁店的一个分店。要计算您的商店的利润,计算只需查看具体商店的相关信息,例如商店的收入和成本。您要对该查询使用标量函数。

要计算您的商店 相对于其他商店 的利润,计算不仅必须查看您商店的信息,还必须查看其他商店的信息。您要对该查询使用窗口函数。

您可以将窗口函数视为采用两个实参:第一个实参是要在计算中使用的列或表达式,例如“收入”或“利润”。第二个实参定义窗口(即比较中使用的行组);窗口包括当前行(您的商店)和与之进行比较的其他行(同一连锁店中的其他商店)。

要计算您的商店在整个连锁店利润中的占比,您需要将商店的利润除以所有商店的总利润(分店利润 / 连锁店总利润)。

为了帮助解释窗口函数,本主题演示如何在有和没有窗口函数的情况下计算分店在公司利润中所占的百分比。

以下示例显示了一种在 没有 窗口函数的情况下计算商店占连锁店总利润百分比的方法。

首先创建表,加载数据,并计算每个商店的利润。

CREATE TRANSIENT TABLE store_sales (
    branch_ID    INTEGER,
    city        VARCHAR,
    gross_sales NUMERIC(9, 2),
    gross_costs NUMERIC(9, 2),
    net_profit  NUMERIC(9, 2)
    );

INSERT INTO store_sales (branch_ID, city, gross_sales, gross_costs)
    VALUES
    (1, 'Vancouver', 110000, 100000),
    (2, 'Vancouver', 140000, 125000),
    (3, 'Montreal', 150000, 140000),
    (4, 'Montreal', 155000, 146000);

UPDATE store_sales SET net_profit = gross_sales - gross_costs;
Copy

现在显示每家商店占连锁店总利润的百分比:

SELECT branch_ID,
       net_profit AS store_profit,
       (SELECT SUM(net_profit) FROM store_sales) AS chain_profit,
       store_profit / chain_profit * 100 AS store_percentage_of_chain_profit
     FROM store_sales
     ORDER BY branch_ID;
+-----------+--------------+--------------+----------------------------------+
| BRANCH_ID | STORE_PROFIT | CHAIN_PROFIT | STORE_PERCENTAGE_OF_CHAIN_PROFIT |
|-----------+--------------+--------------+----------------------------------|
|         1 |     10000.00 |     44000.00 |                      22.72727300 |
|         2 |     15000.00 |     44000.00 |                      34.09090900 |
|         3 |     10000.00 |     44000.00 |                      22.72727300 |
|         4 |      9000.00 |     44000.00 |                      20.45454500 |
+-----------+--------------+--------------+----------------------------------+
Copy

如果您需要一个更精细的报告,显示每个商店的利润占该城市所有商店的百分比,请使用以下查询:

SELECT branch_ID,
       net_profit AS store_profit,
       (SELECT SUM(net_profit) FROM store_sales AS s2 WHERE s2.city = s1.city) AS city_profit,
       store_profit / city_profit * 100 AS store_percentage_of_city_profit
    FROM store_sales AS s1
    ORDER BY branch_ID;
+-----------+--------------+-------------+---------------------------------+
| BRANCH_ID | STORE_PROFIT | CITY_PROFIT | STORE_PERCENTAGE_OF_CITY_PROFIT |
|-----------+--------------+-------------+---------------------------------|
|         1 |     10000.00 |    25000.00 |                     40.00000000 |
|         2 |     15000.00 |    25000.00 |                     60.00000000 |
|         3 |     10000.00 |    19000.00 |                     52.63157900 |
|         4 |      9000.00 |    19000.00 |                     47.36842100 |
+-----------+--------------+-------------+---------------------------------+
Copy

如果有一个函数可以实现大致相同的目标,将您的商店的利润除以所有商店的利润之和(或者除以指定商店组的利润总和,例如,同一城市的所有商店),那就太好了。这样的函数可以有两个实参,其中一个是要进行计算的列,另一个指定要与哪些行进行比较。第二列可能有点像 WHERE 子句。您可以如下所示使用该函数(这是伪代码, 不是 有效的 SQL):

SELECT branch_ID,
       PERCENTAGE(net_profit, <where_condition>)
    FROM store_sales;
Copy

此函数将当前行(当前商店)的利润除以满足 <where_condition> 的所有商店的利润之和。

例如,要计算每个城市中每家商店的利润占比,伪代码将类似于:

SELECT branch_ID,
       PERCENTAGE(net_profit, 'city')
     FROM store_sales;
Copy

SQL 不支持上面显示的语法,但它支持窗口函数的概念,该函数基于当前行和定义的行组返回结果。

Snowflake 没有名为 PERCENTAGE 的函数,但有一个名为 RATIO_TO_REPORT 的函数,该函数可将当前行中的值除以窗口中所有行中的值之和。以下是前面查询的等效项。

SELECT branch_ID,
       city,
       100 * RATIO_TO_REPORT(net_profit) OVER (PARTITION BY city)
    FROM store_sales AS s1
    ORDER BY city, branch_ID;
+-----------+-----------+------------------------------------------------------------+
| BRANCH_ID | CITY      | 100 * RATIO_TO_REPORT(NET_PROFIT) OVER (PARTITION BY CITY) |
|-----------+-----------+------------------------------------------------------------|
|         3 | Montreal  |                                                52.63157900 |
|         4 | Montreal  |                                                47.36842100 |
|         1 | Vancouver |                                                40.00000000 |
|         2 | Vancouver |                                                60.00000000 |
+-----------+-----------+------------------------------------------------------------+
Copy

OVER() 子句定义计算中使用的行组。它的作用与我们前面的 PERCENTAGE 函数中假设的第二个实参 ( <where_condition>) 相同。

PARTITION BY 子句分支允许我们将该窗口划分为子窗口,在本例中,每个城市一个子窗口。(如果语句未显式使用 PARTITION BY 子句,则窗口函数将整个输入作为单个窗口进行运算。)

如果您想查看相对于整个连锁店的利润占比比,而不仅仅是特定城市内的商店,请忽略以下 PARTITION BY 子句:

SELECT branch_ID,
       100 * RATIO_TO_REPORT(net_profit) OVER ()
    FROM store_sales AS s1
    ORDER BY branch_ID;
+-----------+-------------------------------------------+
| BRANCH_ID | 100 * RATIO_TO_REPORT(NET_PROFIT) OVER () |
|-----------+-------------------------------------------|
|         1 |                               22.72727300 |
|         2 |                               34.09090900 |
|         3 |                               22.72727300 |
|         4 |                               20.45454500 |
+-----------+-------------------------------------------+
Copy

顺序敏感型窗口函数

上一个查询的结果不依赖于 PARTITION BY 子句分支所选行的顺序。商店在公司利润中所占的百分比不取决于查询读取其他商店数据的顺序。

但是,某些查询对顺序敏感。有两种类型的顺序敏感型窗口函数:

  • 与排名相关的窗口函数。

  • 窗口框架函数。

有些函数既是与排名相关的函数,又是窗口框架函数。

顺序敏感型函数使用 OVER 子句的 ORDER BY 子句分支,例如:

select order_sensitive_function(column_name) over ( [partition by <col2>] order by <col3> )
...
Copy

ORDER BY 子句可以与 ``ASC``(升序)或 ``DESC``(降序)一起使用。默认为升序。

OVER 子句的 ORDER BY 子句分支与对查询的最终输出进行排序的 ORDER BY 子句是分开的。)

窗口框架函数

窗口框架 是窗口中行的子集。

窗口框架函数使用窗口框架来计算移动平均线等内容。

Snowflake 支持两种类型的窗口框架:

  • 累积。

  • 滑动。

累积窗口框架

累积窗口是一个可变宽度的窗口,从固定点开始,并随着每个新行的增长而增长。例如,“本月迄今为止的销售额”是使用累积窗口计算的,该窗口从本月的第一天开始,并随着每天新数据的添加而增长:

+--------+-------+--------------+
| Day of | Sales | Sales So Far |
| Month  | Today | This Month   |
|--------+-------+--------------+
|      1 |    10 |           10 |
|      2 |    10 |           20 |
|      3 |    10 |           30 |
|    ... |   ... |          ... |
+--------+-------+--------------+
Copy

通常,累积窗口会定期从 0 开始。例如,如果上表显示了 2 月份的销售额,则在 3 月 1 日,窗口框架的起点将变为 3 月 1 日,并且该月到目前为止的销售额将重置为 0 并从 3 月 1 日开始计数。

滑动窗口框架

您可以通过设想从正在与栅栏平行行驶的汽车侧窗向外看的情景来理解滑动窗口的概念。如果栅栏柱之间的间距均匀,且车窗宽度是栅栏柱间距的整数倍,那么您看到的栅栏柱数量将保持不变;然而,随着汽车前行,“旧”的栅栏柱会离开您的视线范围,“新”的栅栏柱则进入视线范围,尽管在任何时刻您看到的栅栏柱数量相同,但您看的并不是同一根栅栏柱。

滑动窗口框架是一种固定宽度的窗口框架,它沿着窗口中的行“滑动”,每次都显示窗口的不同切片。与驶过栅栏柱的汽车一样,窗口框架沿着数据移动,旧行从框架中消失,新行出现,因此框架的宽度(框架中的行数)始终相同。

滑动窗口通常用于计算移动平均线。移动平均线是根据固定大小的区间(例如天数)计算的平均值。平均值是“移动的”,因为尽管间隔的大小是恒定的,但随着窗口的滑动,间隔中的实际值会随时间(或其他因素)而变化。

例如,股票市场分析师通常部分基于股票价格的 13 周移动平均线来分析股票。今天的移动平均线价格是今天结束时的价格和最近 13 周内每天结束时的价格的平均值。如果股票每周交易 5 天,并且在过去 13 周内没有假期,则移动平均线是最近 65 个交易日(包括今天)中每个交易日的平均价格。

以下示例显示了 6 月最后一天和 7 月头几天股票价格的 13 周(91 天)移动平均线的变化情况:

  • 6 月 30 日,函数返回 4 月 1 日至 6 月 30 日(含)的平均价格。

  • 7 月 1 日,函数返回 4 月 2 日至 7 月 1 日(含)的平均价格。

  • 7 月 2 日,函数返回 4 月 3 日至 7 月 2 日(含)的平均价格。

备注

尽管滑动窗口是固定宽度的,但当窗口首次应用于新数据源时,数据源可能没有足够的数据来填充该窗口。例如,如果记录保存从 4 月 1 日开始,则从 4 月 1 日到 6 月 29 日,滑动窗口将包含少于 91 天的数据。只有在窗口完成填充后,窗口的宽度才会保持不变。

下面的示例在当月的前 7 天使用一个小的(3 天)滑动窗口。这演示了窗口框架如何在窗口中滑动,始终保留 3 个最新值,用于计算窗口内的总数。此图考虑到在周期开始时,窗口可能未满:

7 天窗口内的 3 天滑动窗口框架

而且,正如您在相应的表中看到的那样,最后一列包含最近三天的销售数据的总和。例如,第 4 天的列值为 36,它是第 2、3 和 4 天的销售额的总和 (11 + 12 + 13):

+--------+-------+---------------+
| Day of | Sales | Most Recent   |
| Month  | Today | 3 Days' Sales |
|--------+-------+---------------+
|      1 |    10 |            10 |
|      2 |    11 |            21 |
|      3 |    12 |            33 |
|      4 |    13 |            36 |
|      5 |    14 |            39 |
|    ... |   ... |           ... |
+--------+-------+---------------+
Copy

有关顺序敏感型窗口函数的更多信息

PARTITION BY 和 ORDER BY 子句是独立的。您可以使用 ORDER BY 子句而不使用 PARTITION BY 子句。要将您的商店与连锁店中的所有其他商店(而不仅仅是您所在城市的其他商店)进行排名,请使用以下查询:

SELECT
    branch_ID,
    net_profit,
    RANK() OVER (ORDER BY net_profit DESC) AS sales_rank
  FROM store_sales
Copy

备注

OVER 子句中的 ORDER BY 子句分支仅控制窗口函数处理行的顺序;子句不控制整个查询的输出。控制查询的输出顺序仍然需要在查询的顶层使用外部 ORDER BY 子句。这些 ORDER BY 子句是独立的,并不相互排斥。以下查询使用第一个 ORDER BY 子句来控制窗口函数的处理,使用第二个 ORDER BY 子句来控制整个查询输出的顺序:

SELECT
    branch_ID,
    net_profit,
    RANK() OVER (ORDER BY net_profit DESC) AS sales_rank
  FROM store_sales
  ORDER BY branch_ID;
Copy

有些窗口函数对顺序敏感,有些则不然。

  • 某些窗口函数需要 ORDER BY 子句。

  • 某些窗口函数禁止 ORDER BY 子句。

  • 某些窗口函数使用 ORDER BY 子句(如果存在),但不需要该子句。

  • 某些窗口函数将 ORDER BY 子句视为隐式累积窗口框架子句。(有关隐式窗口框架的更多信息,请参阅 窗口框架使用说明。)

小心

在大多数情况下, SQL 是一种显式语言。语言完全遵循您的指示。隐式子句很少,即一个子句隐式地创建另一个不可见的子句。对于某些窗口函数, ORDER BY 子句会隐式添加其他子句(累积窗口框架子句)。这会变更行为。有关隐式窗口框架子句的更多信息,请参阅 窗口框架使用说明

由于隐式行为(而非显式行为)可能会导致难以理解的结果,因此 Snowflake 建议避免使用隐式窗口框架。相反,请将所有窗口框架都设为显式。

下图显示了窗口函数、窗口框架函数和排名相关函数之间的关系:

                      Window Functions

  Order-insensitive Functions      Order-sensitive Functions
+-----------------------------+--------------------------------------------------------------------+
|                             | Rank-related Functions                                             |
|                             +--------------------------------------------------------------------+
|                             | Functions that can be both Rank-related and Window Frame functions |
|                             +--------------------------------------------------------------------+
|                             | Window Frame Functions                                             |
+-----------------------------+--------------------------------------------------------------------+
Copy

如上图所示:

  • 某些窗口函数对顺序不敏感。

  • 某些窗口函数对顺序敏感。

  • 顺序敏感型窗口函数分为两类:

    • 与排名相关的函数。

    • 窗口框架函数。

  • 一些顺序敏感型函数既是与排名相关的函数,也是窗口框架函数。

窗口函数与聚合函数

许多窗口函数和 聚合函数 名称相同。例如,有一个 SUM() 窗口函数和一个 SUM() 聚合函数。

要区分两者的用法,请记住:

  • 对于聚合函数,输入为多行,输出为 1 行。

  • 对于窗口函数,有两项输入,一项是行窗口,一项是该窗口内的一行,输出为每个 输入行 1 行。

聚合函数会指明有关所有合并行的信息,例如 SUM,但不提供有关单个行的信息。

窗口函数会指明当前行相对于窗口中所有其他行的一些信息。

以下 SQL 语句显示了使用 SUM() 聚合函数(为整个输入行组返回 1 行)和使用 SUM() 窗口函数(为输入行组中的每一行返回 1 行)之间的区别:

SELECT SUM(net_profit) 
    FROM store_sales;
+-----------------+
| SUM(NET_PROFIT) |
|-----------------|
|        44000.00 |
+-----------------+
SELECT branch_ID, SUM(net_profit) OVER ()
    FROM store_sales
    ORDER BY branch_id;
+-----------+-------------------------+
| BRANCH_ID | SUM(NET_PROFIT) OVER () |
|-----------+-------------------------|
|         1 |                44000.00 |
|         2 |                44000.00 |
|         3 |                44000.00 |
|         4 |                44000.00 |
+-----------+-------------------------+
Copy

调用窗口函数

本节提供有关调用窗口函数的语法的详细信息。

窗口和窗口框架使用 OVER 子句指定:

... OVER ( [ PARTITION BY <expr1> ]
           [ ORDER BY <expr2> [ { cumulativeFrame | slidingFrame } ] ]
         ) ...
Copy

窗口函数 中更详细地介绍了窗口框架语法。本主题重点介绍窗口函数所需的语法子集,具体如下:

  • PARTITION BY 子句分支将数据分区为多个窗口。例如,如果要计算超过一个月的月销售额,可以按月对数据进行分区。这将允许您计算 1 月份的销售总额、2 月份的销售总额等。

  • ORDER BY 子句分支主要用于与排名相关的窗口函数以及滑动和累积窗口框架函数;它决定了每个窗口内行的顺序。

图解示例

此示例使用一个销售场景来说明本主题前面介绍的许多概念。

假设您需要生成财务报表,该报表显示基于上周销售额的值:

  • 每日销售额

  • **周内排名**(即当周从高到低的销售额排名)

    这使用*与排名相关的窗口函数* (RANK)。

  • **本周迄今为止的销售额**(即从本周开始到当天(包括当天)的所有天数的“累计金额”)

    这使用包含 累积窗口框架窗口函数 (SUM)。

  • 本周总销售额

    这使用 SUM 作为一个简单的窗口函数。

  • **3 天移动平均线**(即当天和前两天的平均值)

    这使用 (AVG) 作为包含 滑动窗口框架窗口函数

报表可能如下所示:

+--------+-------+------+--------------+-------------+--------------+
| Day of | Sales | Rank | Sales So Far | Total Sales | 3-Day Moving |
| Week   | Today |      | This Week    | This Week   | Average      |
|--------+-------+------+--------------+-------------|--------------+
|      1 |    10 |    4 |           10 |          84 |         10.0 |
|      2 |    14 |    3 |           24 |          84 |         12.0 |
|      3 |     6 |    5 |           30 |          84 |         10.0 |
|      4 |     6 |    5 |           36 |          84 |          9.0 |
|      5 |    14 |    3 |           50 |          84 |         10.0 |
|      6 |    16 |    2 |           66 |          84 |         11.0 |
|      7 |    18 |    1 |           84 |          84 |         12.0 |
+--------+-------+------+--------------+-------------+--------------+
Copy

此查询的 SQL 有点复杂。本讨论不是将其显示为单次查询,而是将 SQL 细分为单个列。

在实际场景中,您可能拥有多年的数据,因此要计算特定一周数据的总和和平均值,您需要使用一周窗口,或使用如下所示的筛选器:

... WHERE date >= start_of_relevant_week and date <= end_of_relevant_week ...
Copy

但是,对于此示例,假设该表仅包含最近一周的数据。

CREATE TABLE store_sales_2 (
    day INTEGER,
    sales_today INTEGER
    );
+-------------------------------------------+
| status                                    |
|-------------------------------------------|
| Table STORE_SALES_2 successfully created. |
+-------------------------------------------+
INSERT INTO store_sales_2 (day, sales_today) VALUES
    (1, 10),
    (2, 14),
    (3,  6),
    (4,  6),
    (5, 14),
    (6, 16),
    (7, 18);
+-------------------------+
| number of rows inserted |
|-------------------------|
|                       7 |
+-------------------------+
Copy

计算销售额排名

使用 RANK 函数计算 Rank 列:

SELECT day, 
       sales_today, 
       RANK()
           OVER (ORDER BY sales_today DESC) AS Rank
    FROM store_sales_2
    ORDER BY day;
+-----+-------------+------+
| DAY | SALES_TODAY | RANK |
|-----+-------------+------|
|   1 |          10 |    5 |
|   2 |          14 |    3 |
|   3 |           6 |    6 |
|   4 |           6 |    6 |
|   5 |          14 |    3 |
|   6 |          16 |    2 |
|   7 |          18 |    1 |
+-----+-------------+------+
Copy

请注意,虽然该时间段有 7 天,但只有 5 个不同的排名(1、2、3、5、6)。有两个并列(第 3 名和第 6 名),因此没有排名第 4 或第 7 的行。

计算本周到目前为止的销售额

使用 SUM 作为包含累积窗口框架的窗口函数计算 Sales So Far This Week 列:

SELECT day, 
       sales_today, 
       SUM(sales_today)
           OVER (ORDER BY day
               ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
               AS "SALES SO FAR THIS WEEK"
    FROM store_sales_2
    ORDER BY day;
+-----+-------------+------------------------+
| DAY | SALES_TODAY | SALES SO FAR THIS WEEK |
|-----+-------------+------------------------|
|   1 |          10 |                     10 |
|   2 |          14 |                     24 |
|   3 |           6 |                     30 |
|   4 |           6 |                     36 |
|   5 |          14 |                     50 |
|   6 |          16 |                     66 |
|   7 |          18 |                     84 |
+-----+-------------+------------------------+
Copy

此查询按日期对行进行排序,然后针对每个日期计算从窗口开始到当前日期(含)的销售额总和。

计算本周总销售额

使用 SUM 作为简单的窗口函数计算 Total Sales This Week 列。

SELECT day, 
       sales_today, 
       SUM(sales_today)
           OVER ()
               AS total_sales
    FROM store_sales_2
    ORDER BY day;
+-----+-------------+-------------+
| DAY | SALES_TODAY | TOTAL_SALES |
|-----+-------------+-------------|
|   1 |          10 |          84 |
|   2 |          14 |          84 |
|   3 |           6 |          84 |
|   4 |           6 |          84 |
|   5 |          14 |          84 |
|   6 |          16 |          84 |
|   7 |          18 |          84 |
+-----+-------------+-------------+
Copy

计算 3 天的移动平均线

使用 AVG 作为包含滑动窗口框架的窗口函数计算 3-Day Moving Average 列:

SELECT day, 
       sales_today, 
       AVG(sales_today)
           OVER (ORDER BY day ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
               AS "3-DAY MOVING AVERAGE"
    FROM store_sales_2
    ORDER BY day;
+-----+-------------+----------------------+
| DAY | SALES_TODAY | 3-DAY MOVING AVERAGE |
|-----+-------------+----------------------|
|   1 |          10 |               10.000 |
|   2 |          14 |               12.000 |
|   3 |           6 |               10.000 |
|   4 |           6 |                8.666 |
|   5 |          14 |                8.666 |
|   6 |          16 |               12.000 |
|   7 |          18 |               16.000 |
+-----+-------------+----------------------+
Copy

这个滑动窗口框架和前面描述的窗口框架的区别只有起点:

  • 在滑动窗口框架中,起点向右滑动。

  • 在累积窗口框架中,起点是固定的,并且框架会随着窗口内每一行的增加而不断累积。

组合使用

下面是我们查询的最终版本,显示:

  • 排名。

  • 累积窗口框架。

  • 简单的窗口函数。

  • 滑动窗口函数。

    SELECT day, 
           sales_today, 
           RANK()
               OVER (ORDER BY sales_today DESC) AS Rank,
           SUM(sales_today)
               OVER (ORDER BY day
                   ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
                   AS "SALES SO FAR THIS WEEK",
           SUM(sales_today)
               OVER ()
                   AS total_sales,
           AVG(sales_today)
               OVER (ORDER BY day ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
                   AS "3-DAY MOVING AVERAGE"
        FROM store_sales_2
        ORDER BY day;
    +-----+-------------+------+------------------------+-------------+----------------------+
    | DAY | SALES_TODAY | RANK | SALES SO FAR THIS WEEK | TOTAL_SALES | 3-DAY MOVING AVERAGE |
    |-----+-------------+------+------------------------+-------------+----------------------|
    |   1 |          10 |    5 |                     10 |          84 |               10.000 |
    |   2 |          14 |    3 |                     24 |          84 |               12.000 |
    |   3 |           6 |    6 |                     30 |          84 |               10.000 |
    |   4 |           6 |    6 |                     36 |          84 |                8.666 |
    |   5 |          14 |    3 |                     50 |          84 |                8.666 |
    |   6 |          16 |    2 |                     66 |          84 |               12.000 |
    |   7 |          18 |    1 |                     84 |          84 |               16.000 |
    +-----+-------------+------+------------------------+-------------+----------------------+
    
    Copy

其他示例

本节提供了在窗口函数中使用窗口子句分支的更详细示例,并说明了这些子句分支如何协同工作。

这些示例使用下表和数据:

CREATE TABLE sales (sales_date DATE, quantity INTEGER);

INSERT INTO sales (sales_date, quantity) VALUES
    ('2018-01-01', 1),
    ('2018-01-02', 3),
    ('2018-01-03', 5),
    ('2018-02-01', 2)
    ;
Copy

备注

其中许多示例使用 两个 ORDER BY 子句,一个用于窗口子句,另一个用于以最易读的顺序提供结果集。就本主题而言,对 ORDER BY 子句的引用通常是对窗口中子句的引用。

包含 PARTITION BY 子句的窗口

窗口子句的 PARTITION BY 子句分支根据输入表达式的值将数据划分为不同的子集; SELECT 语句应用于每个子集,输出具有每个子集的行。

请注意,这与 GROUP BY 子句的工作方式相似,但并不完全相同。

下一个示例显示每个月的销售数量,并使用 PARTITION BY 子句将数据划分为一个月的子集:

SELECT MONTH(sales_date) AS MONTH_NUM, 
       SUM(quantity) OVER (PARTITION BY MONTH(sales_date)) AS QUANTITY
    FROM sales
    ORDER BY sales_date;
+-----------+----------+
| MONTH_NUM | QUANTITY |
|-----------+----------|
|         1 |        9 |
|         1 |        9 |
|         1 |        9 |
|         2 |        2 |
+-----------+----------+
Copy

如您所见,前三行是重复的。月份 #1 有 3 个输入行,窗口函数为每个输入行生成一个输出行,因此输出中有 3 个月份 #1 的输出行。SUM 函数不用作累积函数或滑动函数;它应用于整个窗口,并且每次都返回整个窗口的相同值,因此该函数返回重复值,如上所示。

您可以使用 DISTINCT 关键字减少重复项:

SELECT DISTINCT MONTH(sales_date) AS MONTH_NUM, 
       SUM(quantity) OVER (PARTITION BY MONTH(sales_date))
    FROM sales
    ORDER BY MONTH_NUM;
+-----------+-----------------------------------------------------+
| MONTH_NUM | SUM(QUANTITY) OVER (PARTITION BY MONTH(SALES_DATE)) |
|-----------+-----------------------------------------------------|
|         1 |                                                   9 |
|         2 |                                                   2 |
+-----------+-----------------------------------------------------+
Copy

在这种特殊情况下,您可以使用 GROUP BY 子句而不是窗口子句。例如:

SELECT MONTH(sales_date) AS MONTH_NUM, 
       SUM(quantity)
    FROM sales
    GROUP BY MONTH_NUM
    ORDER BY MONTH_NUM;
+-----------+---------------+
| MONTH_NUM | SUM(QUANTITY) |
|-----------+---------------|
|         1 |             9 |
|         2 |             2 |
+-----------+---------------+
Copy

但是, GROUP BY 不如窗口子句灵活。此外,在更复杂的查询中,不能总是用 GROUP BY 代替窗口子句。

包含 ORDER BY 子句的窗口

ORDER BY 子句控制每个窗口(如果有多个分区,则控制每个分区)中数据的顺序。如果要在添加新行时显示一段时间内的“累计总和”,这将非常有用。

可以从窗口的开头到当前行(含)或从当前行到窗口的末尾计算累计总和。

查询可以使用“滑动”窗口,这是一个固定宽度的窗口,用于处理相对于当前行的 N 个指定行(例如,最近的 10 行,包括当前行)。

累积窗口和滑动窗口如下所述。

累积窗口框架示例

在“累积”窗口框架中,从窗口开始到当前行(或从当前行到窗口的末尾)计算值:

SELECT MONTH(sales_date) AS MONTH_NUM, 
       quantity, 
       SUM(quantity) OVER (ORDER BY MONTH(sales_date)
                     ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
           AS CUMULATIVE_SUM_QUANTITY
    FROM sales
    ORDER BY sales_date;
Copy

查询结果包括显示 CUMULATIVE_SUM_QUANTITY 列计算方式的其他注释:

+-----------+----------+-------------------------+
| MONTH_NUM | QUANTITY | CUMULATIVE_SUM_QUANTITY |
|-----------+----------+-------------------------|
|         1 |        1 |                       1 |  -- sum = 1
|         1 |        3 |                       4 |  -- sum = 1 + 3
|         1 |        5 |                       9 |  -- sum = 1 + 3 + 5
|         2 |        2 |                      11 |  -- sum = 1 + 3 + 5 + 2
+-----------+----------+-------------------------+
Copy

滑动窗口框架示例

在金融领域,分析师经常研究“移动平均线”。

例如,您可能有一个图表,其中 X 轴是时间,Y 轴显示股票过去 13 周的平均价格(即“13 周移动平均线”)。在股票价格的 13 周移动平均线图中,显示的 6 月 30 日的价格不是 6 月 30 日的股票价格,而是截至 6 月 30 日(包括 6 月 30 日)的 13 周(即 4 月 1 日至 6 月 30 日)的股票 平均 价格。7 月 1 日的值是 4 月 2 日至 7 月 1 日的平均价格;7 月 2 日的值是 4 月 3 日至 7 月 2 日的平均价格,以此类推。每天,该窗口都会有效地将最近一天的值添加到移动平均线中,并删除最旧的一天的值。这消除了日常波动,并且更便于发现趋势。

移动平均线可以使用“滑动窗口”计算。窗口具有特定的行宽度。在上面的股价示例中,13 周是 91 天,因此滑动窗口将是 91 天。如果每天衡量一次(例如在一天结束时),则窗口将是 91 行“宽”。

要定义 91 行宽的窗口,请执行以下操作:

SELECT AVG(price) OVER (ORDER BY timestamp1
                        ROWS BETWEEN 90 PRECEDING AND CURRENT ROW)
    FROM sales;
Copy

备注

当窗口开始时,它的宽度可能小于 91 天。例如,假设您想要股票的 13 周移动平均价格。如果股票是在 4 月 1 日首次创建的,那么在 4 月 3 日只有 3 天的价格信息存在,因此窗口只有 3 行宽。

以下示例显示了在足以容纳两个样本的滑动窗口上求和的结果:

SELECT MONTH(sales_date) AS MONTH_NUM,
       quantity,
       SUM(quantity) OVER (ORDER BY sales_date
                           ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) 
           AS SLIDING_SUM_QUANTITY
  FROM sales
  ORDER BY sales_date;
Copy

查询结果包括显示 SLIDING_SUM_QUANTITY 列计算方式的其他注释:

+-----------+----------+----------------------+
| MONTH_NUM | QUANTITY | SLIDING_SUM_QUANTITY |
|-----------+----------+----------------------+
|         1 |        1 |                   1  |  -- sum = 1
|         1 |        3 |                   4  |  -- sum = 1 + 3
|         1 |        5 |                   8  |  -- sum = 3 + 5 (1 is no longer in the window)
|         2 |        2 |                   7  |  -- sum = 5 + 2 (3 is no longer in the window)
+-----------+----------+----------------------+
Copy

请注意,“滑动窗口”功能需要 ORDER BY 子句;滑动窗口必须知道行进入和退出滑动窗口的顺序。

包含 PARTITION BY 和 ORDER BY 子句的窗口

您可以组合使用 PARTITION BYORDER BY 子句来获取分区内的累计总和。在此示例中,分区为一个月,并且由于总和仅适用于分区,因此总和将在每个新月份的月初重置为 0

SELECT MONTH(sales_date) AS MONTH_NUM,
       SUM(quantity) OVER (PARTITION BY MONTH(sales_date) ORDER BY sales_date)
          AS MONTHLY_CUMULATIVE_SUM_QUANTITY
    FROM sales
    ORDER BY sales_date;
Copy

查询结果包括显示 MONTHLY_CUMULATIVE_SUM_QUANTITY 列计算方式的其他注释:

+-----------+---------------------------------+
| MONTH_NUM | MONTHLY_CUMULATIVE_SUM_QUANTITY |
|-----------+---------------------------------+
|         1 |                               1 |  -- sum = 1
|         1 |                               4 |  -- sum = 1 + 3
|         1 |                               9 |  -- sum = 1 + 3 + 5
|         2 |                               2 |  -- sum = 0 + 2 (new month)
+-----------+---------------------------------+
Copy

您可以组合使用分区和滑动窗口。在下面的示例中,滑动窗口通常有两行宽,但每次到达新分区(即新月份)时,滑动窗口仅从该分区中的第一行开始:

SELECT
       MONTH(sales_date) AS MONTH_NUM,
       quantity,
       SUM(quantity) OVER (PARTITION BY MONTH(sales_date) 
                           ORDER BY sales_date
                           ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) 
         AS MONTHLY_SLIDING_SUM_QUANTITY
    FROM sales
    ORDER BY sales_date;
Copy

查询结果包括显示 MONTHLY_SLIDING_SUM_QUANTITY 列计算方式的其他注释:

+-----------+----------+------------------------------+
| MONTH_NUM | QUANTITY | MONTHLY_SLIDING_SUM_QUANTITY |
|-----------+----------+------------------------------+
|         1 |        1 |                           1  |  -- sum = 1
|         1 |        3 |                           4  |  -- sum = 1 + 3
|         1 |        5 |                           8  |  -- sum = 3 + 5
|         2 |        2 |                           2  |  -- sum = 0 + 2 (new month)
+-----------+----------+------------------------------+
Copy
语言: 中文