1. 程式人生 > >SQL進階隨筆--case用法(一)

SQL進階隨筆--case用法(一)

SQL進階一整個是根據我看了pdf版本的整理以及自己的見解整理。後期也方便我自己檢視和複習。

CASE 表示式

CASE 表示式是從 SQL-92 標準開始被引入的。可能因為它是相對較新的技術,所以儘管使用起來非常便利,但其真正的價值卻並不怎麼為人所知。很多人不用它,或者用它的簡略版函式,例如 DECODE(Oracle)、IF (MySQL)等。然而,正如 Joe Celko 所說,CASE表示式也許是 SQL-92 標準里加入的最有用的特性。如果能用好它,那麼 SQL 能解決的問題就會更廣泛,寫法也會更加漂亮。而且,因為 CASE 表示式是不依賴於具體資料庫的技術,所以可以提高 SQL 程式碼的可移植性。這裡強烈推薦大家改用 CASE 表示式,特別是使用DECODE 函式的 Oracle 使用者 。

正對decode,我們和case進行下對比,然後引出我們的主角case。

DECODE 是 Oracle 使用者很熟悉的函式,它有以下四個不如 CASE 表示式的地方。
• 它是 Oracle 獨有的函式,所以不具有可移植性。
• 分支數最大支援 127 個(引數上限 255 個,一個分支需要 2 個引數)。
• 如果分支數增加,程式碼會變得非常難讀。
• 表達能力較弱。具體來說,引數裡不能使用謂詞,也不能巢狀子查詢。

ok,現在用一些案例去了解學習下優點眾多的case用法

#CASE 表示式概述

首先我們來學習一下基本的寫法,CASE 表示式有簡單 CASE 表示式(simple case expression)和搜尋 CASE 表示式(searched caseexpression)兩種寫法,它們分別如下所示。

■CASE 表示式的寫法

-- 簡單CASE 表示式
CASE sex
WHEN '1' THEN ''
WHEN '2' THEN ''
ELSE '其他' END
-- 搜尋CASE 表示式
CASE WHEN sex = '1' THEN ''
WHEN sex = '2' THEN ''
ELSE '其他' END

這兩種寫法的執行結果是相同的,“sex”列(欄位)如果是 '1' ,那麼結果為男;如果是 '2' ,那麼結果為女。簡單 CASE 表示式正如其名,寫法簡單,但能實現的事情比較有限。簡單 CASE 表示式能寫的條件,搜尋 CASE 表示式也能寫,所以後面採用搜尋 CASE 表示式的寫法。

 

我們在編寫 SQL 語句的時候需要注意,在發現為真的 WHEN 子句時,CASE 表示式的真假值判斷就會中止,而剩餘的 WHEN 子句會被忽略。為了避免引起不必要的混亂,使用 WHEN 子句時要注意條件的排他性。

■剩餘的 WHEN 子句被忽略的寫法示例

-- 例如,這樣寫的話,結果裡不會出現“第二”
CASE WHEN col_1 IN ('a', 'b') THEN '第一'
WHEN col_1 IN ('a') THEN '第二'
ELSE '其他' END

注意事項 1:統一各分支返回的資料型別
雖然這一點無需多言,但這裡還是要強調一下:一定要注意 CASE 表示式裡各個分支返回的資料型別是否一致。某個分支返回字元型,而其他分支返回數值型的寫法是不正確的。
注意事項 2:不要忘了寫 END
使用 CASE 表示式的時候,最容易出現的語法錯誤是忘記寫 END 。雖然忘記寫時程式會返回比較容易理解的錯誤訊息,不算多麼致命的錯誤。但是,感覺自己寫得沒問題,而執行時卻出錯的情況大多是由這個原因引起的,所以請一定注意一下。

注意事項 3:養成寫 ELSE 子句的習慣
與 END 不同,ELSE 子句是可選的,不寫也不會出錯。不寫 ELSE 子句時,CASE 表示式的執行結果是 NULL 。但是不寫可能會造成“語法沒有錯誤,結果卻不對”這種不易追查原因的麻煩,所以最好明確地寫上 ELSE 子句(即便是在結果可以為 NULL 的情況下)。養成這樣的習慣後,我們從程式碼上就可以清楚地看到這種條件下會生成 NULL,而且將來程式碼有修改時也能減少失誤。

#將已有編號方式轉換為新的方式並統計

在進行非定製化統計時,我們經常會遇到將已有編號方式轉換為另外一種便於分析的方式並進行統計的需求。例如,現在有一張按照“‘1:北海道’、‘2:青森’、……、‘47:沖繩’”這種編號方式來統計都道府縣 人口的表,我們需要以東北、關東、九州等地區為單位來分組,並統計人口數量。具體來說,就是統計下表 PopTbl 中的內容,得出如右表“統計結果”所示的結果。

在“統計結果”這張表中,“四國”對應的是表 PopTbl 中的“德島、香川、愛媛、高知 ”,“九 州”對應的是表 PopTbl 中的“福岡、佐賀、長崎”。

大家會怎麼實現呢?定義一個包含“地區編號”列的檢視是一種做法,但是這樣一來,需要新增的列的數量將等同於統計物件的編號個數,而且很難動態地修改。

而如果使用 CASE 表示式,則用如下所示的一條 SQL 語句就可以完成。為了便於理解,這裡用縣名(pref_name )代替編號作為GROUP BY 的列。

-- 把縣編號轉換成地區編號(1)
SELECT CASE pref_name
WHEN '德島' THEN '四國'
WHEN '香川' THEN '四國'
WHEN '愛媛' THEN '四國'
WHEN '高知' THEN '四國'
WHEN '福岡' THEN '九州'
WHEN '佐賀' THEN '九州'
WHEN '長崎' THEN '九州'
ELSE '其他' END AS district,
SUM(population)
FROM PopTbl
GROUP BY CASE pref_name
WHEN '德島' THEN '四國'
WHEN '香川' THEN '四國'
WHEN '愛媛' THEN '四國'
WHEN '高知' THEN '四國'
WHEN '福岡' THEN '九州'
WHEN '佐賀' THEN '九州'
WHEN '長崎' THEN '九州'
ELSE '其他' END;

這裡的關鍵在於將 SELECT 子句裡的 CASE 表示式複製到 GROUP BY子句裡。需要注意的是,如果對轉換前的列“pref_name ”進行 GROUP BY ,就得不到正確的結果(因為這並不會引起語法錯誤,所以容易被忽視)。

 

同樣地,也可以將數值按照適當的級別進行分類統計。例如,要按人口數量等級(pop_class )查詢都道府縣個數的時候,就可以像下面這樣寫 SQL 語句。

-- 按人口數量等級劃分都道府縣
SELECT CASE WHEN population < 100 THEN '01'
WHEN population >= 100 AND population < 200 THEN '02'
WHEN population >= 200 AND population < 300 THEN '03'
WHEN population >= 300 THEN '04'
ELSE NULL END AS pop_class,
COUNT(*) AS cnt
FROM PopTbl
GROUP BY CASE WHEN population < 100 THEN '01'
WHEN population >= 100 AND population < 200 THEN '02'
WHEN population >= 200 AND population < 300 THEN '03'
WHEN population >= 300 THEN '04'
ELSE NULL END;
pop_class cnt
--------- ----
01     1
02     3
03     3
04     2

這個技巧非常好用。不過,必須在 SELECT 子句和 GROUP BY 子句這兩處寫一樣的 CASE 表示式,這有點兒麻煩。後期需要修改的時候,很容易發生只改了這一處而忘掉改另一處的失誤。

所以,如果我們可以像下面這樣寫,那就方便多了。

-- 把縣編號轉換成地區編號(2) :將CASE 表示式歸納到一處
SELECT CASE pref_name
WHEN '德島' THEN '四國'
WHEN '香川' THEN '四國'
WHEN '愛媛' THEN '四國'
WHEN '高知' THEN '四國'
WHEN '福岡' THEN '九州'
WHEN '佐賀' THEN '九州'
WHEN '長崎' THEN '九州'
ELSE '其他' END AS district,
SUM(population)
FROM PopTbl
GROUP BY district; ←-------GROUP BY 子句裡引用了SELECT 子句中定義的別名

但是沒錯,這裡的 GROUP BY 子句使用的正是 SELECT 子句裡定義的列的別稱——district 。但是嚴格來說,這種寫法是違反標準 SQL 的規則的。因為 GROUP BY 子句比 SELECT 語句先執行,所以在 GROUP BY 子句中引用在 SELECT 子句裡定義的別稱是不被允許的。事實上,在 Oracle、DB2、SQL Server 等資料庫裡採用這種寫法時就會出錯。

不過也有支援這種 SQL 語句的資料庫,例如在 PostgreSQL 和MySQL 中,這個查詢語句就可以順利執行。這是因為,這些資料庫在執行查詢語句時,會先對 SELECT 子句裡的列表進行掃描,並對列進行計算。不過因為這是違反標準的寫法,所以這裡不強烈推薦大家使用。但是,這樣寫出來的 SQL 語句確實非常簡潔,而且可讀性也很好。

■用一條 SQL 語句進行不同條件的統計

進行不同條件的統計是 CASE 表示式的著名用法之一。例如,我們需要往儲存各縣人口數量的表 PopTbl 裡新增上“性別”列,然後求按性別、縣名彙總的人數。具體來說,就是統計表 PopTbl2 中的資料,然後求出如表“統計結果”所示的結果。圖片沒有擷取完全。

 

 通常的做法是像下面這樣,通過在 WHERE 子句裡分別寫上不同的條件,然後執行兩條 SQL 語句來查詢。

■示例程式碼 3

-- 男性人口
SELECT pref_name,
SUM(population)
FROM PopTbl2
WHERE sex = '1'
GROUP BY pref_name;
-- 女性人口
SELECT pref_name,
SUM(population)
FROM PopTbl2
WHERE sex = '2'
GROUP BY pref_name;

最後需要通過宿主語言或者應用程式將查詢結果按列展開。如果使用UNION ,只用一條 SQL 語句就可以實現,但使用這種做法時,工作量並沒有減少,SQL 語句也會變得很長。而如果使用 CASE 表示式,下面這一條簡單的 SQL 語句就可以搞定。

 

SELECT pref_name,
-- 男性人口
SUM( CASE WHEN sex = '1' THEN population ELSE 0 END) AS cnt_m,
-- 女性人口
SUM( CASE WHEN sex = '2' THEN population ELSE 0 END) AS cnt_f
FROM PopTbl2
GROUP BY pref_name;

上面這段程式碼所做的是,分別統計每個縣的“男性”(即 '1' )人數和“女性”(即 '2' )人數。也就是說,這裡是將“行結構”的資料轉換成了“列結構”的資料。除了 SUM ,COUNT 、AVG 等聚合函式也都可以用於將行結構的資料轉換成列結構的資料。

這個技巧可貴的地方在於,它能將 SQL 的查詢結果轉換為二維表的格式。如果只是簡單地用 GROUP BY 進行聚合,那麼查詢後必須通過宿主語言或者 Excel 等應用程式將結果的格式轉換一下,才能使之成為交叉表。看上面的執行結果會發現,此時輸出的已經是側欄為縣名、表頭為性別的交叉表了。在製作統計表時,這個功能非常方便。如果用一句話來形容這個技巧,可以這樣說:

 新手用 WHERE 子句進行條件分支,高手用 SELECT 子句進行條件分支。