Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

SQL SELECT 入門:PARTITION BY 與窗函數教學

最後更新:2026年1月27日資料庫

前面我們學過了 GROUP BY 搭配聚合函數,可以把資料分組後做統計。

但你有沒有發現一個問題?

一旦用了 GROUP BY,資料就被「壓縮」了,你看不到每一筆原始資料的樣子。

這篇文章要教你一個大絕招:窗函數(Window Function)。

它可以讓你在做聚合統計的同時,還能保留每一筆原始資料。

GROUP BY 的限制:資料會被壓縮

我們先來回顧一下 GROUP BY 的特性。

當你用 GROUP BY 做分組統計時,資料會發生什麼事?

假設我們有一張員工銷售表,原本有 4 筆資料:

組員組別銷售額
AliceA組1200
BobA組800
CarolB組1500
DavidB組900
組別A組
銷售額1200
組別A組
銷售額800
組別B組
銷售額1500
組別B組
銷售額900

現在老闆想知道「各組的平均銷售額」,你用 GROUP BY 來做:

SELECT 組別, AVG(銷售額) AS 平均銷售額
FROM 員工銷售表
GROUP BY 組別

結果變成這樣:

組別平均銷售額
A組1000
B組1200
平均銷售額1000
平均銷售額1200

原本 4 筆資料,現在只剩 2 筆了。

每一組的資料被「合併壓縮」,變成一個聚合結果。

這就是 GROUP BY 的本質:用一筆資料來代表一組資料。

所以你看不到每一個組員的臉孔了,Alice、Bob、Carol、David 都消失了,只剩下 A組 和 B組。

問題情境:需要聚合結果,又要保留每一筆資料

某天老闆跟你說:

「我想要知道每一組當中,每個組員的銷售額跟該組平均值的差額是多少。我要看誰高於平均、誰拖後腿。」

你仔細分析一下這個需求:

  1. 要算「各組的平均銷售額」→ 需要分組 + 聚合
  2. 要看「每個組員的銷售額」→ 需要保留每一筆資料
  3. 要算「個人 vs 平均的差額」→ 需要把兩者放在同一列做運算

等等,這不是互相矛盾嗎?

你用了 GROUP BY 之後,就看不到個別組員了啊!

還記得 GROUP BY 的那個原則嗎?

當你用了 GROUP BY,SELECT 裡面只能放兩種東西:

  1. GROUP BY 後面有寫的欄位(例如:組別)
  2. 聚合函數的結果(例如:AVG(銷售額))

所以如果你想寫這樣的查詢:

SELECT 組員, 組別, 銷售額, AVG(銷售額)
FROM 員工銷售表
GROUP BY 組別

這會直接報錯!

因為「組員」和「銷售額」沒有出現在 GROUP BY 後面,也不是聚合函數,所以不能放在 SELECT 裡面。

為什麼會有這個限制?

因為 SQL 的執行順序是這樣的:

  1. FROM:先決定要從哪張表撈資料
  2. WHERE:篩選符合條件的資料
  3. GROUP BY:把資料分組壓縮
  4. 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 組的成員

就像是窗框上裝了一個濾鏡,自動把不同組的資料過濾掉。

然後窗函數會對框框裡的資料做計算(例如:算平均、加總、計數),把結果記在這筆資料的旁邊。

這就是窗函數的完整運作流程:

  1. 根據指定的欄位把資料分區(例如:按組別分區)
  2. 窗框移到每一筆資料時,只看到同一分區的資料
  3. 對窗框內的資料做聚合運算
  4. 把運算結果附加到這筆資料上

這就是為什麼叫做「窗」函數:它定義了一個會滑動的視野範圍,在每個位置對窗內資料做運算,並把結果附加到每一筆資料上。

窗函數的語法

聚合函數() 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 組別)

這行的意思是:

  1. OVER (PARTITION BY 組別):建立一個窗框,按組別分區
  2. 當窗框滑到某筆資料時,只會看到同一組的資料
  3. AVG(銷售額):對窗框裡的銷售額算平均
  4. 把平均值附加到這筆資料上

GROUP BY vs 窗函數的差別

用一個比喻來說明:

  • GROUP BY:像是一台碎紙機。你把所有同類型的紙條丟進去,攪碎後壓成一塊紙磚。原本 10 張紙條,最後只剩下 1 塊磚(一筆聚合結果)。
  • 窗函數:像是在每張紙條旁邊貼便利貼。你透過「窗」看到這張紙條和它的鄰居,算出一個結果,然後把結果寫在便利貼上貼回去。紙條本身完整保留,一張都沒少。

所以窗函數可以幫你做到一件事:

在做聚合統計的同時,保留每一筆原始資料。

它不會把資料壓縮,而是把聚合結果「附加到」每一筆資料上。

實際操作:計算各組平均銷售額

假設我們有一張員工銷售表:

組員組別銷售額
AliceA組1200
BobA組800
CarolB組1500
DavidB組900
組別A組
銷售額1200
組別A組
銷售額800
組別B組
銷售額1500
組別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,所以可以同時顯示個別明細和聚合結果。

執行結果

結果會是:

組員組別銷售額組別平均銷售額
AliceA組12001000
BobA組8001000
CarolB組15001200
DavidB組9001200
組別A組
銷售額1200
組別平均銷售額1000
組別A組
銷售額800
組別平均銷售額1000
組別B組
銷售額1500
組別平均銷售額1200
組別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 員工銷售表

結果會是:

組員組別銷售額與平均的差額
AliceA組1200+200
BobA組800-200
CarolB組1500+300
DavidB組900-300
組別A組
銷售額1200
與平均的差額+200
組別A組
銷售額800
與平均的差額-200
組別B組
銷售額1500
與平均的差額+300
組別B組
銷售額900
與平均的差額-300

一目瞭然!

Alice 和 Carol 高於平均,Bob 和 David 低於平均。

老闆要的報表就完成了。

窗函數還能做排名

PM 又跑來說:

「老闆還想看每個人在組內的排名,可以加上去嗎?」

沒問題!

窗函數除了可以做聚合統計,還可以做分區內的排序。

用 ROW_NUMBER() 做組內排名

SELECT 
    組員,
    組別,
    銷售額,
    ROW_NUMBER() OVER (PARTITION BY 組別 ORDER BY 銷售額 DESC) AS 組內排名
FROM 員工銷售表

這段語法的意思是:

  1. PARTITION BY 組別:先按組別分區
  2. ORDER BY 銷售額 DESC:在每個分區內,按銷售額從高到低排序
  3. ROW_NUMBER():給每一筆資料一個列數(排名)

結果會是:

組員組別銷售額組內排名
AliceA組12001
BobA組8002
CarolB組15001
DavidB組9002
組別A組
銷售額1200
組內排名1
組別A組
銷售額800
組內排名2
組別B組
銷售額1500
組內排名1
組別B組
銷售額900
組內排名2

注意!

這裡的排名是「組內排名」,不是整張表的排名。

Alice 是 A 組第 1 名,Carol 是 B 組第 1 名。

這就是窗函數的另一個強大功能:可以按照分區來做排序。

以前用 ORDER BY 只能對整張表排序,現在可以對每個分區分別排序了。

PARTITION BY vs GROUP BY 的比較

最後幫大家整理一下這兩個的差別:

特性GROUP BYPARTITION BY
資料壓縮會壓縮,每組只剩一筆不壓縮,保留每一筆
聚合結果直接顯示附加到每一筆資料
用途只看分組統計結果統計結果 + 原始明細都要
搭配排序只能對整張表排序可以對每個分區排序
GROUP BY會壓縮,每組只剩一筆
PARTITION BY不壓縮,保留每一筆
GROUP BY直接顯示
PARTITION BY附加到每一筆資料
GROUP BY只看分組統計結果
PARTITION BY統計結果 + 原始明細都要
GROUP BY只能對整張表排序
PARTITION BY可以對每個分區排序

簡單來說:

  • 你只需要看統計結果 → 用 GROUP BY
  • 你需要統計結果,又要保留明細 → 用窗函數 + PARTITION BY

小結

這篇文章學到的重點:

  1. GROUP BY 會壓縮資料:分組後每組只剩一筆,看不到個別資料
  2. 窗函數可以分組又不壓縮:用 PARTITION BY 分區,聚合結果會附加到每一筆資料
  3. 窗函數的語法:聚合函數() OVER (PARTITION BY 欄位)
  4. 窗函數可以做分區內排序:搭配 ORDER BY 和 ROW_NUMBER() / RANK()
  5. PARTITION BY vs GROUP BY:前者不壓縮,後者會壓縮

窗函數是 SQL 進階查詢中非常重要的技術,學會它可以解決很多「既要分組統計、又要保留明細」的需求!

留言 (2)

世界之王
世界之王2026年1月28日

感謝你的優質文章,
每天搭捷運固定看複習跟學習

徐培鈞
徐培鈞2026年1月28日

哈哈,謝謝支持🙏

發表留言

留言將在審核後顯示。

資料庫

目錄

  • GROUP BY 的限制:資料會被壓縮
  • 問題情境:需要聚合結果,又要保留每一筆資料
  • 窗函數:分組統計又不壓縮資料
  • 為什麼叫做「窗」函數?
  • 窗函數的語法
  • GROUP BY vs 窗函數的差別
  • 實際操作:計算各組平均銷售額
  • 第一步:該組的平均銷售額
  • 第二步:每個人的銷售額
  • 第三步:組合成完整的 SQL
  • 對比 GROUP BY 的寫法
  • 執行結果
  • 計算個人與平均的差額
  • 窗函數還能做排名
  • 用 ROW_NUMBER() 做組內排名
  • PARTITION BY vs GROUP BY 的比較
  • 小結