前面我們學過了 GROUP BY 搭配聚合函數,可以把資料分組後做統計。
但你有沒有發現一個問題?
一旦用了 GROUP BY,資料就被「壓縮」了,你看不到每一筆原始資料的樣子。
這篇文章要教你一個大絕招:窗函數(Window Function)。
它可以讓你在做聚合統計的同時,還能保留每一筆原始資料。
GROUP BY 的限制:資料會被壓縮
我們先來回顧一下 GROUP BY 的特性。
當你用 GROUP BY 做分組統計時,資料會發生什麼事?
假設我們有一張員工銷售表,原本有 4 筆資料:
| 組員 | 組別 | 銷售額 |
|---|---|---|
| Alice | A組 | 1200 |
| Bob | A組 | 800 |
| Carol | B組 | 1500 |
| David | B組 | 900 |
現在老闆想知道「各組的平均銷售額」,你用 GROUP BY 來做:
SELECT 組別, AVG(銷售額) AS 平均銷售額
FROM 員工銷售表
GROUP BY 組別結果變成這樣:
| 組別 | 平均銷售額 |
|---|---|
| A組 | 1000 |
| B組 | 1200 |
原本 4 筆資料,現在只剩 2 筆了。
每一組的資料被「合併壓縮」,變成一個聚合結果。
這就是 GROUP BY 的本質:用一筆資料來代表一組資料。
所以你看不到每一個組員的臉孔了,Alice、Bob、Carol、David 都消失了,只剩下 A組 和 B組。
問題情境:需要聚合結果,又要保留每一筆資料
某天老闆跟你說:
「我想要知道每一組當中,每個組員的銷售額跟該組平均值的差額是多少。我要看誰高於平均、誰拖後腿。」
你仔細分析一下這個需求:
- 要算「各組的平均銷售額」→ 需要分組 + 聚合
- 要看「每個組員的銷售額」→ 需要保留每一筆資料
- 要算「個人 vs 平均的差額」→ 需要把兩者放在同一列做運算
等等,這不是互相矛盾嗎?
你用了 GROUP BY 之後,就看不到個別組員了啊!
還記得 GROUP BY 的那個原則嗎?
當你用了 GROUP BY,SELECT 裡面只能放兩種東西:
- GROUP BY 後面有寫的欄位(例如:組別)
- 聚合函數的結果(例如:AVG(銷售額))
所以如果你想寫這樣的查詢:
SELECT 組員, 組別, 銷售額, AVG(銷售額)
FROM 員工銷售表
GROUP BY 組別這會直接報錯!
因為「組員」和「銷售額」沒有出現在 GROUP BY 後面,也不是聚合函數,所以不能放在 SELECT 裡面。
為什麼會有這個限制?
因為 SQL 的執行順序是這樣的:
- FROM:先決定要從哪張表撈資料
- WHERE:篩選符合條件的資料
- GROUP BY:把資料分組壓縮
- SELECT:選取要顯示的欄位
注意第 3 步和第 4 步的順序:GROUP BY 在 SELECT 之前執行。
當 GROUP BY 執行完畢時,資料已經被壓縮了,每一組只剩下一筆。
這時候原本的個別資料(像是組員、銷售額)已經流失,所以 SELECT 沒辦法選取這些欄位。
用傳統的 GROUP BY,這件事做不到。
窗函數:分組統計又不壓縮資料
這時候就要請出我們的大絕招:窗函數(Window Function)。
在學語法之前,我們先透過它的名稱來理解這個函數的核心概念。
為什麼叫做「窗」函數?
視野的限制
想像你站在屋子裡,透過窗戶往外看。
你的視野是被窗框限制住的,你只能看到窗框裡的那一小塊風景,而不是整個世界。
這就是「窗」的第一個概念:限制視野的範圍。
在 SQL 裡面,雖然資料庫可能有幾百萬筆資料,但透過窗函數的 OVER() 子句,你可以定義一個「窗」,讓運算只發生在「你看得到的那個框框」裡。
白話說:這扇窗幫你把「全世界的資料」過濾成「你現在關心的那一小區」。
滑動視窗
「窗」還有另一個重要的特性:它會移動。
想像你把一張很長的報表貼在牆上,然後手上拿著一個小窗框。
你把窗框壓在報表上,透過窗框只能看到框住的那幾筆資料。
接著你把窗框往下移動:
- 當窗框移到第 5 筆資料時,框框裡可能裝著第 3、4、5 筆資料
- 當窗框移到第 6 筆資料時,第 3 筆就「移出窗外」了,第 6 筆則「進入窗內」
這個框框會隨著你處理的每一筆資料,一路往下滑動。
搭配分區做聚合運算
光是「看到」窗框裡的資料還不夠,窗函數還會對窗框裡的資料做運算。
而且在實務上,我們通常會搭配「分區」來使用。
什麼是分區?
假設報表上有 A 組和 B 組的資料混在一起,當你指定「按組別分區」時,窗框裡就只會看到同一組的資料。
- 當窗框移到 A 組的某筆資料時,框框裡只會出現 A 組的成員
- 當窗框移到 B 組的某筆資料時,框框裡只會出現 B 組的成員
就像是窗框上裝了一個濾鏡,自動把不同組的資料過濾掉。
然後窗函數會對框框裡的資料做計算(例如:算平均、加總、計數),把結果記在這筆資料的旁邊。
這就是窗函數的完整運作流程:
- 根據指定的欄位把資料分區(例如:按組別分區)
- 窗框移到每一筆資料時,只看到同一分區的資料
- 對窗框內的資料做聚合運算
- 把運算結果附加到這筆資料上
這就是為什麼叫做「窗」函數:它定義了一個會滑動的視野範圍,在每個位置對窗內資料做運算,並把結果附加到每一筆資料上。
窗函數的語法
聚合函數() OVER (PARTITION BY 分區欄位)這個語法看起來有點複雜,我們拆開來看。
OVER():定義「窗」
OVER() 以及括號內的整個部分,就是在定義「窗」。
還記得前面說的嗎?窗框會限制你的視野範圍。
OVER() 就是在告訴 SQL:「我要用窗函數了,請幫我建立一個窗框。」
PARTITION BY:設定分區
PARTITION BY 分區欄位 是在設定窗框的「分區規則」。
還記得前面說的濾鏡嗎?PARTITION BY 就是那個濾鏡。
舉個例子:
OVER (PARTITION BY 部門)這行的意思是:當窗框滑到「研發部」的員工時,框框裡只會看到研發部的同事,不會跨越邊界去看到「業務部」的資料。
如果沒有寫 PARTITION BY,窗框就會看到整張表的所有資料。
聚合函數():對窗框內的資料做運算
最前面的 聚合函數() 就是你要對窗框裡的資料做什麼運算。
例如:AVG()、SUM()、COUNT()、MAX()、MIN() 等等。
當窗框滑到每一筆資料時,就會對框框裡看到的資料做一次計算,然後把結果附加到這筆資料上。
完整範例
把這三個部分組合起來:
AVG(銷售額) OVER (PARTITION BY 組別)這行的意思是:
- OVER (PARTITION BY 組別):建立一個窗框,按組別分區
- 當窗框滑到某筆資料時,只會看到同一組的資料
- AVG(銷售額):對窗框裡的銷售額算平均
- 把平均值附加到這筆資料上
GROUP BY vs 窗函數的差別
用一個比喻來說明:
- GROUP BY:像是一台碎紙機。你把所有同類型的紙條丟進去,攪碎後壓成一塊紙磚。原本 10 張紙條,最後只剩下 1 塊磚(一筆聚合結果)。
- 窗函數:像是在每張紙條旁邊貼便利貼。你透過「窗」看到這張紙條和它的鄰居,算出一個結果,然後把結果寫在便利貼上貼回去。紙條本身完整保留,一張都沒少。
所以窗函數可以幫你做到一件事:
在做聚合統計的同時,保留每一筆原始資料。
它不會把資料壓縮,而是把聚合結果「附加到」每一筆資料上。
實際操作:計算各組平均銷售額
假設我們有一張員工銷售表:
| 組員 | 組別 | 銷售額 |
|---|---|---|
| Alice | A組 | 1200 |
| Bob | A組 | 800 |
| Carol | B組 | 1500 |
| David | B組 | 900 |
老闆要的是:每個人的銷售額,以及該組的平均銷售額。
我們來拆解這個需求,一步步寫出對應的語法。
第一步:該組的平均銷售額
先看「該組的平均銷售額」這句話。
- 「平均銷售額」→ 要用
AVG(銷售額) - 「該組的」→ 要按組別分區 →
OVER (PARTITION BY 組別)
組合起來:
AVG(銷售額) OVER (PARTITION BY 組別)再給它一個別名,方便辨識:
AVG(銷售額) OVER (PARTITION BY 組別) AS 組別平均銷售額第二步:每個人的銷售額
再看「每個人的銷售額」這句話。
這代表我們要保留每一筆資料的明細,所以要選取:
- 組員
- 組別
- 銷售額
第三步:組合成完整的 SQL
窗函數要放在哪裡?
放在 SELECT 裡面,當作一個新的欄位。
把前兩步組合起來:
SELECT
組員,
組別,
銷售額,
AVG(銷售額) OVER (PARTITION BY 組別) AS 組別平均銷售額
FROM 員工銷售表對比 GROUP BY 的寫法
你會發現,這裡沒有 GROUP BY!
如果用傳統的 GROUP BY 寫法,你會這樣寫:
SELECT 組別, AVG(銷售額) AS 平均銷售額
FROM 員工銷售表
GROUP BY 組別但這樣寫的話,你就不能在 SELECT 裡面放「組員」和「銷售額」了,因為它們沒有出現在 GROUP BY 後面。
前面說過,GROUP BY 在 SELECT 之前執行,當 GROUP BY 執行完畢時,個別資料已經流失了。
窗函數的好處就在這裡:你不需要 GROUP BY,所以可以同時顯示個別明細和聚合結果。
執行結果
結果會是:
| 組員 | 組別 | 銷售額 | 組別平均銷售額 |
|---|---|---|---|
| Alice | A組 | 1200 | 1000 |
| Bob | A組 | 800 | 1000 |
| Carol | B組 | 1500 | 1200 |
| David | B組 | 900 | 1200 |
你看到了嗎?
每一筆資料都還在,沒有被壓縮!
而且每一筆資料後面都多了一個欄位,顯示該組的平均銷售額。
A 組的平均是 (1200 + 800) / 2 = 1000,所以 Alice 和 Bob 那兩列的「組別平均銷售額」都是 1000。
B 組的平均是 (1500 + 900) / 2 = 1200,所以 Carol 和 David 那兩列的「組別平均銷售額」都是 1200。
這就是窗函數的威力:把分區的聚合結果,附加到每一筆資料上。
計算個人與平均的差額
既然聚合結果可以附加到每一筆資料,那我們就可以直接拿它來做運算。
還記得嗎?SELECT 不只能選取欄位,還可以做各種運算。
例如:
SELECT 10 * 5結果是 50,資料庫會先計算再輸出。
同樣的道理,窗函數的結果也可以拿來做運算。
老闆要的「個人銷售額 vs 組別平均的差額」,可以這樣寫:
SELECT
組員,
組別,
銷售額,
銷售額 - AVG(銷售額) OVER (PARTITION BY 組別) AS 與平均的差額
FROM 員工銷售表結果會是:
| 組員 | 組別 | 銷售額 | 與平均的差額 |
|---|---|---|---|
| Alice | A組 | 1200 | +200 |
| Bob | A組 | 800 | -200 |
| Carol | B組 | 1500 | +300 |
| David | B組 | 900 | -300 |
一目瞭然!
Alice 和 Carol 高於平均,Bob 和 David 低於平均。
老闆要的報表就完成了。
窗函數還能做排名
PM 又跑來說:
「老闆還想看每個人在組內的排名,可以加上去嗎?」
沒問題!
窗函數除了可以做聚合統計,還可以做分區內的排序。
用 ROW_NUMBER() 做組內排名
SELECT
組員,
組別,
銷售額,
ROW_NUMBER() OVER (PARTITION BY 組別 ORDER BY 銷售額 DESC) AS 組內排名
FROM 員工銷售表這段語法的意思是:
- PARTITION BY 組別:先按組別分區
- ORDER BY 銷售額 DESC:在每個分區內,按銷售額從高到低排序
- ROW_NUMBER():給每一筆資料一個列數(排名)
結果會是:
| 組員 | 組別 | 銷售額 | 組內排名 |
|---|---|---|---|
| Alice | A組 | 1200 | 1 |
| Bob | A組 | 800 | 2 |
| Carol | B組 | 1500 | 1 |
| David | B組 | 900 | 2 |
注意!
這裡的排名是「組內排名」,不是整張表的排名。
Alice 是 A 組第 1 名,Carol 是 B 組第 1 名。
這就是窗函數的另一個強大功能:可以按照分區來做排序。
以前用 ORDER BY 只能對整張表排序,現在可以對每個分區分別排序了。
PARTITION BY vs GROUP BY 的比較
最後幫大家整理一下這兩個的差別:
| 特性 | GROUP BY | PARTITION BY |
|---|---|---|
| 資料壓縮 | 會壓縮,每組只剩一筆 | 不壓縮,保留每一筆 |
| 聚合結果 | 直接顯示 | 附加到每一筆資料 |
| 用途 | 只看分組統計結果 | 統計結果 + 原始明細都要 |
| 搭配排序 | 只能對整張表排序 | 可以對每個分區排序 |
簡單來說:
- 你只需要看統計結果 → 用 GROUP BY
- 你需要統計結果,又要保留明細 → 用窗函數 + PARTITION BY
小結
這篇文章學到的重點:
- GROUP BY 會壓縮資料:分組後每組只剩一筆,看不到個別資料
- 窗函數可以分組又不壓縮:用 PARTITION BY 分區,聚合結果會附加到每一筆資料
- 窗函數的語法:
聚合函數() OVER (PARTITION BY 欄位) - 窗函數可以做分區內排序:搭配 ORDER BY 和 ROW_NUMBER() / RANK()
- PARTITION BY vs GROUP BY:前者不壓縮,後者會壓縮
窗函數是 SQL 進階查詢中非常重要的技術,學會它可以解決很多「既要分組統計、又要保留明細」的需求!