前面學的 SELECT,都是針對「單筆資料」做運算。
比如說,計算每個商品的含稅價:
SELECT 名稱, 單價 * 1.05 AS 含稅價
FROM 商品計算蘋果的含稅價時,只需要看蘋果自己的單價,不用管香蕉或牛奶。
但有些問題不是這樣。
比如說:「會員的平均收入是多少?」
這個問題沒辦法只看一筆資料就回答,必須把所有會員的收入加起來,再除以人數。
什麼是聚合運算?
這種「把很多筆資料放在一起,用一個值來代表」的運算,叫做聚合運算(Aggregate)。
為什麼叫「聚合」?
因為你把一堆資料「聚」在一起,「合」成一個結果。
舉個例子:台灣的平均月收入是 5 萬元。
這個「5 萬元」是怎麼來的?
是把全台灣所有人的月收入加起來,除以人數,得到一個數字。
這個數字「代表」了全台灣人的收入狀況。
當然,你可能會覺得「5 萬元不能代表我」,因為大多數人低於平均,是少數高薪的人拉高了平均。
這時候你可能會說:「應該用中位數比較準。」
但不管是平均數還是中位數,本質都一樣:把一堆資料放在一起,用一個值來代表這一群資料。
聚合運算的特性
這是很重要的觀念。
聚合運算的本質是:輸入很多筆資料,輸出一筆。
| 運算前 | 運算後 |
|---|---|
| 100 筆會員資料 | 1 個平均收入 |
| 1000 筆訂單 | 1 個總金額 |
| 50 個學生成績 | 1 個最高分 |
不管原本有幾筆資料,聚合運算後都只剩下一筆。
記住這個特性,等一下會用到。
常見的聚合函數
在 SQL 中,聚合運算是透過「聚合函數」來實現的。
| 函數 | 用途 |
|---|---|
COUNT() | 計算有幾筆資料 |
SUM() | 把所有數值加起來 |
AVG() | 計算平均值 |
MAX() | 找出最大值 |
MIN() | 找出最小值 |
這些函數的共同點:輸入一整欄的資料,輸出一個數字。
找出收入高於平均的會員
老闆說:「請你找出收入高於平均的會員。」
聽起來很簡單,你可能會這樣寫:
SELECT *
FROM 會員
WHERE 收入 > AVG(收入)WHERE 一次只能看一筆資料
看起來很合理,但這樣會報錯。
為什麼?
還記得前面學的嗎?WHERE 是一筆一筆檢查的。
假設會員表單有 100 筆資料,WHERE 會這樣做:
- 拿出第 1 筆(小明),檢查「小明的收入是否高於平均」→ 符合就留下,不符合就過濾
- 拿出第 2 筆(小華),檢查「小華的收入是否高於平均」→ 符合就留下,不符合就過濾
- …一直到第 100 筆
問題來了:當資料庫在檢查小明的時候,它只看小明這一筆資料。
假設小明的收入是 60000,資料庫會問:「60000 是否高於平均?」
但「平均收入」是多少?它不知道。
因為平均收入要這樣算:
平均收入 = (小明的收入 + 小華的收入 + 小美的收入 + ... + 第 100 個人的收入) ÷ 100也就是說,你必須先看完全部 100 筆資料,才能算出平均收入。
但 WHERE 是一筆一筆檢查的。
檢查小明的時候,才剛開始,還沒看到小華、小美、還有後面 97 個人的資料。
既然還沒看完全部資料,就沒辦法算出平均收入。
既然不知道平均收入是多少,就沒辦法判斷「小明的收入是否高於平均」。
這就矛盾了:
- 聚合運算(AVG)需要「看完全部 100 筆」才能算出結果
- WHERE 一次只能「看 1 筆」
所以聚合函數不能直接放在 WHERE 裡面。
聚合函數會把資料「壓縮」成一筆
剛剛說過,聚合運算的特性是:輸入很多筆,輸出一筆。
換句話說,聚合函數會把資料「壓縮」。
100 筆會員資料,經過 AVG(收入) 之後,變成 1 個數字。
這會造成一個問題:壓縮後的結果,沒辦法和原本的資料放在一起。
你可能會想:既然 AVG 不能放在 WHERE,那我放在 SELECT 裡面,先算出平均收入,再拿來比較。
SELECT *, AVG(收入) AS 平均收入
FROM 會員
WHERE 收入 > 平均收入這樣還是會報錯。
為什麼?
WHERE 比 SELECT 先執行。
還記得前面學過的執行順序嗎?
| 順序 | 指令 |
|---|---|
| 1 | FROM |
| 2 | WHERE |
| 3 | SELECT |
WHERE 在 SELECT 之前執行。
當 WHERE 在執行 收入 > 平均收入 的時候,SELECT 還沒執行。
SELECT 還沒執行,就代表 AVG(收入) AS 平均收入 這個計算還沒做。
計算還沒做,「平均收入」這個別名就還不存在。
所以 WHERE 會說:「平均收入是什麼?我不認識。」
壓縮後的筆數對不上。
SELECT * 是要輸出每一筆資料,100 筆就是 100 筆。
AVG(收入) 是把 100 筆壓縮成 1 筆。
假設你寫了 SELECT *, AVG(收入) AS 平均收入,資料庫會很困惑:
| id | 姓名 | 收入 | 平均收入 |
|---|---|---|---|
| 1 | 小明 | 60000 | ??? |
| 2 | 小華 | 40000 | ??? |
| 3 | 小美 | 50000 | ??? |
| … | … | … | ??? |
| 100 | 小王 | 45000 | ??? |
「平均收入」這一欄應該填什麼?
AVG(收入) 的結果只有一個數字(假設是 50000),但表格有 100 列。
你可能會想:「資料庫應該夠聰明吧?就把 50000 填進每一格就好了啊。」
但資料庫不會這樣做。
為什麼?
因為 SELECT * 和 AVG(收入) 是兩種完全不同的操作:
SELECT *:把資料「一筆一筆列出來」AVG(收入):把資料「全部壓縮成一筆」
這兩種操作的「方向」是相反的,一個要展開,一個要壓縮。
資料庫不會自作主張幫你決定「把 1 筆複製成 100 筆」,因為那樣做可能不是你要的結果。
所以資料庫選擇直接報錯,讓你明確告訴它:你到底要展開,還是要壓縮?
子查詢產生不了表單
你可能會想:那我用子查詢,先把平均收入算出來當成一個欄位,再篩選。
SELECT *
FROM (
SELECT *, AVG(收入) AS 平均收入
FROM 會員
) AS 子表單
WHERE 收入 > 平均收入這樣還是會報錯。
為什麼?
還記得前面學過的嗎?SELECT 的本質是「輸出一個表單」,這個輸出的表單可以再當作另一個查詢的輸入。
子查詢就是利用這個原理:先用內層的 SELECT 產生一個表單,再讓外層的 SELECT 使用這個表單。
但問題是:內層的 SELECT *, AVG(收入) 本身就會報錯。
它根本產生不了一個正常的表單。
既然產生不了表單,外層的查詢自然也無法執行。
就像你想用一個壞掉的零件去組裝機器,不管你怎麼組,機器都不會動。
用子查詢回傳一個數字
換個思路。
平均收入是一個數字,假設是 50000。
那「收入高於平均」就等於「收入高於 50000」:
SELECT *
FROM 會員
WHERE 收入 > 50000這樣寫當然可以,WHERE 在一筆一筆檢查的時候,比較的對象是一個固定的數字。
問題是:我們不知道平均收入是多少,要怎麼動態算出來?
答案是:用子查詢算出平均收入,它的結果就是一個數字。
SELECT *
FROM 會員
WHERE 收入 > (SELECT AVG(收入) FROM 會員)為什麼這樣可以?
前面不是說「SELECT *, AVG(收入) 會報錯」嗎?這裡怎麼又可以了?
關鍵差異在於:
| 寫法 | SELECT 輸出的筆數 | 結果 |
|---|---|---|
SELECT *, AVG(收入) | 100 筆 + 1 筆 | 矛盾,報錯 |
SELECT AVG(收入) | 只有 1 筆 | 沒有矛盾,成功 |
SELECT *, AVG(收入) 會報錯,是因為 SELECT * 要輸出 100 筆,AVG(收入) 只有 1 筆,數量對不上。
但 SELECT AVG(收入) 不一樣。
它只做一件事:把所有收入壓縮成一個平均值。
沒有 SELECT *,就沒有「100 筆 vs 1 筆」的矛盾。
所以 (SELECT AVG(收入) FROM 會員) 可以順利執行,產生一個數字,假設是 50000。
那整個查詢就等於:
SELECT *
FROM 會員
WHERE 收入 > 50000WHERE 在一筆一筆檢查的時候,比較的對象是一個已經算好的數字,不是一個「還要去算」的聚合函數。
這樣就不會報錯了!
小結
這篇文章介紹了聚合運算:
- 聚合運算:把很多筆資料放在一起,用一個值來代表
- 多筆變一筆:這是聚合運算最重要的特性
- 不能放在 WHERE:因為 WHERE 是一筆一筆檢查的
- 不能和一般欄位混用:一筆一筆和聚合互相矛盾
- 用子查詢繞過:讓子查詢回傳一個數字,就可以放在 WHERE 裡比較
之後還會學到用 GROUP BY 來做分組聚合,把「多筆變一筆」變成「多筆變多組,每組一筆」。