官术网_书友最值得收藏!

在SQL里表達(dá)條件分支

CASE 表達(dá)式是SQL里非常重要而且使用起來(lái)非常便利的技術(shù),我們應(yīng)該學(xué)會(huì)用它來(lái)描述條件分支。能熟練掌握 CASE 表達(dá)式,可以說(shuō)是SQL初級(jí)者和中級(jí)者之間的區(qū)別。本節(jié)將通過(guò)行列轉(zhuǎn)換、已有數(shù)據(jù)重分(分類)、與約束的結(jié)合使用、針對(duì)聚合結(jié)果的條件分支等例題,來(lái)介紹 CASE 表達(dá)式的用法。

CASE 表達(dá)式是從SQL-92 標(biāo)準(zhǔn)開(kāi)始被引入的。它雖然已經(jīng)被引入了二十多年,但在主流 DBMS 中仍然可以正常使用。不過(guò),可能因?yàn)樗窍鄬?duì)較新的技術(shù),所以盡管使用起來(lái)非常便利,但人們(尤其是初級(jí)者)并不怎么理解其真正的價(jià)值。很多人不用它,或者用它的簡(jiǎn)略版函數(shù),例如 DECODE(Oracle)、IF(MySQL)等。然而,正如著名的SQL專家喬 · 塞爾科所說(shuō),CASE 表達(dá)式也許是SQL-92 標(biāo)準(zhǔn)里加入的最有用的特性。如果能用好它,那么SQL能解決的問(wèn)題就會(huì)更廣泛,寫(xiě)法也會(huì)更加漂亮。而且,CASE 表達(dá)式是不依賴于具體數(shù)據(jù)庫(kù)的技術(shù),具有提高SQL代碼的可移植性等優(yōu)點(diǎn) 1

1 例如,DECODE 是 Oracle 用戶很熟悉的函數(shù),它有以下 4 個(gè)不如 CASE 表達(dá)式的地方。
· 它是 Oracle 獨(dú)有的函數(shù),所以不具有可移植性
· 分支數(shù)最多支持 127 個(gè)(參數(shù)上限為 255 個(gè),1 個(gè)分支需要 2 個(gè)參數(shù))
· 如果分支數(shù)增加,代碼會(huì)變得非常難讀
· 表達(dá)能力較弱。具體來(lái)說(shuō),就是參數(shù)里不能使用謂詞,也不能嵌套子查詢

首先,我們來(lái)學(xué)習(xí)一下基本的寫(xiě)法。CASE 表達(dá)式有簡(jiǎn)單 CASE 表達(dá)式(simple case expression)和搜索 CASE 表達(dá)式(searched case expression)兩種寫(xiě)法,它們分別如下所示。

CASE 表達(dá)式的寫(xiě)法

--簡(jiǎn)單 CASE 表達(dá)式
CASE sex
  WHEN '1' THEN '男'
  WHEN '2' THEN '女'
ELSE '其他' END
--搜索 CASE 表達(dá)式
CASE WHEN sex = '1' THEN '男'
     WHEN sex = '2' THEN '女'
ELSE '其他' END

這兩種寫(xiě)法的執(zhí)行結(jié)果是相同的,sex 列(字段)如果是 '1',那么結(jié)果為男;如果是 '2',那么結(jié)果為女。簡(jiǎn)單 CASE 表達(dá)式正如其名,寫(xiě)法簡(jiǎn)單,但能實(shí)現(xiàn)的事情比較有限。簡(jiǎn)單 CASE 表達(dá)式能寫(xiě)的條件,搜索 CASE 表達(dá)式也能寫(xiě),所以本書(shū)基本上采用搜索 CASE 表達(dá)式的寫(xiě)法。

我們?cè)诰帉?xiě)SQL語(yǔ)句的時(shí)候需要注意,在發(fā)現(xiàn)結(jié)果為真的 WHEN 子句時(shí),CASE 表達(dá)式的真假值判斷就會(huì)中止,而剩余的 WHEN 子句會(huì)被忽略(不再判斷)2。為了避免引起不必要的混亂,使用 WHEN 子句時(shí)要注意條件的排他性。

2 該評(píng)價(jià)方法稱為短路求值(short-circuit evaluation)或最小化求值(minimal evaluation)。當(dāng)確定了整個(gè)表達(dá)式的值時(shí),判斷就會(huì)中止,而剩余的判斷將不再執(zhí)行,這是一種“省力”的判斷方式。Java 和 Python 等編程語(yǔ)言中也有執(zhí)行這種判斷的運(yùn)算符(有些語(yǔ)言只會(huì)進(jìn)行短路求值)。

剩余的 WHEN 子句被忽略的寫(xiě)法示例

-- 例如,這樣寫(xiě)的話,結(jié)果里不會(huì)出現(xiàn)“第二”
CASE WHEN col_1 IN ('a', 'b') THEN '第一'
     WHEN col_1 IN ('a')      THEN '第二'
ELSE '其他' END

此外,使用 CASE 表達(dá)式的時(shí)候,還需要注意以下幾點(diǎn)。

 

注意事項(xiàng) 1 統(tǒng)一各分支返回的數(shù)據(jù)類型

雖然這一點(diǎn)無(wú)須多言,但這里還是要強(qiáng)調(diào)一下:一定要注意 CASE 表達(dá)式里各個(gè)分支返回的數(shù)據(jù)類型是否一致。某個(gè)分支返回字符型,而其他分支返回?cái)?shù)值型的寫(xiě)法是不正確的。這是因?yàn)椋?code>CASE 表達(dá)式是最終要得出單一值的表達(dá)式,這一點(diǎn)與“< 數(shù)值 > + < 數(shù)值 >”是一樣的。如果“<數(shù)值 > + < 數(shù)值 >”的結(jié)果會(huì)隨具體情況而發(fā)生變化,有時(shí)是數(shù)值,有時(shí)是日期,那運(yùn)算就不成立了。

注意事項(xiàng) 2 不要忘了寫(xiě) END

使用 CASE 表達(dá)式的時(shí)候,最容易出現(xiàn)的語(yǔ)法錯(cuò)誤是忘記寫(xiě) ENDEND 是必不可少的,如果忘記寫(xiě),就會(huì)發(fā)生語(yǔ)法錯(cuò)誤。雖然我們忘記寫(xiě)的時(shí)候,程序會(huì)返回比較容易理解的錯(cuò)誤消息,不算多么致命的錯(cuò)誤,但是感覺(jué)自己寫(xiě)得沒(méi)問(wèn)題,執(zhí)行時(shí)卻出錯(cuò)的情況大多是由這個(gè)原因引起的,所以請(qǐng)一定注意一下。

注意事項(xiàng) 3 養(yǎng)成寫(xiě) ELSE 子句的習(xí)慣

END 不同,ELSE 子句是可選的,不寫(xiě)也不會(huì)出錯(cuò)。不寫(xiě) ELSE 子句時(shí),CASE 表達(dá)式的執(zhí)行結(jié)果是 NULL,但是不寫(xiě)可能會(huì)造成“語(yǔ)法沒(méi)有錯(cuò)誤,結(jié)果卻不對(duì)”這種不易追查原因的麻煩,所以最好明確地寫(xiě)上 ELSE 子句(即便是在結(jié)果可以為 NULL 的情況下)。養(yǎng)成這樣的習(xí)慣后,我們從代碼上就可以清楚地看到這種條件下會(huì)生成 NULL,而且將來(lái)代碼有修改時(shí)也能減少失誤。

 

如果單純地看 CASE 表達(dá)式的使用方法,大家可能會(huì)有“CASE 表達(dá)式只是將標(biāo)簽換種說(shuō)法而已”之類的感覺(jué)。

實(shí)際上也確實(shí)如此(圖 1.1.1)。

圖 1.1.1 CASE 表達(dá)式就是將標(biāo)簽換種說(shuō)法

在單獨(dú)使用 CASE 表達(dá)式的情況下,它只是一個(gè)將某列的值換為其他值的工具。這樣的話,它與 IFDECODE 等依賴于具體實(shí)現(xiàn)的函數(shù)沒(méi)有什么區(qū)別。當(dāng)與其他的SQL工具搭配使用時(shí),CASE 表達(dá)式才能發(fā)揮出真正的價(jià)值,特別是與聚合函數(shù)(SUMAVG)和 GROUP BY 子句一起使用時(shí),甚至?xí)l(fā)揮出巨大的威力。接下來(lái),我們通過(guò)幾個(gè)示例,來(lái)看一下 CASE 表達(dá)式的真正價(jià)值。

在進(jìn)行非定制化統(tǒng)計(jì)時(shí),我們經(jīng)常會(huì)遇到將已有編號(hào)方式轉(zhuǎn)換為另外一種便于分析的方式并進(jìn)行統(tǒng)計(jì)的需求。例如,現(xiàn)在有一張以北海道、青森等縣(日本的縣級(jí)市)為單位記錄人口的表,我們需要以東北、關(guān)東、九州等地區(qū) 3 為單位來(lái)分組,并統(tǒng)計(jì)人口數(shù)量。具體來(lái)說(shuō),就是統(tǒng)計(jì)下頁(yè)表 PopTbl 中的內(nèi)容,得出如右表“統(tǒng)計(jì)結(jié)果”所示的結(jié)果。從表的設(shè)計(jì)上來(lái)說(shuō),這種表其實(shí)最好使用“縣的編號(hào)”作為鍵,但這里為了方便理解,我們使用“縣的名稱”作為鍵(本書(shū)的講解優(yōu)先考慮SQL語(yǔ)句的可讀性,示例中基本上使用的是名稱,而不是鍵)。

3 日本的省級(jí)行政單位有都、道、府、縣,包含一都(東京都)、二府(京都府和大阪府)、一道(北海道)和諸多的縣,統(tǒng)稱都道府縣。多個(gè)較近的縣被劃歸到一個(gè)地區(qū),如關(guān)東地區(qū)、九州地區(qū)等,類似我國(guó)的華北地區(qū)、華南地區(qū)等概念。——譯者注

4在“統(tǒng)計(jì)結(jié)果”這張表中,“四國(guó)”對(duì)應(yīng)的是表 PopTbl 中的“德島、香川、愛(ài)媛、高知”,“九州”對(duì)應(yīng)的是表 PopTbl 中的“福岡、佐賀、長(zhǎng)崎”。——編者注

大家會(huì)怎么實(shí)現(xiàn)呢?定義一個(gè)包含“地區(qū)編號(hào)”列的視圖是一種做法,但是這樣一來(lái),需要添加的列的數(shù)量將等同于統(tǒng)計(jì)對(duì)象的編號(hào)個(gè)數(shù),而且很難動(dòng)態(tài)地修改。

如果使用 CASE 表達(dá)式,則用如下所示的一條SQL語(yǔ)句就可以完成。

-- 把縣名轉(zhuǎn)換成地區(qū)名(1)
SELECT  CASE pref_name
                WHEN '德島' THEN '四國(guó)'
                WHEN '香川' THEN '四國(guó)'
                WHEN '愛(ài)媛' THEN '四國(guó)'
                WHEN '高知' THEN '四國(guó)'
                WHEN '福岡' THEN '九州'
                WHEN '佐賀' THEN '九州'
                WHEN '長(zhǎng)崎' THEN '九州'
        ELSE '其他' END AS district,
        SUM(population)
  FROM  PopTbl
 GROUP BY CASE pref_name
                WHEN '德島' THEN '四國(guó)'
                WHEN '香川' THEN '四國(guó)'
                WHEN '愛(ài)媛' THEN '四國(guó)'
                WHEN '高知' THEN '四國(guó)'
                WHEN '福岡' THEN '九州'
                WHEN '佐賀' THEN '九州'
                WHEN '長(zhǎng)崎' THEN '九州'
        ELSE '其他' END;

這里將 SELECT 子句里的 CASE 表達(dá)式復(fù)制到 GROUP BY 子句里。需要注意的是,如果對(duì)轉(zhuǎn)換前的 pref_name 列進(jìn)行 GROUP BY,就得不到正確的結(jié)果(因?yàn)檫@并不會(huì)引起語(yǔ)法錯(cuò)誤,所以容易被忽視)。

同樣地,也可以將數(shù)值按照適當(dāng)?shù)募?jí)別進(jìn)行分類統(tǒng)計(jì)。例如,要按人口數(shù)量等級(jí)(pop_class)查詢都道府縣個(gè)數(shù)的時(shí)候,就可以像下面這樣寫(xiě)SQL語(yǔ)句。

-- 按人口數(shù)量等級(jí)劃分都道府縣
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;

執(zhí)行結(jié)果

pop_class  cnt
--------- ----
01           1
02           3
03           3
04           2

這個(gè)技巧非常好用。不過(guò),必須在 SELECT 子句和 GROUP BY 子句這兩處寫(xiě)一樣的 CASE 表達(dá)式,這有點(diǎn)麻煩。后期需要修改的時(shí)候,很容易發(fā)生只改了這一處而忘掉改另一處的失誤。

所以,如果我們可以像下頁(yè)這樣寫(xiě),這就方便多了。

沒(méi)錯(cuò),這里的 GROUP BY 子句使用的正是 SELECT 子句里定義的列的別稱——district。有的 DBMS 支持這種SQL語(yǔ)句。例如在 PostgreSQL和 MySQL中,這個(gè)查詢語(yǔ)句就可以順利執(zhí)行,因?yàn)檫@些數(shù)據(jù)庫(kù)在執(zhí)行查詢語(yǔ)句時(shí),會(huì)先對(duì) SELECT 子句里的列表進(jìn)行掃描,并對(duì)列進(jìn)行計(jì)算。但遺憾的是,在 Oracle、DB2、SQLServer 等數(shù)據(jù)庫(kù)里采用這種寫(xiě)法時(shí),程序就會(huì)報(bào)錯(cuò) 5。由于 DBMS 之間并不兼容,所以這里不是很推薦大家使用這種寫(xiě)法。不過(guò),按照這種方式寫(xiě)出來(lái)的SQL語(yǔ)句確實(shí)非常簡(jiǎn)潔,而且可讀性很好。希望將來(lái)所有的 DBMS 都能夠支持這種語(yǔ)法。

5 例如,在 Oracle 中執(zhí)行該 SELECT 子句,就會(huì)出現(xiàn)下面的錯(cuò)誤消息。

12 行發(fā)生了錯(cuò)誤。:
ORA-00904:'DISTRICT':無(wú)效的標(biāo)識(shí)符。

這表示“PopTbl 中并沒(méi)有列名 DISTRICT”,倒也確實(shí)如此,但筆者覺(jué)得這個(gè)查詢也可以更靈活一點(diǎn),比如去查詢 SELECT 子句中定義的虛擬列名。

多條件統(tǒng)計(jì)是 CASE 表達(dá)式的著名用法之一。例如,我們需要往存儲(chǔ)各縣人口數(shù)量的表 PopTbl 里添加上“性別”列,然后求按性別、縣名匯總的人數(shù)。具體來(lái)說(shuō),就是統(tǒng)計(jì)表 PopTbl2 中的數(shù)據(jù),然后求出如表“統(tǒng)計(jì)結(jié)果”所示的結(jié)果。其中,1 表示男性,2 表示女性。

通常的做法是像下面這樣,分別在 WHERE 子句里寫(xiě)上不同的條件,然后執(zhí)行兩條SQL語(yǔ)句。

-- 男性人口
SELECT pref_name,
       population
  FROM PopTbl2
 WHERE sex = '1';
-- 女性人口
SELECT pref_name,
       population
  FROM PopTbl2
 WHERE sex = '2';

接著,還需要通過(guò)宿主語(yǔ)言或者應(yīng)用程序?qū)⒉樵兘Y(jié)果按列展開(kāi)。如果使用 UNION,只用一條SQL語(yǔ)句就可以實(shí)現(xiàn)查詢,但使用這種做法時(shí),工作量是一樣的,性能并沒(méi)有得到優(yōu)化,SQL語(yǔ)句也會(huì)變得很長(zhǎng)。如果使用 CASE 表達(dá)式,一條簡(jiǎn)單的SQL語(yǔ)句就可以搞定。

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;

執(zhí)行結(jié)果

pref_name    cnt_m  cnt_f
-----------  -----  -----
德島            60     40
香川           100    100
愛(ài)媛           100     50
高知           100    100
福岡           100    200
佐賀            20     80
長(zhǎng)崎           125    125
東京           250    150

上面這段代碼所做的是,分別統(tǒng)計(jì)每個(gè)縣的男性(即 '1')人口和女性(即 '2')人口。也就是說(shuō),這里是將“行結(jié)構(gòu)”的數(shù)據(jù)轉(zhuǎn)換成了“列結(jié)構(gòu)”的數(shù)據(jù)。除了 SUMCOUNTAVG 等聚合函數(shù)也都可以用于將行結(jié)構(gòu)的數(shù)據(jù)轉(zhuǎn)換成列結(jié)構(gòu)的數(shù)據(jù)。

這個(gè)技巧可貴的地方在于,它能將SQL的查詢結(jié)果轉(zhuǎn)換為二維表的格式。如果只是簡(jiǎn)單地用 GROUP BY 進(jìn)行聚合,那么查詢后必須通過(guò)宿主語(yǔ)言或者 Excel 等應(yīng)用程序?qū)⒔Y(jié)果的格式轉(zhuǎn)換一下,才能使之成為交叉表。看上面的執(zhí)行結(jié)果會(huì)發(fā)現(xiàn),此時(shí)輸出的已經(jīng)是側(cè)欄為縣名、表頭為性別的交叉表了。在制作統(tǒng)計(jì)表時(shí),這個(gè)功能非常方便。如果用一句話來(lái)形容這個(gè)技巧,可以這樣說(shuō):

新手用 WHERE 子句進(jìn)行條件分支,高手用 SELECT 子句進(jìn)行條件分支。

如此好的技巧,請(qǐng)大家多使用。

第一次看到這個(gè) SELECT 語(yǔ)句,可能會(huì)有人產(chǎn)生疑問(wèn):“又不是計(jì)算人口總數(shù),有必要使用這個(gè) SUM 函數(shù)嗎?”(筆者也曾有過(guò)這樣的疑問(wèn))。

從結(jié)論來(lái)講,該 SUM 函數(shù)是必須使用的。我們?cè)囍サ?SUM 函數(shù),再進(jìn)行查詢,就知道原因了。

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

執(zhí)行結(jié)果

pref_name    cnt_m  cnt_f
-----------  -----  -----
德島            60      0
德島             0     40
香川           100      0
香川             0    100
愛(ài)媛           100      0
愛(ài)媛             0     50
高知           100      0
高知             0    100
福岡           100      0
福岡             0    200
佐賀            20      0
佐賀             0     80
長(zhǎng)崎           125      0
長(zhǎng)崎             0    125
東京           250      0
東京             0    150

看到這樣的結(jié)果,大家應(yīng)該都能理解了吧?確實(shí),通過(guò)使用 CASE 表達(dá)式,我們就可以創(chuàng)建男性人口列(cnt_m)和女性人口列(cnt_f)。不過(guò),僅這樣并不能聚合記錄,因此原始的表 PopTbl2 中的記錄數(shù)就直接作為結(jié)果輸出了。因此,聚合記錄就要用到聚合函數(shù) SUMCASE 表達(dá)式本身并沒(méi)有聚合記錄的功能。正如前面所講,CASE 表達(dá)式只是將標(biāo)簽換種說(shuō)法而已,這里就是性別條件不滿足時(shí)將人口換為 0 了。

其實(shí),CASE 表達(dá)式和 CHECK 約束是很般配的一對(duì)組合。也許有很多數(shù)據(jù)庫(kù)工程師不怎么用 CHECK 約束,但是一旦他們了解了 CHECK 約束和 CASE 表達(dá)式結(jié)合使用之后的強(qiáng)大威力,就一定會(huì)躍躍欲試的 6

6 MySQL8.0 還不支持 CHECK 約束。

假設(shè)某公司規(guī)定“女性員工的工資必須在 20 萬(wàn)日元以下”,而在這個(gè)公司的人事表中,這條無(wú)理的規(guī)定是使用 CHECK 約束來(lái)描述的,代碼如下所示。

CONSTRAINT check_salary CHECK
   ( CASE WHEN sex = '2'
          THEN CASE WHEN salary <= 200000
                    THEN 1 ELSE 0 END
     ELSE 1 END = 1 )

在這段代碼里,CASE 表達(dá)式被嵌入 CHECK 約束里,描述了“如果是女性員工,則工資是 20 萬(wàn)日元以下”這個(gè)命題(判斷事情的語(yǔ)句)。在命題邏輯中,該命題是名為蘊(yùn)含式(conditional)的邏輯表達(dá)式,記作 P → Q。這里的 P 和 Q 表示任意命題,整體讀作“P 蘊(yùn)含 Q”。

這里需要重點(diǎn)理解的是蘊(yùn)含式和邏輯與(logical product)的區(qū)別。邏輯與也是一個(gè)邏輯表達(dá)式,意思是“P 且 Q”,記作 P ∧ Q。用邏輯與改寫(xiě)的 CHECK 約束如下所示。

CONSTRAINT check_salary CHECK
   ( sex = '2' AND salary <= 200000 )

當(dāng)然,這兩個(gè)約束的程序行為不一樣。究竟哪里不一樣呢?請(qǐng)先思考一下,再看下面的答案和解釋。

答案

如果在 CHECK 約束里使用邏輯與,該公司將不能雇傭男性員工。而如果使用蘊(yùn)含式,男性也可以在這里工作。

解釋

要想讓邏輯與 P ∧ Q 為真,需要命題 P 和命題 Q 均為真,或者一個(gè)為真且另一個(gè)無(wú)法判定真假。也就是說(shuō),能在這家公司工作的是“性別為女且工資在 20 萬(wàn)日元以下”的員工,以及性別或者工資無(wú)法確定的員工(如果一個(gè)條件為假,那么即使另一個(gè)條件無(wú)法確定真假,也不能在這里工作)。

而要想讓蘊(yùn)含式 P → Q 為真,需要命題 P 和命題 Q 均為真,或者 P 為假,或者 P 無(wú)法判定真假。也就是說(shuō)如果不滿足“是女性”這個(gè)前提條件,則無(wú)須考慮工資約束。

請(qǐng)參考下面這個(gè)關(guān)于邏輯與和蘊(yùn)含式的真值表。U 是SQL中三值邏輯的特有值 unknown 的縮寫(xiě)(關(guān)于三值邏輯,1-4 節(jié)將詳細(xì)介紹)。

如上表所示,蘊(yùn)含式在員工性別不是女性(或者無(wú)法確定性別)的時(shí)候?yàn)檎妫梢哉f(shuō)相比邏輯與約束,它更加寬松。

下面思考一下這樣一種需求:以某數(shù)值型的列的當(dāng)前值為判斷對(duì)象,將其更新成別的值。這里的問(wèn)題是,此時(shí) UPDATE 操作的條件會(huì)有多個(gè)分支。例如,我們通過(guò)下面這樣一張出自某公司人事部的員工工資信息表 Salaries 來(lái)看一下這種情況。

假設(shè)現(xiàn)在需要根據(jù)以下條件對(duì)該表的數(shù)據(jù)進(jìn)行更新。

01.對(duì)當(dāng)前工資為 30 萬(wàn)日元以上的員工,降薪 10%。

02.對(duì)當(dāng)前工資為 25 萬(wàn)日元以上且不滿 28 萬(wàn)日元的員工,加薪 20%。

按照這些要求更新完的數(shù)據(jù)應(yīng)該如下表所示。

乍一看,分別執(zhí)行下面兩個(gè) UPDATE 操作好像就可以做到,但這樣做的結(jié)果是不正確的。

-- 條件 1
UPDATE Salaries
SET salary = salary * 0.9
WHERE salary >= 300000;
-- 條件 2
UPDATE Salaries
SET salary = salary * 1.2
WHERE salary >= 250000 AND salary < 280000;

我們來(lái)分析一下不正確的原因。例如這里有一個(gè)員工,當(dāng)前工資是 30 萬(wàn)日元,按“條件 1”執(zhí)行 UPDATE 操作后,工資會(huì)被更新為 27 萬(wàn)日元,但繼續(xù)按“條件 2”執(zhí)行 UPDATE 操作后,工資又會(huì)被更新為 32.4 萬(wàn)日元。這樣一來(lái),本來(lái)應(yīng)該被降薪的員工卻被加薪了 2.4 萬(wàn)日元。

這樣的結(jié)果當(dāng)然并非人事部所愿。員工相田的工資必須被準(zhǔn)確地降為 27 萬(wàn)日元。問(wèn)題在于,第 1 次的 UPDATE 操作執(zhí)行后,“當(dāng)前工資”發(fā)生了變化,如果還拿它當(dāng)作第 2 次 UPDATE 的判定條件,結(jié)果就會(huì)不準(zhǔn)確。然而,即使將兩條SQL語(yǔ)句的執(zhí)行順序顛倒一下,當(dāng)前工資為 27 萬(wàn)日元的員工,其工資的更新結(jié)果也會(huì)出現(xiàn)問(wèn)題。為了避免出現(xiàn)這些問(wèn)題,準(zhǔn)確地表達(dá)出可惡的人事部長(zhǎng)的意圖,可以像下面這樣用 CASE 表達(dá)式來(lái)寫(xiě)SQL。

UPDATE Personnel
   SET salary = CASE WHEN salary >= 300000
                     THEN salary * 0.9
                     WHEN salary >= 250000 AND salary < 280000
                     THEN salary * 1.2
                ELSE salary END;

這條SQL語(yǔ)句不僅執(zhí)行結(jié)果正確,而且因?yàn)橹恍鑸?zhí)行 1 次,所以性能也更高。這樣的話,人事部長(zhǎng)就會(huì)滿意了吧?

需要注意的是,SQL語(yǔ)句最后一行的 ELSE salary 非常重要,必須寫(xiě)上。因?yàn)槿绻麤](méi)有它,條件 1 和條件 2 都不滿足的員工的工資就會(huì)被更新成 NULL。這一點(diǎn)與 CASE 表達(dá)式的設(shè)計(jì)有關(guān),在前面介紹 CASE 表達(dá)式的時(shí)候我們就已經(jīng)了解到,如果 CASE 表達(dá)式里沒(méi)有明確指定 ELSE 子句,執(zhí)行結(jié)果會(huì)被默認(rèn)地處理成 ELSE NULL。現(xiàn)在大家明白筆者最開(kāi)始強(qiáng)調(diào)使用 CASE 表達(dá)式時(shí)要習(xí)慣性地寫(xiě)上 ELSE 子句的理由了吧?

這個(gè)技巧的應(yīng)用范圍很廣。例如,可以用它輕松完成主鍵值調(diào)換這種繁重的工作。通常,當(dāng)我們想調(diào)換主鍵值 ab 時(shí),需要將主鍵值臨時(shí)轉(zhuǎn)換成某個(gè)中間值。使用這種方法時(shí)需要執(zhí)行 3 次 UPDATE 操作,但是如果使用 CASE 表達(dá)式,1 次就可以做到。

如果在調(diào)換上表的主鍵值 ab 時(shí)不用 CASE 表達(dá)式,則需要像下頁(yè)這樣寫(xiě) 3 條SQL語(yǔ)句。

--1. 將 a 轉(zhuǎn)換為中間值 d
UPDATE SomeTable
   SET p_key = 'd'
 WHERE p_key = 'a';
--2. 將 b 調(diào)換為 a
UPDATE SomeTable
   SET p_key = 'a'
 WHERE p_key = 'b';
--3. 將 d 調(diào)換為 b
UPDATE SomeTable
   SET p_key = 'b'
 WHERE p_key = 'd';

像上面這樣做,結(jié)果確實(shí)沒(méi)有問(wèn)題。只是,這里沒(méi)有必要執(zhí)行 3 次 UPDATE 操作,而且中間值 d 是否總能使用也是問(wèn)題。如果使用 CASE 表達(dá)式,就不必?fù)?dān)心這些,1 次就可以完成調(diào)換。

-- 用 CASE 表達(dá)式調(diào)換主鍵值
UPDATE SomeTable
   SET p_key = CASE WHEN p_key = 'a'
                    THEN 'b'
                    WHEN p_key = 'b'
                    THEN 'a'
               ELSE p_key END
 WHERE p_key IN ('a', 'b');

顯而易見(jiàn),這條SQL語(yǔ)句按照“如果是 a 則更新為 b,如果是 b 則更新為 a”這樣的條件分支進(jìn)行了 UPDATE 操作。不只是主鍵,唯一鍵的調(diào)換也可以用同樣的方法進(jìn)行。本例的關(guān)鍵點(diǎn)和上一例的加薪與降薪一樣,即用 CASE 表達(dá)式的條件分支進(jìn)行的更新操作是一氣呵成的,因此可以避免出現(xiàn)主鍵重復(fù)所導(dǎo)致的錯(cuò)誤 7

7 如果在 PostgreSQL和 MySQL數(shù)據(jù)庫(kù)執(zhí)行這條SQL語(yǔ)句,會(huì)因主鍵重復(fù)而出現(xiàn)錯(cuò)誤。例如,PostgeSQL數(shù)據(jù)庫(kù)中會(huì)顯示下面的錯(cuò)誤消息。

Error:重復(fù)鍵違反唯一約束 "sometable_pkey"
DETAIL:鍵 (p_key)=(b) 已經(jīng)存在

之所以會(huì)發(fā)生錯(cuò)誤,是因?yàn)樵趯⒅麈I為 'a' 的行的主鍵修改為 'b' 時(shí),主鍵 'b' 還是修改前的值。但是,約束的檢查本來(lái)就發(fā)生在更新完成后,因此在更新過(guò)程中主鍵一時(shí)出現(xiàn)重復(fù)也沒(méi)有問(wèn)題。事實(shí)上,在 Oracle、DB2 和SQLServer 數(shù)據(jù)庫(kù)中執(zhí)行該 UPDATE 語(yǔ)句也都沒(méi)有問(wèn)題。在 PostgreSQL數(shù)據(jù)庫(kù)中,建表時(shí)如果使用延遲約束(DEFERRABLE)選項(xiàng),執(zhí)行也不會(huì)發(fā)生錯(cuò)誤。

但是,一般來(lái)說(shuō)需要進(jìn)行這樣的調(diào)換是因?yàn)楸淼脑O(shè)計(jì)出現(xiàn)了問(wèn)題,所以請(qǐng)先重新審視一下表的設(shè)計(jì),去掉不必要的約束。

DECODE 函數(shù)等相比,CASE 表達(dá)式的一大優(yōu)勢(shì)在于能夠判斷表達(dá)式。也就是說(shuō),在 CASE 表達(dá)式里,我們能使用 BETWEENLIKE<> 等便利的謂詞組合,還能嵌套子查詢的 INEXISTS 謂詞。因此,CASE 表達(dá)式具有非常強(qiáng)大的表達(dá)能力。

如下所示,這里有一張培訓(xùn)學(xué)校的課程一覽表和一張展示每個(gè)月所設(shè)課程的表。

我們要用這兩張表來(lái)生成下面這樣的交叉表,以便于一目了然地知道每個(gè)月開(kāi)設(shè)的課程。

執(zhí)行結(jié)果

course_name  6 月  7 月  8 月
-----------  ----  ----  ----
會(huì)計(jì)入門(mén)       ○   ×    ×
財(cái)務(wù)知識(shí)       ×   ×    ○
簿記考試       ○   ×    ×
稅務(wù)師         ○   ○    ○

我們需要做的是,檢查表 OpenCourses 中的各月里有表 CourseMaster 中的哪些課程。這個(gè)匹配條件可以用 CASE 表達(dá)式來(lái)寫(xiě)。

-- 表的匹配:使用 IN 謂詞
SELECT course_name,
       CASE WHEN course_id IN
                    (SELECT course_id FROM OpenCourses
                      WHERE month = 201806) THEN '○'
            ELSE '×' END AS "6 月",
       CASE WHEN course_id IN
                    (SELECT course_id FROM OpenCourses
                      WHERE month = 201807) THEN '○'
            ELSE '×' END AS "7 月",
       CASE WHEN course_id IN
                    (SELECT course_id FROM OpenCourses
                      WHERE month = 201808) THEN '○'
            ELSE '×' END AS "8 月"
  FROM CourseMaster;
-- 表的匹配:使用 EXISTS 謂詞
SELECT CM.course_name,
       CASE WHEN EXISTS
                    (SELECT course_id FROM OpenCourses OC
                      WHERE month = 201806
                        AND OC.course_id = CM.course_id) THEN '○'
            ELSE '×' END AS "6 月",
       CASE WHEN EXISTS
                    (SELECT course_id FROM OpenCourses OC
                      WHERE month = 201807
                        AND OC.course_id = CM.course_id) THEN '○'
            ELSE '×' END AS "7 月",
       CASE WHEN EXISTS
                    (SELECT course_id FROM OpenCourses OC
                      WHERE month = 201808
                        AND OC.course_id = CM.course_id) THEN '○'
            ELSE '×' END AS "8 月"
  FROM CourseMaster CM;

這樣的查詢沒(méi)有進(jìn)行聚合,因此也不需要排序,月份增加的時(shí)候僅修改 SELECT 子句就可以了,擴(kuò)展性比較好。

無(wú)論使用 IN 還是 EXISTS,得到的結(jié)果是一樣的,但從性能方面來(lái)說(shuō),EXISTS 更好。通過(guò) EXISTS 進(jìn)行的子查詢能夠用到“month, course_id”這樣的主鍵索引,因此當(dāng)表 OpenCourses 里的數(shù)據(jù)比較多時(shí),使用 EXISTS 的優(yōu)勢(shì)會(huì)更大。

換個(gè)角度來(lái)看,表之間的數(shù)據(jù)匹配就是生成一張表側(cè)欄固定的交叉表,因此使用外連接的方法也可以完成。關(guān)于外連接的思路,我們將在 1-8 節(jié)進(jìn)行學(xué)習(xí)。

接下來(lái)介紹一下稍微高級(jí)的用法。這個(gè)用法乍一看可能讓人覺(jué)得是語(yǔ)法錯(cuò)誤,實(shí)際上并非如此,而且它在所有的 DBMS 中都可以使用。我們來(lái)看一道例題,假設(shè)這里有一張顯示了學(xué)生及其加入的社團(tuán)的一覽表。這張表的主鍵是“學(xué)號(hào)、社團(tuán) ID”。

有的學(xué)生同時(shí)加入了多個(gè)社團(tuán)(如學(xué)號(hào)為 100、200 的學(xué)生),有的學(xué)生只加入了一個(gè)社團(tuán)(如學(xué)號(hào)為 300、400、500 的學(xué)生)。對(duì)于加入了多個(gè)社團(tuán)的學(xué)生,我們通過(guò)將其“主社團(tuán)標(biāo)志”列設(shè)置為 Y 或者 N 來(lái)表明哪一個(gè)社團(tuán)是他的主社團(tuán);對(duì)于只加入了一個(gè)社團(tuán)的學(xué)生,我們將其“主社團(tuán)標(biāo)志”列設(shè)置為 N。

接下來(lái),我們按照下面的條件查詢這張表里的數(shù)據(jù)。

01.獲取只加入了一個(gè)社團(tuán)的學(xué)生的社團(tuán) ID。

02.獲取加入了多個(gè)社團(tuán)的學(xué)生的主社團(tuán) ID。

很容易想到的辦法是,針對(duì)兩個(gè)條件分別寫(xiě)SQL語(yǔ)句來(lái)查詢。要想知道學(xué)生“是否加入了多個(gè)社團(tuán)”,我們需要用 HAVING 子句對(duì)聚合結(jié)果進(jìn)行判斷。

條件 1 的SQL

-- 條件 1 :選擇只加入了一個(gè)社團(tuán)的學(xué)生
SELECT std_id, MAX(club_id) AS main_club
  FROM StudentClub
 GROUP BY std_id
HAVING COUNT(*) = 1;

執(zhí)行結(jié)果 1

std_id   main_club
------   ----------
300      4
400      5
500      6

條件 2 的SQL

-- 條件 2 :選擇加入了多個(gè)社團(tuán)的學(xué)生
SELECT std_id, club_id AS main_club
  FROM StudentClub
 WHERE main_club_flg = 'Y' ;

執(zhí)行結(jié)果 2

std_id  main_club
------  ----------
100     1
200     3

這樣做也能得到正確的結(jié)果,但需要寫(xiě)多條SQL語(yǔ)句,存在性能問(wèn)題。如果使用 CASE 表達(dá)式,下面這一條SQL語(yǔ)句就可以了。

SELECT std_id,
       CASE WHEN COUNT(*) = 1 -- 只加入了一個(gè)社團(tuán)的學(xué)生
            THEN MAX(club_id)
       ELSE MAX(CASE WHEN main_club_flg = 'Y'
                          THEN club_id
                ELSE NULL END) END AS main_club
  FROM StudentClub
 GROUP BY std_id;

執(zhí)行結(jié)果

std_id   main_club
------   ----------
100      1
200      3
300      4
400      5
500      6

這條SQL語(yǔ)句在 CASE 表達(dá)式里使用了聚合函數(shù),又在聚合函數(shù)里使用了 CASE 表達(dá)式。這種嵌套的寫(xiě)法讓人有點(diǎn)眼花繚亂,其主要目的是用 CASE WHEN COUNT(*) = 1 ... ELSE ... 這樣的 CASE 表達(dá)式來(lái)表示“只加入了一個(gè)社團(tuán)還是加入了多個(gè)社團(tuán)”這樣的條件分支。

這種寫(xiě)法比較新穎,因?yàn)槲覀冊(cè)诔鯇W(xué)SQL的時(shí)候,都學(xué)過(guò)對(duì)聚合結(jié)果進(jìn)行條件判斷時(shí)要用 HAVING 子句,但從這道例題可以看出,在 SELECT 語(yǔ)句里使用 CASE 表達(dá)式也可以完成同樣的工作,如果用一句話來(lái)形容這個(gè)技巧,可以這樣說(shuō):

新手用 HAVING 子句進(jìn)行條件分支,高手用 SELECT 子句進(jìn)行條件分支。

通過(guò)這道例題我們可以明白:CASE 表達(dá)式用在 SELECT 子句里時(shí),既可以寫(xiě)在聚合函數(shù)內(nèi)部,也可以寫(xiě)在聚合函數(shù)外部。這種高度自由的寫(xiě)法正是 CASE 表達(dá)式的魅力所在。那么,為什么 CASE 表達(dá)式中可以使用聚合函數(shù)呢?這是因?yàn)榫酆虾瘮?shù)是函數(shù),而 SELECT 子句中的最終結(jié)果是單個(gè)數(shù)值,所以這個(gè)數(shù)值可以成為外部 CASE 表達(dá)式的輸入。

本節(jié),我們一起領(lǐng)略了 CASE 表達(dá)式的靈活性和強(qiáng)大的表達(dá)能力。CASE 表達(dá)式是支撐SQL聲明式編程的根基之一,也是靈活運(yùn)用SQL時(shí)不可或缺的基礎(chǔ)技能,請(qǐng)一定要學(xué)會(huì)它。即便在本書(shū)的后半部分,也幾乎沒(méi)有哪一節(jié)是不用 CASE 表達(dá)式的,這也是筆者把它放在本書(shū)開(kāi)頭來(lái)介紹的原因。

面向過(guò)程語(yǔ)言中也有“CASE 語(yǔ)句”這樣的條件分支,因此 CASE 表達(dá)式經(jīng)常會(huì)與其混淆,被叫作 CASE“語(yǔ)句”。這是錯(cuò)誤的。準(zhǔn)確來(lái)說(shuō),它并不是語(yǔ)句,而是和 1+1 或者 a/b 一樣,屬于表達(dá)式的范疇。結(jié)束符 END 確實(shí)看起來(lái)像是在標(biāo)記一連串處理過(guò)程的終結(jié),所以初次接觸 CASE 表達(dá)式的人容易對(duì)這一點(diǎn)感到困惑。“表達(dá)式”和“語(yǔ)句”的名稱區(qū)別恰恰反映了二者在功能處理方面的差異。

作為表達(dá)式,CASE 表達(dá)式在執(zhí)行時(shí)會(huì)被判定為一個(gè)固定值,因此它可以寫(xiě)在聚合函數(shù)內(nèi)部;也正因?yàn)樗潜磉_(dá)式,所以還可以寫(xiě)在 SELECT 子句、GROUP BY 子句、WHERE 子句、ORDER BY 子句里。簡(jiǎn)單點(diǎn)說(shuō),在能寫(xiě)列名和常量的地方,通常都可以寫(xiě) CASE 表達(dá)式。從這個(gè)意義上來(lái)說(shuō),與 CASE 表達(dá)式最接近的不是面向過(guò)程語(yǔ)言里的 CASE 語(yǔ)句,而是 Lisp 和 Scheme 等函數(shù)式語(yǔ)言里的 casecond 這樣的條件表達(dá)式。關(guān)于SQL和函數(shù)式語(yǔ)言的對(duì)比,第 2 章會(huì)進(jìn)行介紹。

CASE 表達(dá)式可以寫(xiě)在任何地方
     -SELECT 子句
     -WHERE 子句
     -GROUP BY 子句
     -HAVING 子句
     -ORDER BY 子句
     -PARTITION BY 子句
     -在 CHECK 約束中
     -函數(shù)的參數(shù)
     -謂詞的參數(shù)
     -在其他表達(dá)式中(也包含 CASE 表達(dá)式本身)

我們來(lái)回顧一下本節(jié)要點(diǎn)。

01.在 GROUP BY 子句里使用 CASE 表達(dá)式,可以靈活地選擇聚合單位的編號(hào)或等級(jí)。這一點(diǎn)在進(jìn)行非定制化統(tǒng)計(jì)時(shí)能發(fā)揮巨大的威力。

02.在聚合函數(shù)中使用 CASE 表達(dá)式,可以輕松地將行結(jié)構(gòu)的數(shù)據(jù)轉(zhuǎn)換成列結(jié)構(gòu)的數(shù)據(jù)。

03.聚合函數(shù)也可以嵌套進(jìn) CASE 表達(dá)式里,因此可以在不使用 HAVING 子句的情況下匯總查詢。

04.相比依賴于具體數(shù)據(jù)庫(kù)的函數(shù),CASE 表達(dá)式擁有更強(qiáng)大的表達(dá)能力和更好的可移植性。

05.正因?yàn)?CASE 表達(dá)式是一種表達(dá)式而不是語(yǔ)句,才有了這諸多的優(yōu)點(diǎn)。

06.使用 CASE 表達(dá)式,可以將多條SQL語(yǔ)句匯總為一條,可讀性和性能都能得到提升。

如果想了解更多關(guān)于 CASE 表達(dá)式的內(nèi)容,請(qǐng)參考下頁(yè)的文獻(xiàn)資料。

● 塞爾科 .SQL權(quán)威指南(第 4 版)[M]. 王淵,鐘鳴,朱巍,譯 . 北京:人民郵電出版社,2013. 請(qǐng)參考 15.3.5 節(jié)“在 UPDATE 中使用 CASE 表達(dá)式”和 18.1 節(jié)“CASE 表達(dá)式”等。從 CASE 表達(dá)式的詳細(xì)用法到具體事例,這兩節(jié)都有細(xì)致的介紹。  

● 塞爾科 .SQL解惑(第 2 版)[M]. 米全喜,譯 . 北京:人民郵電出版社,2008. 關(guān)于在 CASE 表達(dá)式中嵌入聚合函數(shù),請(qǐng)參考“謎題 13 教師”“謎題 36 雙重職務(wù)”“謎題 43 畢業(yè)”。另外,“謎題 44 成對(duì)的款式”運(yùn)用了在 UPDATE 里進(jìn)行條件分支的技巧,“謎題 45 辣味香腸比薩餅”用 CASE 表達(dá)式巧妙地將行結(jié)構(gòu)的數(shù)據(jù)轉(zhuǎn)換成了列結(jié)構(gòu)的數(shù)據(jù)。

● 練習(xí)題 1-1-1:多列數(shù)據(jù)的最大值

用SQL從多行數(shù)據(jù)里選出最大值或最小值很容易——通過(guò) GROUP BY 子句對(duì)合適的列進(jìn)行聚合操作,并使用 MAXMIN 聚合函數(shù)就可以求出。那么,從多列數(shù)據(jù)里選出最大值該怎么做呢?

樣本數(shù)據(jù)如下表所示。

先思考一下從表里選出 xy 二者中較大的值的情況。此時(shí),求得的結(jié)果應(yīng)該如下所示。

執(zhí)行結(jié)果

key     greatest
-----   ---------
A              2
B              5
C              7
D              3

Oracle、PostgreSQL和 MySQL數(shù)據(jù)庫(kù)直接提供了可以實(shí)現(xiàn)這個(gè)需求的 GREATEST 函數(shù),但是這里請(qǐng)不要用這些函數(shù),而是用標(biāo)準(zhǔn)SQL的方法來(lái)實(shí)現(xiàn)。

求出 xy 二者中較大的值后,再試著將列數(shù)擴(kuò)展到 3 列以上吧。這次求的是 xyz 三者中的最大值,因此結(jié)果應(yīng)該如下所示。

執(zhí)行結(jié)果

key     greatest
-----   ---------
A              3
B              5
C              7
D              8

● 練習(xí)題 1-1-2:轉(zhuǎn)換行列——在表頭里加入?yún)R總和再揭8

8 “再揭”一詞常用于表示再次使用前述內(nèi)容,這里指的是在表格中以合計(jì)值的形式再次體現(xiàn)德島、香川、愛(ài)媛和高知這 4 個(gè)縣的數(shù)據(jù)。——譯者注

使用正文中的表 PopTbl2 作為樣本數(shù)據(jù),練習(xí)一下把行結(jié)構(gòu)的數(shù)據(jù)轉(zhuǎn)換為列結(jié)構(gòu)的數(shù)據(jù)吧。

這次請(qǐng)生成下面這樣的表頭里帶有匯總和再揭的二維表。

執(zhí)行結(jié)果

性別    全國(guó)      德島    香川     愛(ài)媛      高知  四國(guó)(再揭)
----   ------   -----   ------   ------   -------  ----------
1         855      60      100      100       100         360
2         845      40      100       50       100         290

“全國(guó)”列里是表 PopTbl2 中所有都道府縣(限于篇幅,還有一些都道府縣未列出)人口的合計(jì)值。另外,最右邊的“四國(guó)(再揭)”列里是四國(guó)地區(qū) 4 個(gè)縣的合計(jì)值。

● 練習(xí)題 1-1-3:用 ORDER BY 生成“排序”列

最后這個(gè)練習(xí)題用到的是比較小眾的技巧,但有時(shí)又必須使用它,所以我們也來(lái)看一下。

對(duì)練習(xí)題 1-1-1 里用過(guò)的表 Greatests 正常執(zhí)行 SELECT key FROM Greatests ORDER BY key; 這個(gè)查詢后,結(jié)果通常會(huì)按照 key 列的值的字母表順序顯示出來(lái)。

那么,請(qǐng)思考一個(gè)查詢語(yǔ)句,使得結(jié)果按照 B-A-D-C 這樣的指定順序進(jìn)行排列。這個(gè)順序并沒(méi)有什么具體的意義,大家也可以在實(shí)現(xiàn)完上述需求后,試著實(shí)現(xiàn)讓結(jié)果按照其他順序排列。

主站蜘蛛池模板: 呼玛县| 醴陵市| 淮安市| 增城市| 黄冈市| 兰溪市| 竹北市| 邢台市| 崇仁县| 镇赉县| 闵行区| 静宁县| 成武县| 扶风县| 无锡市| 丰都县| 克拉玛依市| 电白县| 和政县| 马山县| 诏安县| 育儿| 易门县| 龙川县| 龙海市| 镇巴县| 崇礼县| 冀州市| 兰坪| 赫章县| 广州市| 交口县| 阿拉尔市| 司法| 娱乐| 黄骅市| 栖霞市| 陇南市| 墨玉县| 巴中市| 昭平县|