SQL 窗口函數簡介


學習重點

窗口函數可以進行排序、生成序列號等一般的聚合函數無法實現的高級操作。

理解 PARTITION BYORDER BY 這兩個關鍵字的含義十分重要。

一、什么是窗口函數

窗口函數也稱為 OLAP 函數 [1]。為了讓大家快速形成直觀印象,才起了這樣一個容易理解的名稱(“窗口”的含義我們將在隨后進行說明)。

KEYWORD

  • 窗口函數

  • OLAP 函數

OLAP 是 OnLine Analytical Processing 的簡稱,意思是對數據庫數據進行實時分析處理。例如,市場分析、創建財務報表、創建計划等日常性商務工作。

KEYWORD

  • OLAP

窗口函數就是為了實現 OLAP 而添加的標准 SQL 功能 [2]

專欄

窗口函數的支持情況

很多數據庫相關工作者過去都會有這樣的想法:“好不容易將業務數據插入到了數據庫中,如果能夠使用 SQL 對其進行實時分析的話,一定會很方便吧。”但是關系數據庫提供支持 OLAP 用途的功能僅>僅只有 10 年左右的時間。

其中的理由有很多,這里我們就不一一介紹了。大家需要注意的是,還有一部分 DBMS 並不支持這樣的新功能。

本節將要介紹的窗口函數也是其中之一,截至 2016 年 5 月,Oracle、SQL Server、DB2、PostgreSQL 的最新版本都已經支持了該功能,但是 MySQL 的最新版本 5.7 還是不支持該功能。

通過前面的學習,我們已經知道各個 DBMS 都有自己支持的特定語法和不支持的語法。標准 SQL 添加新功能的時候也會遇到同樣的問題 [3]

二、窗口函數的語法

接下來,就讓我們通過示例來學習窗口函數吧。窗口函數的語法有些復雜。

語法 1 窗口函數

<窗口函數> OVER ([PARTITION BY <列清單>]
                         ORDER BY <排序用列清單>)

[] 中的內容可以省略。

其中重要的關鍵字是 PARTITION BYORDER BY,理解這兩個關鍵字的作用是幫助我們理解窗口函數的關鍵。

2.1 能夠作為窗口函數使用的函數

在學習 PARTITION BYORDER BY 之前,我們先來列舉一下能夠作為窗口函數使用的函數。窗口函數大體可以分為以下兩種。

① 能夠作為窗口函數的聚合函數(SUMAVGCOUNTMAXMIN

RANKDENSE_RANKROW_NUMBER專用窗口函數

KEYWORD

  • 專用窗口函數

② 中的函數是標准 SQL 定義的 OLAP 專用函數,本教程將其統稱為“專用窗口函數”。從這些函數的名稱可以很容易看出其 OLAP 的用途。

其中 ① 的部分是我們在 對表進行聚合查詢 中學過的聚合函數。將聚合函數書寫在“語法 1”的“<窗口函數>”中,就能夠當作窗口函數來使用了。總之,聚合函數根據使用語法的不同,可以在聚合函數和窗口函數之間進行轉換。

三、語法的基本使用方法——使用 RANK 函數

首先讓我們通過專用窗口函數 RANK 來理解一下窗口函數的語法吧。正如其名稱所示,RANK 是用來計算記錄排序的函數。

KEYWORD

  • RANK 函數

例如,對於之前使用過的 Product 表中的 8 件商品,讓我們根據不同的商品種類(product_type),按照銷售單價(sale_price)從低到高的順序排序,結果如下所示。

執行結果

product_name | product_type | sale_price | ranking
-------------+--------------+------------+--------
 叉子        | 廚房用具      |        500 |       1
 擦菜板      | 廚房用具      |        880 |       2
 菜刀        | 廚房用具      |       3000 |       3
 高壓鍋      | 廚房用具      |       6800 |       4
 T恤衫       | 衣服          |       1000 |       1
 運動T恤     | 衣服          |       4000 |       2
 圓珠筆      | 辦公用品      |        100 |       1
 打孔器      | 辦公用品      |        500 |       2

以廚房用具為例,銷售單價最便宜的“叉子”排在第 1 位,最貴的“高壓鍋”排在第 4 位,確實按照我們的要求進行了排序。

能夠得到上述結果的 SELECT 語句請參考代碼清單 1。

代碼清單 1 根據不同的商品種類,按照銷售單價從低到高的順序創建排序表

Oracle SQL Server DB2 PostgreSQL

SELECT product_name, product_type, sale_price,
       RANK () OVER (PARTITION BY product_type
                          ORDER BY sale_price) AS ranking
  FROM Product;

PARTITION BY 能夠設定排序的對象范圍。本例中,為了按照商品種類進行排序,我們指定了 product_type

ORDER BY 能夠指定按照哪一列、何種順序進行排序。為了按照銷售單價的升序進行排列,我們指定了 sale_price。此外,窗口函數中的 ORDER BYSELECT 語句末尾的 ORDER BY 一樣,可以通過關鍵字 ASC/DESC 來指定升序和降序。省略該關鍵字時會默認按照 ASC,也就是升序進行排序。本例中就省略了上述關鍵字 [4]

KEYWORD

  • PARTITION BY 子句

  • ORDER BY 子句

通過圖 1,我們就很容易理解 PARTITION BYORDER BY 的作用了。如圖所示,PARTITION BY 在橫向上對表進行分組,而 ORDER BY 決定了縱向排序的規則。

PARTITION BY 和ORDER BY 的作用

圖 1 PARTITION BY 和ORDER BY 的作用

窗口函數兼具之前我們學過的 GROUP BY 子句的分組功能以及 ORDER BY 子句的排序功能。但是,PARTITION BY 子句並不具備 GROUP BY 子句的匯總功能。因此,使用 RANK 函數並不會減少原表中記錄的行數,結果中仍然包含 8 行數據。

法則 1

窗口函數兼具分組和排序兩種功能。

通過 PARTITION BY 分組后的記錄集合稱為窗口。此處的窗口並非“窗戶”的意思,而是代表范圍。這也是“窗口函數”名稱的由來。[5]

KEYWORD

  • 窗口

法則 2

通過 PARTITION BY 分組后的記錄集合稱為“窗口”。

此外,各個窗口在定義上絕對不會包含共通的部分。就像刀切蛋糕一樣,干凈利落。這與通過 GROUP BY 子句分割后的集合具有相同的特征。

四、無需指定 PARTITION BY

使用窗口函數時起到關鍵作用的是 PARTITION BYGROUP BY。其中,PARTITION BY 並不是必需的,即使不指定也可以正常使用窗口函數。

那么就讓我們來確認一下不指定 PARTITION BY 會得到什么樣的結果吧。這和使用沒有 GROUP BY 的聚合函數時的效果一樣,也就是將整個表作為一個大的窗口來使用。

事實勝於雄辯,下面就讓我們刪除代碼清單 1 中 SELECT 語句的 PARTITION BY 試試看吧(代碼清單 2)。

代碼清單 2 不指定 PARTITION BY

Oracle SQL Server DB2 PostgreSQL

SELECT product_name, product_type, sale_price,
       RANK () OVER (ORDER BY sale_price) AS ranking
  FROM Product;

上述 SELECT 語句的結果如下所示。

執行結果

product_name | product_type | sale_price | ranking
-------------+--------------+------------+--------
 圓珠筆      | 辦公用品     |         100 |       1
 叉子        | 廚房用具     |         500 |       2
 打孔器      | 辦公用品     |         500 |       2
 擦菜板      | 廚房用具     |         880 |       4
 T恤衫       | 衣服         |        1000 |       5
 菜刀        | 廚房用具     |        3000 |       6
 運動T恤     | 衣服         |        4000 |       7
 高壓鍋      | 廚房用具     |        6800 |       8

之前我們得到的是按照商品種類分組后的排序,而這次變成了全部商品的排序。像這樣,當希望先將表中的數據分為多個部分(窗口),再使用窗口函數時,可以使用 PARTITION BY 選項。

五、專用窗口函數的種類

從上述結果中我們可以看到,“打孔器”和“叉子”都排在第 2 位,而之后的“擦菜板”跳過了第 3 位,直接排到了第 4 位,這也是通常的排序方法,但某些情況下可能並不希望跳過某個位次來進行排序。

這時可以使用 RANK 函數之外的函數來實現。下面就讓我們來總結一下具有代表性的專用窗口函數吧。

  • RANK 函數

    計算排序時,如果存在相同位次的記錄,則會跳過之后的位次。

    例)有 3 條記錄排在第 1 位時:1 位、1 位、1 位、4 位……

  • DENSE_RANK 函數

    同樣是計算排序,即使存在相同位次的記錄,也不會跳過之后的位次。

    例)有 3 條記錄排在第 1 位時:1 位、1 位、1 位、2 位……

  • ROW_NUMBER 函數

    賦予唯一的連續位次。

    例)有 3 條記錄排在第 1 位時:1 位、2 位、3 位、4 位……

KEYWORD

  • RANK 函數

  • DENSE_RANK 函數

  • ROW_NUMBER 函數

除此之外,各 DBMS 還提供了各自特有的窗口函數。上述 3 個函數(對於支持窗口函數的 DBMS 來說)在所有的 DBMS 中都能夠使用。下面就讓我們來比較一下使用這 3 個函數所得到的結果吧(代碼清單 3)。

代碼清單 3 比較 RANKDENSE_RANKROW_NUMBER 的結果

Oracle SQL Server DB2 PostgreSQL

SELECT product_name, product_type, sale_price,
     RANK () OVER (ORDER BY sale_price) AS ranking,
     DENSE_RANK () OVER (ORDER BY sale_price) AS dense_ranking,
     ROW_NUMBER () OVER (ORDER BY sale_price) AS row_num
 FROM Product;

執行結果

執行結果

將結果中的 ranking 列和 dense_ranking 列進行比較可以發現,dense_ranking 列中有連續 2 個第 2 位,這和 ranking 列的情況相同。但是接下來的“擦菜板”的位次並不是第 4 而是第 3。這就是使用 DENSE_RANK 函數的效果了。

此外,我們可以看到,在 row_num 列中,不管銷售單價(sale_price)是否相同,每件商品都會按照銷售單價從低到高的順序得到一個連續的位次。銷售單價相同時,DBMS 會根據適當的順序對記錄進行排列。想為記錄賦予唯一的連續位次時,就可以像這樣使用 ROW_NUMBER 來實現。

使用 RANKROW_NUMBER 時無需任何參數,只需要像 RANK() 或者 ROW_NUMBER() 這樣保持括號中為空就可以了。這也是專用窗口函數通常的使用方式,請大家牢記。這一點與作為窗口函數使用的聚合函數有很大的不同,之后我們將會詳細介紹。

法則 3

由於專用窗口函數無需參數,因此通常括號中都是空的。

六、窗口函數的適用范圍

目前為止我們學過的函數大部分都沒有使用位置的限制,最多也就是在 WHERE 子句中使用聚合函數時會有些注意事項。但是,使用窗口函數的位置卻有非常大的限制。更確切地說,窗口函數只能書寫在一個特定的位置。

這個位置就是 SELECT 子句之中。反過來說,就是這類函數不能在 WHERE 子句或者 GROUP BY 子句中使用。[6]

雖然我們可以把它當作一種規則死記硬背下來,但是為什么窗口函數只能在 SELECT 子句中使用呢(也就是不能在 WHERE 子句或者 GROUP BY 子句中使用)?下面我們就來簡單說明一下其中的理由。

其理由就是,在 DBMS 內部,窗口函數是對 WHERE 子句或者 GROUP BY 子句處理后的“結果”進行的操作。大家仔細想一想就會明白,在得到用戶想要的結果之前,即使進行了排序處理,結果也是錯誤的。在得到排序結果之后,如果通過 WHERE 子句中的條件除去了某些記錄,或者使用 GROUP BY 子句進行了匯總處理,那好不容易得到的排序結果也無法使用了。[7]

正是由於這樣的原因,SELECT 子句之外“使用窗口函數是沒有意義的”,所以在語法上才會有這樣的限制。

七、作為窗口函數使用的聚合函數

前面給大家介紹了使用專用窗口函數的示例,下面我們再來看一看把之前學過的 SUM 或者 AVG 等聚合函數作為窗口函數使用的方法。

所有的聚合函數都能用作窗口函數,其語法和專用窗口函數完全相同。但大家可能對所能得到的結果還沒有一個直觀的印象,所以我們還是通過具體的示例來學習。下面我們先來看一個將 SUM 函數作為窗口函數使用的例子(代碼清單 4)。

代碼清單 4 將 SUM 函數作為窗口函數使用

Oracle SQL Server DB2 PostgreSQL

SELECT product_id, product_name, sale_price,
    SUM (sale_price) OVER (ORDER BY product_id) AS current_sum
  FROM Product;

執行結果

 product_id | product_name | sale_price | current_sum
------------+--------------+------------+------------
 0001       | T恤衫        |       1000 |        1000   ←1000
 0002       | 打孔器       |        500 |        1500   ←1000+500
 0003       | 運動T恤      |       4000 |        5500   ←1000+500+4000
 0004       | 菜刀         |       3000 |        8500   ←1000+500+4000+3000
 0005       | 高壓鍋       |       6800 |       15300              ·
 0006       | 叉子         |        500 |       15800              ·
 0007       | 擦菜板       |        880 |       16680              ·
 0008       | 圓珠筆       |        100 |       16780              ·

使用 SUM 函數時,並不像 RANK 或者 ROW_NUMBER 那樣括號中的內容為空,而是和之前我們學過的一樣,需要在括號內指定作為匯總對象的列。本例中我們計算出了銷售單價(sale_price)的合計值(current_sum)。

但是我們得到的並不僅僅是合計值,而是按照 ORDER BY 子句指定的 product_id 的升序進行排列,計算出商品編號“小於自己”的商品的銷售單價的合計值。因此,計算該合計值的邏輯就像金字塔堆積那樣,一行一行逐漸添加計算對象。在按照時間序列的順序,計算各個時間的銷售額總額等的時候,通常都會使用這種稱為累計的統計方法。

KEYWORD

  • 累計

使用其他聚合函數時的操作邏輯也和本例相同。例如,使用 AVG 來代替 SELECT 語句中的 SUM(代碼清單 5)。

代碼清單 5 將 AVG 函數作為窗口函數使用

Oracle SQL Server DB2 PostgreSQL

SELECT product_id, product_name, sale_price,
     AVG (sale_price) OVER (ORDER BY product_id) AS current_avg
  FROM Product;

執行結果

product_id | product_name | sale_price |     current_avg
-----------+--------------+------------+-----------------------
 0001      | T恤衫        |       1000 | 1000.0000000000000000 ←(1000)/1
 0002      | 打孔器       |        500 | 750.0000000000000000  ←(1000+500)/2
 0003      | 運動T恤      |       4000 | 1833.3333333333333333 ←(1000+500+4000)/3
 0004      | 菜刀         |       3000 | 2125.0000000000000000 ←(1000+500+4000+3000)/4
 0005      | 高壓鍋       |       6800 | 3060.0000000000000000 ←(1000+500+4000+3000+6800)/5
 0006      | 叉子         |        500 | 2633.3333333333333333               ·
 0007      | 擦菜板       |        880 | 2382.8571428571428571               ·
 0008      | 圓珠筆       |        100 | 2097.5000000000000000               ·

從結果中我們可以看到,current_avg 的計算方法確實是計算平均值的方法,但作為統計對象的卻只是“排在自己之上”的記錄。像這樣以“自身記錄(當前記錄)”作為基准進行統計,就是將聚合函數當作窗口函數使用時的最大特征。

KEYWORD

  • 當前記錄

八、計算移動平均

窗口函數就是將表以窗口為單位進行分割,並在其中進行排序的函數。其實其中還包含在窗口中指定更加詳細的匯總范圍的備選功能,該備選功能中的匯總范圍稱為框架

KEYWORD

  • 框架

其語法如代碼清單 6 所示,需要在 ORDER BY 子句之后使用指定范圍的關鍵字。

代碼清單 6 指定“最靠近的 3 行”作為匯總對象

Oracle SQL Server DB2 PostgreSQL

SELECT product_id, product_name, sale_price,
       AVG (sale_price) OVER (ORDER BY product_id
                               ROWS 2 PRECEDING) AS moving_avg
  FROM Product;

執行結果(在 DB2 中執行)

product_id    product_name    sale_price     moving_avg
-----------   -------------  -------------   ------------
 0001         T恤衫                 1000           1000 ←(1000)/1
 0002         打孔器                 500            750 ←(1000+500)/2
 0003         運動T恤               4000           1833 ←(1000+500+4000)/3
 0004         菜刀                  3000           2500 ←(500+4000+3000)/3
 0005         高壓鍋                6800           4600 ←(4000+3000+6800)/3
 0006         叉子                   500           3433          ·
 0007         擦菜板                 880           2726          ·
 0008         圓珠筆                 100            493          ·

8.1 指定框架(匯總范圍)

我們將上述結果與之前的結果進行比較,可以發現商品編號為“0004”的“菜刀”以下的記錄和窗口函數的計算結果並不相同。這是因為我們指定了框架,將匯總對象限定為了“最靠近的 3 行”。

這里我們使用了 ROWS(“行”)和 PRECEDING(“之前”)兩個關鍵字,將框架指定為“截止到之前 ~ 行”,因此“ROWS 2 PRECEDING”就是將框架指定為“截止到之前 2 行”,也就是將作為匯總對象的記錄限定為如下的“最靠近的 3 行”。

KEYWORD

  • ROWS 關鍵字

  • PRECEDING 關鍵字

  • 自身(當前記錄)

  • 之前 1 行的記錄

  • 之前 2 行的記錄

也就是說,由於框架是根據當前記錄來確定的,因此和固定的窗口不同,其范圍會隨着當前記錄的變化而變化。

將框架指定為截止到當前記錄之前 2 行(最靠近的 3 行)

圖 2 將框架指定為截止到當前記錄之前 2 行(最靠近的 3 行)

如果將條件中的數字變為“ROWS 5 PRECEDING”,就是“截止到之前 5 行”(最靠近的 6 行)的意思。

這樣的統計方法稱為移動平均(moving average)。由於這種方法在希望實時把握“最近狀態”時非常方便,因此常常會應用在對股市趨勢的實時跟蹤當中。

使用關鍵字 FOLLOWING(“之后”)替換 PRECEDING,就可以指定“截止到之后~ 行”作為框架了(圖 3)。

KEYWORD

  • 移動平均

  • FOLLOWING 關鍵字

將框架指定為截止到當前記錄之后 2 行(最靠近的 3 行)

圖 3 將框架指定為截止到當前記錄之后 2 行(最靠近的 3 行)

8.2 將當前記錄的前后行作為匯總對象

如果希望將當前記錄的前后行作為匯總對象時,就可以像代碼清單 7 那樣,同時使用 PRECEDING(“之前”)和 FOLLOWING(“之后”)關鍵字來實現。

代碼清單 7 將當前記錄的前后行作為匯總對象

Oracle SQL Server DB2 PostgreSQL

SELECT product_id, product_name, sale_price,
       AVG (sale_price) OVER (ORDER BY product_id
                               ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moving_avg
  FROM Product;

執行結果(在 DB2 中執行)

product_id    product_name    sale_price    moving_avg
-----------   -------------   -----------   -----------
 0001         T恤衫                 1000           750   ←(1000+500)/2
 0002         打孔器                 500          1833   ←(1000+500+4000)/3
 0003         運動T恤               4000          2500   ←(500+4000+3000)/3
 0004         菜刀                  3000          4600   ←(4000+3000+6800)/3
 0005         高壓鍋                6800          3433             ·
 0006         叉子                   500          2726             ·
 0007         擦菜板                 880           493             ·
 0008         圓珠筆                 100           490             ·

在上述代碼中,我們通過指定框架,將“1 PRECEDING”(之前 1 行)和“1 FOLLOWING”(之后 1 行)的區間作為匯總對象。具體來說,就是將如下 3 行作為匯總對象來進行計算(圖 4)。

  • 之前 1 行的記錄

  • 自身(當前記錄)

  • 之后 1 行的記錄

如果能夠熟練掌握框架功能,就可以稱為窗口函數高手了。

將框架指定為當前記錄及其前后 1 行

圖 4 將框架指定為當前記錄及其前后 1 行

九、兩個 ORDER BY

最后我們來介紹一下使用窗口函數時與結果形式相關的注意事項,那就是記錄的排列順序。因為使用窗口函數時必須要在 OVER 子句中使用 ORDER BY,所以可能有讀者乍一看會覺得結果中的記錄會按照該 ORDER BY 指定的順序進行排序。

但其實這只是一種錯覺。OVER 子句中的 ORDER BY 只是用來決定窗口函數按照什么樣的順序進行計算的,對結果的排列順序並沒有影響。因此也有可能像代碼清單 8 那樣,得到一個記錄的排列順序比較混亂的結果。有些 DBMS 也可以按照窗口函數的 ORDER BY 子句所指定的順序對結果進行排序,但那也僅僅是個例而已。

代碼清單 8 無法保證如下 SELECT 語句的結果的排列順序

Oracle SQL Server DB2 PostgreSQL

SELECT product_name, product_type, sale_price,
       RANK () OVER (ORDER BY sale_price) AS ranking
  FROM Product;

有可能會得到下面這樣的結果

 product_name | product_type | sale_price | ranking
--------------+--------------+------------+--------
 菜刀         | 廚房用具      |       3000 |       6
 打孔器       | 辦公用品      |        500 |       2
 運動T恤      | 衣服          |       4000 |       7
 T恤衫        | 衣服          |       1000 |       5
 高壓鍋       | 廚房用具      |       6800 |       8
 叉子         | 廚房用具      |        500 |       2
 擦菜板       | 廚房用具      |        880 |       4
 圓珠筆       | 辦公用品      |        100 |       1

那么,如何才能讓記錄切實按照 ranking 列的升序進行排列呢?

答案非常簡單。那就是在 SELECT 語句的最后,使用 ORDER BY 子句進行指定(代碼清單 9)。這樣就能保證 SELECT 語句的結果中記錄的排列順序了,除此之外也沒有其他辦法了。

代碼清單 9 在語句末尾使用 ORDER BY 子句對結果進行排序

Oracle SQL Server DB2 PostgreSQL

SELECT product_name, product_type, sale_price,
       RANK () OVER (ORDER BY sale_price) AS ranking
  FROM Product
 ORDER BY ranking;

也許大家會覺得在一條 SELECT 語句中使用兩次 ORDER BY 會有點別扭,但是盡管這兩個 ORDER BY 看上去是相同的,但其實它們的功能卻完全不同。

法則 5

將聚合函數作為窗口函數使用時,會以當前記錄為基准來決定匯總對象的記錄。

請參閱

(完)


  1. 在 Oracle 和 SQL Server 中稱為分析函數。 ↩︎

  2. 目前 MySQL 還不支持窗口函數。詳細信息請參考專欄“窗口函數的支持情況”。 ↩︎

  3. 隨着時間推移,標准 SQL 終將能夠在所有的 DBMS 中進行使用。 ↩︎

  4. 其所要遵循的規則與 SELECT 語句末尾的 ORDER BY 子句完全相同。 ↩︎

  5. 從詞語意思的角度考慮,可能“組”比“窗口”更合適一些,但是在 SQL 中,“組”更多的是用來特指使用 GROUP BY 分割后的記錄集合,因此,為了避免混淆,使用 PARTITION BY 時稱為窗口。 ↩︎

  6. 語法上,除了 SELECT 子句,ORDER BY 子句或者 UPDATE 語句的 SET 子句中也可以使用。但因為幾乎沒有實際的業務示例,所以開始的時候大家只要記得“只能在 SELECT 子句中使用”就可以了。 ↩︎

  7. 反之,之所以在 ORDER BY 子句中能夠使用窗口函數,是因為 ORDER BY 子句會在 SELECT 子句之后執行,並且記錄保證不會減少。 ↩︎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM