隨著表中記錄(數據行)的不斷積累,存儲數據逐漸增加,有時我們可能希望計算出這些數據的合計值或者平均值等。 本文介紹如何使用 SQL 語句對錶進行聚合和分組的方法。此外,還介紹在彙總操作時指定條件,以及對彙總結果進行升序、降序的排序方法。 一、對錶進行聚合查詢 本節重點 使用聚合函數對錶中的列進行計算 ...
目錄
隨著表中記錄(數據行)的不斷積累,存儲數據逐漸增加,有時我們可能希望計算出這些數據的合計值或者平均值等。
本文介紹如何使用 SQL 語句對錶進行聚合和分組的方法。此外,還介紹在彙總操作時指定條件,以及對彙總結果進行升序、降序的排序方法。
一、對錶進行聚合查詢
本節重點
使用聚合函數對錶中的列進行計算合計值或者平均值等的彙總操作。
通常,聚合函數會對
NULL
以外的對象進行彙總。但是只有COUNT
函數例外,使用COUNT(*)
可以查出包含NULL
在內的全部數據的行數。使用
DISTINCT
關鍵字刪除重覆值。
1.1 聚合函數
通過 SQL 對數據進行某種操作或計算時需要使用函數。例如,計算表中全部數據的行數時,可以使用 COUNT
函數。該函數就是使用 COUNT(計數)來命名的。
除此之外,SQL 中還有很多其他用於彙總的函數,請大家先記住以下 5 個常用的函數。
-
COUNT
:計算表中的記錄數(行數) -
SUM
:計算表中數值列中數據的合計值 -
AVG
:計算表中數值列中數據的平均值 -
MAX
:求出表中任意列中數據的最大值 -
MIN
:求出表中任意列中數據的最小值
如上所示,用於彙總的函數稱為聚合函數或者聚集函數,本文中統稱為聚合函數。所謂聚合,就是將多行彙總為一行。實際上,所有的聚合函數都是這樣,輸入多行輸出一行。
接下來,本文將繼續使用在 SQL 如何對錶進行創建、更新和刪除操作 中創建的 Product
表(圖 1)來學習聚合函數的使用方法。
1.2. 計算表中數據的行數
首先,我們以 COUNT
函數為例讓大家對函數形成一個初步印象。函數這個詞,與我們在學校數學課上學到的意思是一樣的,就像是輸入某個值就能輸出相應結果的盒子一樣 [1]。
使用 COUNT
函數時,輸入表的列,就能夠輸出數據行數。如圖 2 所示,將表中的列放入名稱為 COUNT
的盒子中,咔嗒咔嗒地進行計算,咕咚一下行數就出來了……就像自動售貨機那樣,很容易理解吧。
接下來讓我們看一下 SQL 中的具體書寫方法。COUNT
函數的語法本身非常簡單,像代碼清單 1 那樣寫在 SELECT
子句中就可以得到表中全部數據的行數了。
代碼清單 1 計算全部數據的行數
執行結果:
COUNT()
中的星號,我們在 SQL SELECT WHERE 語句如何指定一個或多個查詢條件 中已經介紹過,代表全部列的意思。COUNT
函數的輸入值就記述在其後的括弧中。
此處的輸入值稱為參數或者 parameter,輸出值稱為返回值。這些稱謂不僅本文中會使用,在多數編程語言中使用函數時都會頻繁出現,請大家牢記。
1.3 計算 NULL 之外的數據的行數
想要計算表中全部數據的行數時,可以像 SELECT COUNT(*)
這樣使用星號。
如果想得到 purchase_price
列(進貨單價)中非空行數的話,可以像代碼清單 2 那樣,通過將對象列設定為參數來實現。
代碼清單 2 計算 NULL 之外的數據行數
SELECT COUNT(purchase_price)
FROM Product;
執行結果:
count
-------
6
此時,如圖 1 所示,purchase_price
列中有兩行數據是 NULL
,因此並不應該計算這兩行。對於 COUNT
函數來說,參數列不同計算的結果也會發生變化,這一點請大家特別註意。
為了有助於大家理解,請看如下這個只包含 NULL
的表的極端例子。
我們來看一下針對上述表,將星號(*
)和列名作為參數傳遞給 COUNT
函數時所得到的結果(代碼清單 3)。
代碼清單 3 將包含 NULL
的列作為參數時,COUNT(*)
和 COUNT(<列名>)
的結果並不相同
SELECT COUNT(*), COUNT(col_1)
FROM NullTbl;
執行結果:
如上所示,即使對同一個表使用 COUNT
函數,輸入的參數不同得到的結果也會不同。由於將列名作為參數時會得到 NULL
之外的數據行數,所以得到的結果是 0 行。
該特性是 COUNT
函數所特有的,其他函數並不能將星號作為參數(如果使用星號會出錯)。
法則 1
COUNT
函數的結果根據參數的不同而不同。COUNT(*)
會得到包含NULL
的數據行數,而COUNT(<列名>)
會得到NULL
之外的數據行數。
1.4 計算合計值
接下來我們學習其他 4 個聚合函數的使用方法。這些函數的語法基本上與 COUNT
函數相同,但就像我們此前所說的那樣,在這些函數中不能使用星號作為參數。
首先,我們使用計算合計值的 SUM
函數,求出銷售單價的合計值(代碼清單 4)。
代碼清單 4 計算銷售單價的合計值
SELECT SUM(sale_price)
FROM Product;
執行結果:
sum
------
16780
得到的結果 16780
元,是所有銷售單價(sale_price
列)的合計,與下述計算式的結果相同。
接下來,我們將銷售單價和進貨單價(purchase_price
列)的合計值一起計算出來(代碼清單 5)。
代碼清單 5 計算銷售單價和進貨單價的合計值
SELECT SUM(sale_price), SUM(purchase_price)
FROM Product;
執行結果:
這次我們通過 SUM(purchase_price)
將進貨單價的合計值也一起計算出來了,但有一點需要大家註意。具體的計算過程如下所示。
大家都已經註意到了吧,與銷售單價不同,進貨單價中有兩條不明數據 NULL
。對於 SUM
函數來說,即使包含 NULL
,也可以計算出合計值。
還記得 SQL SELECT WHERE 語句如何指定一個或多個查詢條件 內容的讀者可能會產生如下疑問。
“四則運算中如果存在 NULL
,結果一定是 NULL
,那此時進貨單價的合計值會不會也是 NULL
呢?”
有這樣疑問的讀者思維很敏銳,但實際上這兩者並不矛盾。從結果上說,所有的聚合函數,如果以列名為參數,那麼在計算之前就已經把 NULL
排除在外了。
因此,無論有多少個 NULL
都會被無視。這與“等價為 0”並不相同 [2]。
因此,上述進貨單價的計算表達式,實際上應該如下所示。
法則 2
聚合函數會將
NULL
排除在外。但COUNT(*)
例外,並不會排除NULL
。
1.5 計算平均值
接下來,我們練習一下計算多行數據的平均值。為此,我們需要使用 AVG
函數,其語法和 SUM
函數完全相同(代碼清單 6)。
代碼清單 6 計算銷售單價的平均值
SELECT AVG(sale_price)
FROM Product;
執行結果:
avg
----------------------
2097.5000000000000000
平均值的計算式如下所示。
(值的合計)/(值的個數)
就是平均值的計算公式了。下麵我們也像使用 SUM
函數那樣,計算一下包含 NULL
的進貨單價的平均值(代碼清單 7)。
代碼清單 7 計算銷售單價和進貨單價的平均值
SELECT AVG(sale_price), AVG(purchase_price)
FROM Product;
執行結果:
計算進貨單價平均值的情況與 SUM
函數相同,會事先刪除 NULL
再進行計算,因此計算式如下所示。
需要註意的是分母是 6 而不是 8,減少的兩個也就是那兩條 NULL
的數據。
但是有時也想將 NULL
作為 0
進行計算,具體的實現方式請參考 SQL 常用的函數。
1.6 計算最大值和最小值
想要計算出多條記錄中的最大值或最小值,可以分別使用 MAX
和 MIN
函數,它們是英語 maximam(最大值)和 minimum(最小值)的縮寫,很容易記住。
這兩個函數的語法與 SUM
的語法相同,使用時需要將列作為參數(代碼清單 8)。
代碼清單 8 計算銷售單價的最大值和進貨單價的最小值
SELECT MAX(sale_price), MIN(purchase_price)
FROM Product;
執行結果:
如圖 1 所示,我們取得了相應的最大值和最小值。
但是,MAX/MIN
函數和 SUM/AVG
函數有一點不同,那就是 SUM/AVG
函數只能對數值類型的列使用,而 MAX/MIN
函數原則上可以適用於任何數據類型的列。
例如,對圖 1 中日期類型的列 regist_date
使用 MAX/MIN
函數進行計算的結果如下所示(代碼清單 9)。
代碼清單 9 計算登記日期的最大值和最小值
SELECT MAX(regist_date), MIN(regist_date)
FROM Product;
執行結果:
剛剛我們說過 MAX/MIN
函數適用於任何數據類型的列,也就是說,只要是能夠排序的數據,就肯定有最大值和最小值,也就能夠使用這兩個函數。
對日期來說,平均值和合計值並沒有什麼實際意義,因此不能使用 SUM/AVG
函數。
這點對於字元串類型的數據也適用,字元串類型的數據能夠使用 MAX/MIN
函數,但不能使用 SUM/AVG
函數。
法則 3
MAX/MIN
函數幾乎適用於所有數據類型的列。SUM/AVG
函數只適用於數值類型的列。
1.7 使用聚合函數刪除重覆值(關鍵字 DISTINCT)
接下來我們考慮一下下麵這種情況。
在圖 1 中我們可以看到,商品種類(product_type
列)和銷售單價(sale_price
列)的數據中,存在多行數據相同的情況。
例如,拿商品種類來說,表中總共有 3 種商品共 8 行數據,其中衣服 2 行,辦公用品 2 行,廚房用具 4 行。
如果想要計算出商品種類的個數,怎麼做比較好呢?刪除重覆數據然後再計算數據行數似乎是個不錯的辦法。
實際上,在使用 COUNT
函數時,將 SQL 如何對錶進行創建、更新和刪除操作 中介紹過的 DISTINCT
關鍵字作為參數,就能得到我們想要的結果了(代碼清單 10)。
代碼清單 10 計算去除重覆數據後的數據行數
SELECT COUNT(DISTINCT product_type)
FROM Product;
執行結果:
count
-------
3
請註意,這時 DISTINCT
必須寫在括弧中。這是因為必須要在計算行數之前刪除 product_type
列中的重覆數據。
如果像代碼清單 11 那樣寫在括弧外的話,就會先計算出數據行數,然後再刪除重覆數據,結果就得到了 product_type
列的所有行數(也就是 8)。
代碼清單 11 先計算數據行數再刪除重覆數據的結果
SELECT DISTINCT COUNT(product_type)
FROM Product;
執行結果:
count
-------
8
法則 4
想要計算值的種類時,可以在
COUNT
函數的參數中使用DISTINCT
。
不僅限於 COUNT
函數,所有的聚合函數都可以使用 DISTINCT
。
下麵我們來看一下使用 DISTINCT
和不使用 DISTINCT
時 SUM
函數的執行結果(代碼清單 12)。
代碼清單 12 使不使用 DISTINCT 時的動作差異(SUM 函數)
SELECT SUM(sale_price), SUM(DISTINCT sale_price)
FROM Product;
執行結果:
左側是未使用 DISTINCT
時的合計值,和我們之前計算的結果相同,都是 16780
元。
右側是使用 DISTINCT
後的合計值,比之前的結果少了 500
元。
這是因為表中銷售單價為 500
元的商品有兩種——“打孔器”和“叉子”,在刪除重覆數據之後,計算對象就只剩下一條記錄了。
法則 5
在聚合函數的參數中使用
DISTINCT
,可以刪除重覆數據。
二、對錶進行分組
本節重點
使用
GROUP BY
子句可以像切蛋糕那樣將表分割。通過使用聚合函數和GROUP BY
子句,可以根據“商品種類”或者“登記日期”等將表分割後再進行彙總。聚合鍵中包含
NULL
時,在結果中會以“不確定”行(空行)的形式表現出來。使用聚合函數和
GROUP BY
子句時需要註意以下 4 點。
只能寫在
SELECT
子句之中
GROUP BY
子句中不能使用SELECT
子句中列的別名
GROUP BY
子句的聚合結果是無序的
WHERE
子句中不能使用聚合函數
2.1 GROUP BY 子句
目前為止,我們看到的聚合函數的使用方法,無論是否包含 NULL
,無論是否刪除重覆數據,都是針對錶中的所有數據進行的彙總處理。
下麵,我們先把表分成幾組,然後再進行彙總處理。也就是按照“商品種類”“登記日期”等進行彙總。
這裡我們將要第一次接觸到 GROUP BY
子句,其語法結構如下所示。
語法 1 使用 GROUP BY 子句進行彙總
SELECT <列名1>, <列名2>, <列名3>, ……
FROM <表名>
GROUP BY <列名1>, <列名2>, <列名3>, ……;
下麵我們就按照商品種類來統計一下數據行數(= 商品數量)(代碼清單 13)。
代碼清單 13 按照商品種類統計數據行數
SELECT product_type, COUNT(*)
FROM Product
GROUP BY product_type;
執行結果:
product_type | count
--------------+------
衣服 | 2
辦公用品 | 2
廚房用具 | 4
如上所示,未使用 GROUP BY
子句時,結果只有 1 行,而這次的結果卻是多行。這是因為不使用 GROUP BY
子句時,是將表中的所有數據作為一組來對待的。
而使用 GROUP BY
子句時,會將表中的數據分為多個組進行處理。如圖 4 所示,GROUP BY
子句對錶進行了切分。
這樣,GROUP BY
子句就像切蛋糕那樣將表進行了分組。在 GROUP BY
子句中指定的列稱為聚合鍵或者分組列。由於能夠決定表的切分方式,所以是非常重要的列。
當然,GROUP BY
子句也和 SELECT
子句一樣,可以通過逗號分隔指定多列。
如果用畫線的方式來切分表中數據的話,就會得到圖 5 那樣以商品種類為界線的三組數據。然後再計算每種商品的數據行數,就能得到相應的結果了。
法則 6
GROUP BY
就像是切分表的一把刀。
此外,GROUP BY
子句的書寫位置也有嚴格要求,一定要寫在 FROM
語句之後(如果有 WHERE
子句的話需要寫在 WHERE
子句之後)。
如果無視子句的書寫順序,SQL 就一定會無法正常執行而出錯。目前 SQL 的子句還沒有全部登場,已經出現的各子句的暫定順序如下所示。
子句的書寫順序(暫定)
1. SELECT → 2. FROM → 3. WHERE → 4. GROUP BY
法則 7
SQL 子句的順序不能改變,也不能互相替換。
2.2 聚合鍵中包含 NULL 的情況
接下來我們將進貨單價(purchase_price
)作為聚合鍵對錶進行切分。在 GROUP BY
子句中指定進貨單價的結果請參見代碼清單 14。
代碼清單 14 按照進貨單價統計數據行數
SELECT purchase_price, COUNT(*)
FROM Product
GROUP BY purchase_price;
上述 SELECT
語句的結果如下所示:
像 790
元或者 500
元這樣進貨單價很清楚的數據行不會有什麼問題,結果與之前的情況相同。
問題是結果中的第一行,也就是進貨單價為 NULL
的組。
從結果我們可以看出,當聚合鍵中包含 NULL
時,也會將 NULL
作為一組特定的數據,如圖 6 所示。
這裡的 NULL
,大家可以理解為“不確定”。
法則 8
聚合鍵中包含
NULL
時,在結果中會以“不確定”行(空行)的形式表現出來。
2.3 使用 WHERE 子句時 GROUP BY 的執行結果
在使用了 GROUP BY
子句的 SELECT
語句中,也可以正常使用 WHERE
子句。子句的排列順序如前所述,語法結果如下所示。
語法 2 使用 WHERE 子句和 GROUP BY 子句進行彙總處理
SELECT <列名1>, <列名2>, <列名3>, ……
FROM <表名>
WHERE
GROUP BY <列名1>, <列名2>, <列名3>, ……;
像這樣使用 WHERE
子句進行彙總處理時,會先根據 WHERE
子句指定的條件進行過濾,然後再進行彙總處理。請看代碼清單 15。
代碼清單 15 同時使用 WHERE 子句和 GROUP BY 子句
SELECT purchase_price, COUNT(*)
FROM Product
WHERE product_type = '衣服'
GROUP BY purchase_price;
因為上述 SELECT
語句首先使用了 WHERE
子句對記錄進行過濾,所以實際上作為聚合對象的記錄只有 2 行,如表 1 所示。
表 1 WHERE 子句過濾的結果
product_type(商品種類) | product_name(商品名稱) | product_id(商品編號) | sale_price(銷售單價) | purchase_price(進貨單價) | regist_date(登記日期) |
---|---|---|---|---|---|
衣服 | T 恤衫 | 0001 | 1000 | 500 | 2009-09-20 |
衣服 | 運動 T 恤 | 0003 | 4000 | 2800 |
使用進貨單價對這 2 條記錄進行分組,就得到瞭如下的執行結果:
purchase_price | count
----------------+------
500 | 1
2800 | 1
GROUP BY
和 WHERE
並用時,SELECT
語句的執行順序如下所示。
GROUP BY 和 WHERE 並用時 SELECT 語句的執行順序
FROM → WHERE → GROUP BY → SELECT
這與之前語法 2 中的說明順序有些不同,這是由於在 SQL 語句中,書寫順序和 DBMS 內部的執行順序並不相同。這也是 SQL 難以理解的原因之一。
2.4 與聚合函數和 GROUP BY 子句有關的常見錯誤
截至目前,我們已經介紹了聚合函數和 GROUP BY
子句的基本使用方法。雖然由於使用方便而經常被使用,但是書寫 SQL 時卻很容易出錯,希望大家特別小心。
-
常見錯誤 ① ——在 SELECT 子句中書寫了多餘的列
在使用
COUNT
這樣的聚合函數時,SELECT
子句中的元素有嚴格的限制。實際上,使用聚合函數時,SELECT
子句中只能存在以下三種元素。-
常數
-
聚合函數
-
GROUP BY
子句中指定的列名(也就是聚合鍵)
在 資料庫和 SQL 是什麼關係 中我們介紹過,常數就是像數字
123
,或者字元串'測試'
這樣寫在 SQL 語句中的固定值,將常數直接寫在SELECT
子句中沒有任何問題。此外還可以書寫聚合函數或者聚合鍵,這些在之前的示例代碼中都已經出現過了。
這裡經常會出現的錯誤就是把聚合鍵之外的列名書寫在
SELECT
子句之中。例如代碼清單 16 中的SELECT
語句就會發生錯誤,無法正常執行。代碼清單 16 在 SELECT 子句中書寫聚合鍵之外的列名會發生錯誤
SELECT product_name, purchase_price, COUNT(*) FROM Product GROUP BY purchase_price;
執行結果(使用 PostgreSQL 的情況):
ERROR:列"product,product_name"必須包含在GROUP BY子句之中,或者必須在聚合函數內使用 行 1: SELECT product_name, purchase_price, COUNT(*)
列名
product_name
並沒有包含在GROUP BY
子句當中。因此,該列名也不能書寫在SELECT
子句之中 [3]。不支持這種語法的原因,大家仔細想一想應該就明白了。通過某個聚合鍵將表分組之後,結果中的一行數據就代表一組。
例如,使用進貨單價將表進行分組之後,一行就代表了一個進貨單價。問題就出在這裡,聚合鍵和商品名並不一定是一對一的。
例如,進貨單價是
2800
元的商品有“運動 T 恤”和“菜刀”兩種,但是2800
元這一行應該對應哪個商品名呢(圖 7)?如果規定了哪種商品優先表示的話則另當別論,但其實並沒有這樣的規則。
-
像這樣與聚合鍵相對應的、同時存在多個值的列出現在 SELECT
子句中的情況,理論上是不可能的。
法則 9
使用
GROUP BY
子句時,SELECT
子句中不能出現聚合鍵之外的列名。
-
常見錯誤 ② ——在 GROUP BY 子句中寫了列的別名
這也是一個非常常見的錯誤。在 為列設定別名 中我們學過,
SELECT
子句中的項目可以通過AS
關鍵字來指定別名。但是,在
GROUP BY
子句中是不能使用別名的。代碼清單 17 中的SELECT
語句會發生錯誤 [4]。代碼清單 17 GROUP BY 子句中使用列的別名會引發錯誤
上述語句發生錯誤的原因之前已經介紹過了,是 SQL 語句在 DBMS 內部的執行順序造成的——
SELECT
子句在GROUP BY
子句之後執行。在執行
GROUP BY
子句時,SELECT
子句中定義的別名,DBMS 還並不知道。使用 PostgreSQL 執行上述 SQL 語句並不會發生錯誤,而會得到如下結果。但是這樣的寫法在其他 DBMS 中並不是通用的,因此請大家不要使用。
執行結果(使用 PostgreSQL 的情況):
pt | count -------------+------ 衣服 | 2 辦公用品 | 2 廚房用具 | 4
法則 10
在
GROUP BY
子句中不能使用SELECT
子句中定義的別名。 -
常見錯誤 ③ —— GROUP BY 子句的結果能排序嗎
GROUP BY
子句的結果通常都包含多行,有時可能還會是成百上千行。那麼,這些結果究竟是按照什麼順序排列的呢?答案是:“隨機的。”
我們完全不知道結果記錄是按照什麼規則進行排序的。可能乍一看是按照行數的降序或者聚合鍵的升序進行排列的,但其實這些全都是偶然的。
當你再次執行同樣的
SELECT
語句時,得到的結果可能會按照完全不同的順序進行排列。通常
SELECT
語句的執行結果的顯示順序都是隨機的,因此想要按照某種特定順序進行排序的話,需要在SELECT
語句中進行指定。具體的方法將在本文第 4 節中學習。法則 11
GROUP BY
子句結果的顯示是無序的。 -
常見錯誤 ④ ——在 WHERE 子句中使用聚合函數
最後要介紹的是初學者非常容易犯的一個錯誤。
我們還是先來看一下之前提到的按照商品種類(
product_type
列)對錶進行分組,計算每種商品數據行數的例子吧。SELECT
語句如代碼清單 18 所示。代碼清單 18 按照商品種類統計數據行數
SELECT product_type, COUNT(*) FROM Product GROUP BY product_type;
執行結果:
product_type | count --------------+------- 衣服 | 2 辦公用品 | 2 廚房用具 | 4
如果我們想要取出恰好包含 2 行數據的組該怎麼辦呢?滿足要求的是“辦公用品”和“衣服”。
想要指定選擇條件時就要用到
WHERE
子句,初學者通常會想到使用代碼清單 19 中的SELECT
語句吧。代碼清單 19 在 WHERE 子句中使用聚合函數會引發錯誤
SELECT product_type, COUNT(*) FROM Product WHERE COUNT(*) = 2 GROUP BY product_type;
遺憾的是,這樣的
SELECT
語句在執行時會發生錯誤。執行結果(使用 PostgreSQL 的情況):
ERROR: 不能在WHERE子句中使用聚合 行 3: WHERE COUNT(*) = 2 ^
實際上,只有
SELECT
子句和HAVING
子句(以及之後將要學到的ORDER BY
子句)中能夠使用COUNT
等聚合函數。並且,
HAVING
子句可以非常方便地實現上述要求。下一節我們將會學習HAVING
子句。法則 12
只有
SELECT
子句和HAVING
子句(以及ORDER BY
子句)中能夠使用聚合函數。
專欄
DISTINCT 和 GROUP BY
細心的讀者可能會發現,第 1 節中介紹的
DISTINCT
和第 2 節介紹的GROUP BY
子句,都能夠刪除後續列中的重覆數據。例如,代碼清單 A 中的 2 條
SELECT
語句會返回相同的結果。代碼清單 A DISTINCT 和 GROUP BY 能夠實現相同的功能
SELECT DISTINCT product_type FROM Product; SELECT product_type FROM Product GROUP BY product_type;
執行結果:
product_type -------------- 衣服 辦公用品 廚房用具
除此之外,它們還都會把
NULL
作為一個獨立的結果返回,對多列使用時也會得到完全相同的結果。其實不僅處理結果相同,執行速度也基本上差不多,那麼到底應該使用哪一個呢?但其實這個問題本身就是本末倒置的,我們應該考慮的是該
SELECT
語句是否滿足需求。選擇的標準其實非常簡單,在“想要刪除選擇結果中的重覆記錄”時使用
DISTINCT
,在“想要計算彙總結果”時使用GROUP BY
。不使用
COUNT
等聚合函數,而只使用GROUP BY
子句的SELECT
語句,會讓人覺得非常奇怪,使人產生“到底為什麼要對錶進行分組呢?這樣做有必要嗎?”等疑問。SQL 語句的語法與英語十分相似,理解起來非常容易,如果大家浪費了這一優勢,編寫出一些難以理解的 SQL 語句,那就太可惜了。
三、為聚合結果指定條件
本節重點
使用
COUNT
函數等對錶中數據進行彙總操作時,為其指定條件的不是WHERE
子句,而是HAVING
子句。聚合函數可以在
SELECT
子句、HAVING
子句和ORDER BY
子句中使用。
HAVING
子句要寫在GROUP BY
子句之後。
WHERE
子句用來指定數據行的條件,HAVING
子句用來指定分組的條件。
3.1 HAVING 子句
使用前一節學過的 GROUP BY
子句,可以得到將表分組後的結果。在此,我們來思考一下通過指定條件來選取特定組的方法。
例如,如何才能取出“聚合結果正好為 2 行的組”呢(圖 8)?
說到指定條件,估計大家都會首先想到 WHERE
子句。但是,WHERE
子句只能指定記錄(行)的條件,而不能用來指定組的條件(例如,“數據行數為 2 行”或者“平均值為 500”等)。
因此,對集合指定條件就需要使用其他的子句了,此時便可以用 HAVING
子句 [5]。
HAVING
子句的語法如下所示。
語法 3 HAVING 子句
SELECT <列名1>, <列名2>, <列名3>, ……
FROM <表名>
GROUP BY <列名1>, <列名2>, <列名3>, ……
HAVING <分組結果對應的條件>
HAVING
子句必須寫在 GROUP BY
子句之後,其在 DBMS 內部的執行順序也排在 GROUP BY
子句之後。
使用 HAVING 子句時 SELECT 語句的順序
SELECT → FROM → WHERE → GROUP BY → HAVING
法則 13
HAVING
子句要寫在GROUP BY
子句之後。
接下來就讓我們練習一下 HAVING
子句吧。例如,針對按照商品種類進行分組後的結果,指定“包含的數據行數為 2 行”這一條件的 SELECT 語句,請參見代碼清單 20。
代碼清單 20 從按照商品種類進行分組後的結果中,取出“包含的數據行數為 2 行”的組
SELECT product_type, COUNT(*)
FROM Product
GROUP BY product_type
HAVING COUNT(*) = 2;
執行結果:
product_type | count
--------------+------
衣服 | 2
辦公用品 | 2
我們可以看到執行結果中並沒有包含數據行數為 4 行的“廚房用具”。
未使用 HAVING
子句時的執行結果中包含“廚房用具”,但是通過設置 HAVING
子句的條件,就可以選取出只包含 2 行數據的組了(代碼清單 21)。
代碼清單 21 不使用 HAVING 子句的情況
SELECT product_type, COUNT(*)
FROM Product
GROUP BY product_type;
執行結果:
下麵我們再來看一個使用 HAVING
子句的例子。這次我們還是按照商品種類對錶進行分組,但是條件變成了“銷售單價的平均值大於等於 2500
元”。
首先來看一下不使用 HAVING
子句的情況,請參見代碼清單 22。
代碼清單 22 不使用 HAVING 子句的情況
SELECT product_type, AVG(sale_price)
FROM Product
GROUP BY product_type;
執行結果:
product_type | avg
--------------+----------------------
衣服 | 2500.0000000000000000
辦公用品 | 300.0000000000000000
廚房用具 | 2795.0000000000000000
按照商品種類進行切分的 3 組數據都顯示出來了。下麵我們使用 HAVING
子句來設定條件,請參見代碼清單 23。
代碼清單 23 使用 HAVING 子句設定條件的情況
SELECT product_type, AVG(sale_price)
FROM Product
GROUP BY product_type
HAVING AVG(sale_price) >= 2500;
執行結果:
product_type | avg
--------------+----------------------
衣服 | 2500.0000000000000000
廚房用具 | 2795.0000000000000000
銷售單價的平均值為 300
元的“辦公用品”在結果中消失了。
3.2 HAVING 子句的構成要素
HAVING
子句和包含 GROUP BY
子句時的 SELECT
子句一樣,能夠使用的要素有一定的限制,限制內容也是完全相同的。HAVING
子句中能夠使用的 3 種要素如下所示。
-
常數
-
聚合函數
-
GROUP BY
子句中指定的列名(即聚合鍵)
代碼清單 20 中的例文指定了 HAVING COUNT(*)= 2
這樣的條件,其中 COUNT(*)
是聚合函數,2
是常數,全都滿足上述要求。
反之,如果寫成了下麵這個樣子就會發生錯誤(代碼清單 24)。
代碼清單 24 HAVING 子句的不正確使用方法
SELECT product_type, COUNT(*)
FROM Product
GROUP BY product_type
HAVING product_name = '圓珠筆';
執行結果:
ERROR: 列"product,product_name"必須包含在GROUP BY子句當中,或者必須在聚合函數中使用
行 4: HAVING product_name = '圓珠筆';
product_name
列並不包含在 GROUP BY
子句之中,因此不允許寫在 HAVING
子句里。
在思考 HAVING
子句的使用方法時,把一次彙總後的結果(類似表 2 的表)作為 HAVING
子句起始點的話更容易理解。
表 2 按照商品種類分組後的結果
product_type |
COUNT(*) |
---|---|
廚房用具 | 4 |
衣服 | 2 |
辦公用品 | 2 |
可以把這種情況想象為使用 GROUP BY
子句時的 SELECT
子句。彙總之後得到的表中並不存在 product_name
這個列,SQL 當然無法為表中不存在的列設定條件了。
3.3 相對於 HAVING 子句,更適合寫在 WHERE 子句中的條件
也許有的讀者已經發現了,有些條件既可以寫在 HAVING
子句當中,又可以寫在 WHERE
子句當中。這些條件就是聚合鍵所對應的條件。
原表中作為聚合鍵的列也可以在 HAVING
子句中使用。因此,代碼清單 25 中的 SELECT
語句也是正確的。
代碼清單 25 將條件書寫在 HAVING 子句中的情況
SELECT product_type, COUNT(*)
FROM Product
GROUP BY product_type
HAVING product_type = '衣服';
執行結果:
product_type | count
--------------+------
衣服 | 2
上述 SELECT
語句的返回結果與代碼清單 26 中 SELECT
語句的返回結果是相同的。
代碼清單 26 將條件書寫在 WHERE 子句中的情況
SELECT product_type, COUNT(*)
FROM Product
WHERE product_type = '衣服'
GROUP BY product_type;
執行結果:
product_type | count
--------------+------
衣服 | 2
雖然條件分別寫在 WHERE
子句和 HAVING
子句當中,但是條件的內容以及返回的結果都完全相同。因此,大家可能會覺得兩種書寫方式都沒問題。
如果僅從結果來看的話,確實如此。但筆者卻認為,聚合鍵所對應的條件還是應該書寫在 WHERE
子句之中。
理由有兩個。
首先,根本原因是 WHERE
子句和 HAVING
子句的作用不同。如前所述,HAVING
子句是用來指定“組”的條件的。
因此,“行”所對應的條件還是應該寫在 WHERE
子句當中。這樣一來,書寫出的 SELECT
語句不但可以分清兩者各自的功能,理解起來也更加容易。
WHERE 子句 = 指定行所對應的條件
HAVING 子句 = 指定組所對應的條件
其次,對初學者來說,研究 DBMS 的內部實現這一話題有些深奧,這裡就不做介紹了,感興趣的讀者可以參考隨後的專欄——WHERE
子句和 HAVING
子句的執行速度。
法則 14
聚合鍵所對應的條件不應該書寫在
HAVING
子句當中,而應該書寫在WHERE
子句當中。
專欄
WHERE 子句和 HAVING 子句的執行速度
在
WHERE
子句和HAVING
子句中都可以使用的條件,最好寫在WHERE
子句中的另一個理由與性能即執行速度有關係。由於性能不在本文介紹的範圍之內,因此暫不進行說明。通常情況下,為了得到相同的結果,將條件寫在
WHERE
子句中要比寫在HAVING
子句中的處理速度更快,返回結果所需的時間更短。為了理解其中原因,就要從 DBMS 的內部運行機制來考慮。使用
COUNT
函數等對錶中的數據進行聚合操作時,DBMS 內部就會進行排序處理。排序處理是會大大增加機器負擔的高負荷的處理。因此,只有儘可能減少排序的行數,才能提高處理速度。
通過
WHERE
子句指定條件時,由於排序之前就對數據進行了過濾,因此能夠減少排序的數據量。但
HAVING
子句是在排序之後才對數據進行分組的,因此與在WHERE
子句中指定條件比起來,需要排序的數據量就會多得多。雖然 DBMS 的內部處理不盡相同,但是對於排序處理來說,基本上都是一樣的。
此外,
WHERE
子句更具速度優勢的另一個理由是,可以對WHERE
子句指定條件所對應的列創建索引,這樣也可以大幅提高處理速度。創建索引是一種非常普遍的提高 DBMS 性能的方法,效果也十分明顯,這對
WHERE
子句來說也十分有利。
四、對查詢結果進行排序
學習重點
使用
ORDER BY
子句對查詢結果進行排序。在
ORDER BY
子句中列名的後面使用關鍵字ASC
可以進行升序排序,使用DESC
關鍵字可以進行降序排序。
ORDER BY
子句中可以指定多個排序鍵。排序健中包含
NULL
時,會在開頭或末尾進行彙總。
ORDER BY
子句中可以使用SELECT
子句中定義的列的別名。
ORDER BY
子句中可以使用SELECT
子句中未出現的列或者聚合函數。
ORDER BY
子句中不能使用列的編號。
4.1 ORDER BY 子句
截至目前,我們使用了各種各樣的條件對錶中的數據進行查詢。本節讓我們再來回顧一下簡單的 SELECT
語句(代碼清單 27)。
代碼清單 27 顯示商品編號、商品名稱、銷售單價和進貨單價的 SELECT 語句
SELECT product_id, product_name, sale_price, purchase_price
FROM Product;
執行結果:
product_id | product_name | sale_price | purchase_price
------------+---------------+--------------+----------------
0001 | T恤衫 | 1000 | 500
0002 | 打孔器 | 500 | 320
0003 | 運動T恤 | 4000 | 2800
0004 | 菜刀 | 3000 | 2800
0005 | 高壓鍋 | 6800 | 5000
0006 | 叉子 | 500 |
0007 | 擦菜板 | 880 | 790
0008 | 圓珠筆 | 100 |
對於上述結果,在此無需特別說明,本節要為大家介紹的不是查詢結果,而是查詢結果的排列順序。
那麼,結果中的 8 行記錄到底是按照什麼順序排列的呢?乍一看,貌似是按照商品編號從小到大的順序(升序)排列的。
其實,排列順序是隨機的,這隻是個偶然。因此,再次執行同一條 SELECT
語句時,順序可能大為不同。
通常,從表中抽取數據時,如果沒有特別指定順序,最終排列順序便無從得知。即使是同一條 SELECT
語句,每次執行時排列順序很可能發生改變。
但是不進行排序,很可能出現結果混亂的情況。這時,便需要通過在 SELECT
語句末尾添加 ORDER BY
子句來明確指定排列順序。
ORDER BY
子句的語法如下所示。
語法 4 ORDER BY 子句
SELECT <列名1>, <列名2>, <列名3>, ……
FROM <表名>
ORDER BY <排序基準列1>, <排序基準列2>, ……
例如,按照銷售單價由低到高,也就是升序排列時,請參見代碼清單 28。
代碼清單 28 按照銷售單價由低到高(升序)進行排列
SELECT product_id, product_name, sale_price, purchase_price
FROM Product
ORDER BY sale_price;
執行結果:
不論何種情況,ORDER BY
子句都需要寫在 SELECT
語句的末尾。
這是因為對數據行進行排序的操作必須在結果即將返回時執行。ORDER BY
子句中書寫的列名稱為排序鍵。該子句與其他子句的順序關係如下所示。
子句的書寫順序
1. SELECT 子句 → 2. FROM 子句 → 3. WHERE 子句 → 4. GROUP BY 子句 → 5. HAVING 子句 → 6. ORDER BY 子句
法則 15
ORDER BY
子句通常寫在SELECT
語句的末尾。
不想指定數據行的排列順序時,SELECT
語句中不寫 ORDER BY
子句也沒關係。
4.2 指定升序或降序
與上述示例相反,想要按照銷售單價由高到低,也就是降序排列時,可以參見代碼清單 29,在列名後面使用 DESC
關鍵字。
代碼清單 29 按照銷售單價由高到低(降序)進行排列
SELECT product_id, product_name, sale_price, purchase_price
FROM Product
ORDER BY sale_price DESC;
執行結果:
product_id | product_name | sale_ price | purchase_ price
------------+--------------+-------------+----------------
0005 | 高壓鍋 | 6800 | 5000
0003 | 運動T恤 | 4000 | 2800
0004 | 菜刀 | 3000 | 2800
0001 | T恤衫 | 1000 | 500
0007 | 擦菜板 | 880 | 790
0002 | 打孔器 | 500 | 320
0006 | 叉子 | 500 |
0008 | 圓珠筆 | 100 |
如上所示,這次銷售單價最高(6800
元)的高壓鍋排在了第一位。
其實,使用升序進行排列時,正式的書寫方式應該是使用關鍵字 ASC
,但是省略該關鍵字時會預設使用升序進行排序。
這可能是因為實際應用中按照升序排序的情況更多吧。ASC
和 DESC
是 ascendent(上升的)和 descendent(下降的)這兩個單詞的縮寫。
法則 16
未指定
ORDER BY
子句中排列順序時會預設使用升序進行排列。
由於 ASC
和 DESC
這兩個關鍵字是以列為單位指定的,因此可以同時指定一個列為升序,指定其他列為降序。
4.3 指定多個排序鍵
本節開頭曾提到過對銷售單價進行升序排列的 SELECT
語句(代碼清單 28)的執行結果,我們再來回顧一下。
可以發現銷售單價為 500
元的商品有 2 件。相同價格的商品的順序並沒有特別指定,或者可以說是隨機排列的。
如果想要對該順序的商品進行更細緻的排序的話,就需要再添加一個排序鍵。在此,我們以添加商品編號的升序為例,請參見代碼清單 30。
代碼清單 30 按照銷售單價和商品編號的升序進行排序
SELECT product_id, product_name, sale_price, purchase_price
FROM Product
ORDER BY sale_price, product_id;
執行結果:
這樣一來,就可以在 ORDER BY
子句中同時指定多個排序鍵了。規則是優先使用左側的鍵,如果該列存在相同值的話,再接著參考右側的鍵。當然,也可以同時使用 3 個以上的排序鍵。
4.4 NULL 的順序
在此前的示例中,我們已經使用過銷售單價(sale_price
列)作為排序鍵了,這次讓我們嘗試使用進貨單價(purchase_price
列)作為排序鍵吧。
此時,問題來了,圓珠筆和叉子對應的值是 NULL
,究竟 NULL
會按照什麼順序進行排列呢? NULL
是大於 100
還是小於 100
呢?或者說 5000
和 NULL
哪個更大呢?
請大家回憶一下我們在 不能對 NULL 使用比較運算符 中學過的內容。沒錯,不能對 NULL
使用比較運算符,也就是說,不能對 NULL
和數字進行排序,也不能與字元串和日期比較大小。
因此,使用含有 NULL
的列作為排序鍵時, NULL
會在結果的開頭或末尾彙總顯示(代碼清單 31)。
代碼清單 31 按照進貨單價的升序進行排列
SELECT product_id, product_name, sale_price, purchase_price
FROM Product
ORDER BY purchase_price;
執行結果:
究竟是在開頭顯示還是在末尾顯示,並沒有特殊規定。某些 DBMS 中可以指定 NULL
在開頭或末尾顯示,希望大家對自己使用的 DBMS 的功能研究一下。
法則 17
排序鍵中包含
NULL
時,會在開頭或末尾進行彙總。
4.5 在排序鍵中使用顯示用的別名
在第 2 節“常見錯誤 ②”中曾介紹過,在 GROUP BY
子句中不能使用 SELECT
子句中定義的別名,但是在 ORDER BY
子句中卻是允許使用別名的。
因此,代碼清單 32 中的 SELECT
語句並不會出錯,可正確執行。
代碼清單 32 ORDER BY 子句中可以使用列的別名
SELECT product_id AS id, product_name, sale_price AS sp, purchase_price
FROM Product
ORDER BY sp, id;
上述 SELECT
語句與之前按照“銷售單價和商品編號的升序進行排列”的 SELECT
語句(代碼清單 31)意思完全相同:
id | product_name | sp | purchase_price
------+---------------+-------+---------------
0008 | 圓珠筆 | 100 |
0002 | 打孔器 | 500 | 320
0006 | 叉子 | 500 |
0007 | 擦菜板 | 880 | 790
0001 | T恤衫 | 1000 | 500
0004 | 菜刀 | 3000 | 2800
0003 | 運動T恤 | 4000 | 2800
0005 | 高壓鍋 | 6800 | 5000
不能在 GROUP BY
子句中使用的別名,為什麼可以在 ORDER BY
子句中使用呢?這是因為 SQL 語句在 DBMS 內部的執行順序被掩蓋起來了。
SELECT
語句按照子句為單位的執行順序如下所示。
使用 HAVING 子句時 SELECT 語句的順序
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
這隻是一個粗略的總結,雖然具體的執行順序根據 DBMS 的不同而不同,但是大家有這樣一個大致的印象就可以了。
一定要記住 SELECT
子句的執行順序在 GROUP BY
子句之後,ORDER BY
子句之前。
因此,在執行 GROUP BY
子句時,SELECT
語句中定義的別名無法被識別 [^8]。對於在 SELECT
子句之後執行的 ORDER BY
子句來說,就沒有這樣的問題了。
[^8] 也是因為這一原因,HAVING
子句也不能使用別名。
法則 18
在
ORDER BY
子句中可以使用SELECT
子句中定義的別名。
4.6 ORDER BY 子句中可以使用的列
ORDER BY
子句中也可以使用存在於表中、但並不包含在 SELECT
子句之中的列(代碼清單 33)。
代碼清單 33 SELECT 子句中未包含的列也可以在 ORDER BY 子句中使用
SELECT product_name, sale_price, purchase_price
FROM Product
ORDER BY product_id;
執行結果:
product_name | sale_price | purchase_price
---------------+-------------+----------------
T恤衫