SQL 中的 CASE 表達式妙用,漲姿勢了。。

前言

歷史考試選擇題:黃花崗起義第一槍誰開的? A宋教仁 B孫中山 C黃興 D徐錫麟,考生選C。

又看第二題:黃花崗起義第二槍誰開的? 考生傻了,就選了個B。

接着看第三題:黃花崗起義中,第三槍誰開的? 考生瘋了,胡亂選了A。

考試出來就去找出卷老師。老師拿出課本說:黃興連開三槍,揭開了黃花崗起義的序幕。考生:......

CASE表達式 之概念

相信大家都用過CASE表達式,尤其是做一些統計功能的時候,用的特別多,可真要說什麼是 CASE表達式,我估計還真沒幾個人能清楚的表述出來。

CASE表達式和 “2+1” 或者 “120/3” 這樣的表達式一樣,是一種進行運算的功能,正如CASE(情況)這個詞的含義一樣,用於區分情況,在有條件分歧的時候使用它。

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

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

基本格式如下

-- 簡單 CASE表達式
CASE 列(或表達式)
     WHEN <匹配值1> THEN <表達式>
     WHEN <匹配值2> THEN <表達式>
     ......
     ELSE <表達式>
END

-- 搜索 CASE表達式
CASE WHEN <判斷表達式> THEN <表達式>
     WHEN <判斷表達式> THEN <表達式>
     WHEN <判斷表達式> THEN <表達式>
     ......
     ELSE <表達式>
END

-- 簡單 CASE表達式 示例
CASE sex
    WHEN '1' THEN '男'
    WHEN '2' THEN '女'
    ELSE '其他'
END

-- 搜索CASE表達式 示例
CASE WHEN sex = '1' THEN '男'
     WHEN sex = '2' THEN '女'
     ELSE '其他'
END

CASE表達式 的 ELSE子句 可以省略,但推薦不要省略,省略了可能會出現我們意料之外的結果。END不能省,必須有。當 WHEN子句 爲真時,CASE表達式 的真假值判斷就會中止,而剩餘的 WHEN子句會被忽略。爲了避免引起不必要的混亂,使用 WHEN子句 時要注意條件的排他性。

簡單CASE表達式正如其名,寫法簡單,但能實現的功能比較有限。簡單CASE表達式能寫的條件,搜索CASE表達式也能寫,所以基本上採用搜索CASE表達式的寫法。

CASE表達式 之妙用

上面講了 CASE表達式 的理論知識,感覺不痛不癢,那麼接下來我們進入實戰篇,結合一些場景來看看 CASE表達式 的妙用

行轉列

可能我們用的更多的是 IF(MySQL)或 DECODE(Oracle),但這兩者都不是標準的 SQL,更推薦大家用 CASE表達式,移植性更高

假設我們有如下表,以及如下數據

CREATE TABLE t_customer_credit (
    id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
    login_name VARCHAR(50) NOT NULL COMMENT '登錄名',
    credit_type TINYINT(1) NOT NULL COMMENT '額度類型,1:自由資金,2:凍結資金,3:優惠',
    amount DECIMAL(22,6) NOT NULL DEFAULT '0.00000' COMMENT '額度值',
    create_by VARCHAR(50) NOT NULL COMMENT '創建者',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創建時間',
    update_by VARCHAR(50) NOT NULL COMMENT '修改者',
  PRIMARY KEY (id)
);
INSERT INTO `t_customer_credit` VALUES (1, 'zhangsan', 1, 550.000000, 'system', '2019-7-7 11:30:09', '2019-7-8 20:21:05', 'system');
INSERT INTO `t_customer_credit` VALUES (2, 'zhangsan', 2, 0.000000, 'system', '2019-7-7 11:30:09', '2019-7-7 11:30:09', 'system');
INSERT INTO `t_customer_credit` VALUES (3, 'zhangsan', 3, 0.000000, 'system', '2019-7-7 11:30:09', '2019-7-7 11:30:09', 'system');
INSERT INTO `t_customer_credit` VALUES (4, 'lisi', 1, 0.000000, 'system', '2019-7-7 11:30:09', '2019-7-7 11:30:09', 'system');
INSERT INTO `t_customer_credit` VALUES (5, 'lisi', 2, 0.000000, 'system', '2019-7-7 11:30:09', '2019-7-7 11:30:09', 'system');
INSERT INTO `t_customer_credit` VALUES (6, 'lisi', 3, 0.000000, 'system', '2019-7-7 11:30:09', '2019-7-7 11:30:09', 'system');

如果我們要一行顯示用戶的三個額度,而不是 3 條記錄顯示 3 個額度,我們應該怎麼做,方式有很多種,這裏提供如下 3 種

-- 1、最容易想到的IF,不具備移植性,不推薦
SELECT login_name,
    MAX(IF(credit_type=1, amount, 0)) freeAmount,
    MAX(IF(credit_type=2, amount, 0)) freezeAmount,
    MAX(IF(credit_type=3, amount, 0)) promotionAmount
FROM t_customer_credit GROUP BY login_name;

-- 2、CASE表達式,標準的 SQL 規範,具備移植性,推薦使用
SELECT login_name,
    MAX(CASE WHEN credit_type = 1 THEN amount ELSE 0 END) freeAmount,
    MAX(CASE WHEN credit_type = 2 THEN amount ELSE 0 END) freezeAmount,
    MAX(CASE WHEN credit_type = 3 THEN amount ELSE 0 END) promotionAmount
FROM t_customer_credit GROUP BY login_name;

-- 3、自連接,數據量大的情況下,結合索引,效率不錯,具備移植性
SELECT
    a.login_name,a.amount freeAmount,
    b.amount freezeAmount,
    c.amount promotionAmount
FROM (
    SELECT login_name, amount FROM t_customer_credit WHERE credit_type = 1
)a
LEFT JOIN t_customer_credit b ON a.login_name = b.login_name AND b.credit_type = 2
LEFT JOIN t_customer_credit c ON a.login_name = c.login_name AND c.credit_type = 3;

無論是 IF 還是 CASE表達式,都結合了 GROUP BY 與聚合函數,效率是個問題,而自連接是效率最高的,不管在不在 login_name 上加索引

轉換統計

將已有編號方式轉換爲新的方式並統計,在進行非定製化統計時,我們經常會遇到將已有編號方式轉換爲另外一種便於分析的方式並進行統計的需求。假設我們有如下表

DROP TABLE t_province_population;
CREATE TABLE t_province_population (
  id tinyint(2) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  province_name varchar(50) NOT NULL COMMENT '省份名',
  sex tinyint(1) NOT NULL COMMENT '性別,1:男,2:女',
  population int(11) NOT NULL COMMENT '人口數',
  PRIMARY KEY (id)
);

INSERT INTO t_province_population(province_name,sex,population)
VALUES
("黑龍江", 1 ,20),
("黑龍江", 2 ,18),
("內蒙古", 1 ,7),
("內蒙古", 2 ,8),
("海南", 1 ,20),
("海南", 2 ,22),
("西藏", 1 ,8),
("西藏", 2 ,7),
("浙江", 1 ,35),
("浙江", 2 ,35),
("臺灣", 1 ,26),
("臺灣", 2 ,23),
("河南", 1 ,40),
("河南", 2 ,38),
("湖北", 1 ,27),
("湖北", 2 ,24);

SELECT * FROM t_province_population;

我們需要按各個省所在的位置,統計出東南西北中,各個區域內的人口數量

東:浙江、臺灣,西:西藏,南:海南,北:黑龍江、內蒙古,中:湖北、河南

可能有人覺得這個表設計的不合理,應該在設計之初就應該多加一個區域字段(district)來標明各省所屬區域。最好的做法確實是這樣,但這得需要我們在設計之初的時候能考慮得到,或者有這樣的需求,假設我們設計之初沒有這樣的需求,而我們也沒考慮到,那麼有沒有什麼辦法來實現了?我們可以這樣來寫 SQL

-- 通用寫法,適用於多種數據庫
SELECT CASE province_name
    WHEN '浙江' THEN '東'
    WHEN '臺灣' THEN '東'
    WHEN '海南' THEN '南'
    WHEN '西藏' THEN '西'
    WHEN '黑龍江' THEN '北'
    WHEN '內蒙古' THEN '北'
    WHEN '河南' THEN '中'
    WHEN '湖北' THEN '種'
    ELSE '其他' END district,
    SUM(population) populations
FROM t_province_population
GROUP BY CASE province_name
    WHEN '浙江' THEN '東'
    WHEN '臺灣' THEN '東'
    WHEN '海南' THEN '南'
    WHEN '西藏' THEN '西'
    WHEN '黑龍江' THEN '北'
    WHEN '內蒙古' THEN '北'
    WHEN '河南' THEN '中'
    WHEN '湖北' THEN '中'
    ELSE '其他' END;

-- MySQL支持寫法,移植性差
SELECT CASE province_name
    WHEN '浙江' THEN '東'
    WHEN '臺灣' THEN '東'
    WHEN '海南' THEN '南'
    WHEN '西藏' THEN '西'
    WHEN '黑龍江' THEN '北'
    WHEN '內蒙古' THEN '北'
    WHEN '河南' THEN '中'
    WHEN '湖北' THEN '中'
    ELSE '其他' END district,
    SUM(population) populations
FROM t_province_population
GROUP BY district;

結果如下

假設我們需要對各個省份做一個人口數級別的統計,統計出各個級別的數量

  • level_1:population < 20
  • level_2:20 <= population < 50
  • level_3:50 <= population < 70
  • level_4:>= 70

統計出 level_1 ~ level_4 的數量各有多少

SQL 與執行結果如下

SELECT
    CASE WHEN population < 20 THEN 'level_1'
        WHEN population >= 20 AND population < 50 THEN 'level_2'
        WHEN population >= 50 AND population < 70 THEN 'level_3'
        WHEN population >= 70 THEN 'level_4'
        ELSE NULL
    END pop_level,
    COUNT(*) cnt
FROM (
    SELECT province_name,SUM(population) population FROM t_province_population GROUP BY province_name
)a
GROUP BY
    CASE WHEN population < 20 THEN 'level_1'
        WHEN population >= 20 AND population < 50 THEN 'level_2'
        WHEN population >= 50 AND population < 70 THEN 'level_3'
        WHEN population >= 70 THEN 'level_4'
        ELSE NULL
    END;

這種轉換統計還是比較常用的,重點就是 GROUP BY 子句的寫法。

條件分支

SELECT 條件分支

還是以上面的 t_province_population 爲例,如果我們想要直觀的知道各個省份的男、女數量情況,類似如下

我們要怎麼寫 SQL?有如下兩種方法

-- 1、CASE表達式 集合 GROUP BY
SELECT province_name,
    SUM(CASE WHEN sex = 1 THEN population ELSE 0 END) c,
    SUM(CASE WHEN sex = 2 THEN population ELSE 0 END) f_pops
FROM t_province_population
GROUP BY province_name;

-- 2、自關聯
SELECT t.province_name, t.population m_pops, a.population f_pops
FROM t_province_population t
LEFT JOIN t_province_population a
ON t.province_name = a.province_name
WHERE t.sex = 1 AND a.sex = 2;

其實就是行轉列,行轉列更容易懂

UPDATE 條件分支

我們有一張薪資表,如下

CREATE TABLE t_user_salaries(
  id int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  name varchar(50) NOT NULL COMMENT '姓名',
    sex tinyint(1) NOT NULL COMMENT '性別,1:男,2:女',
  salary int(11) NOT NULL COMMENT '薪資',
  PRIMARY KEY (id)
);

INSERT INTO t_user_salaries(name, sex,salary) VALUES
("張三", 1, 30000),
("李四", 1, 27000),
("王五", 1, 22000),
("菲菲", 2, 24000),
("趙六", 1, 29000);

SELECT * FROM t_user_salaries;

假設現在需要根據以下條件對該表的數據進行更新:1、對當前工資爲 30000 元以上的員工,降薪 10%,2、對當前工資爲 25000 元以上且不滿 28000 元的員工,加薪 20%。調整之後的薪資如下所示

乍一看,分別執行下面兩個 UPDATE 操作好像就可以做到,但是我們執行下看看結果

-- 條件1
UPDATE t_user_salaries
SET salary = salary * 0.9
WHERE salary >= 30000;

-- 條件2
UPDATE t_user_salaries
SET salary = salary * 1.2
WHERE salary >= 25000 AND salary < 28000;

我們發現張三的薪資不降反升了!這是因爲執行 條件1的SQL後,張三的薪資又滿足條件2了,所以又更新了一遍,導致他的薪資變多了,有人可能會說,把條件1和條件2的SQL換下順序不就好了嗎,我們來試試

-- 條件2
UPDATE t_user_salaries
SET salary = salary * 1.2
WHERE salary >= 25000 AND salary < 28000;

-- 條件1
UPDATE t_user_salaries
SET salary = salary * 0.9
WHERE salary >= 30000;

張三的薪資是降對了,可李四的薪資卻漲錯了!這是因爲李四的薪資滿足條件2,升了 20% 之後又滿足條件1,又降了 10%。難道就沒有就沒有正確的方式了?我們來看看這個 SQL

UPDATE t_user_salaries SET salary =
    CASE WHEN salary >= 30000 THEN salary * 0.9
            WHEN salary >= 25000 AND salary < 28000 THEN salary * 1.2
            ELSE salary
    END;

SELECT * FROM t_user_salaries;

完美不?特別完美,這個技巧的應用範圍很廣,值得我們掌握

CHECK 約束

注意:CHECK 是標準的 SQL,但是 MySQL 卻沒有實現它,所以 CHECK 在 MySQL 中是不起作用的!

回到我們的薪資表,假設某個公司有這樣一個無理的規定:女性員工的工資不得高於50000,我們如果實現它? 方式有兩種:1、代碼層面控制 、2、數據庫表加約束。

代碼層面控制就不多說了,這我們平時最能想到的,實際也是用的最多的;那從表約束,我們該如何實現了,像這樣嗎?

-- 創建表的時候增加約束
CREATE TABLE t_user_salaries_check(
  name varchar(50) NOT NULL COMMENT '姓名',
    sex tinyint(1) NOT NULL COMMENT '性別,1:男,2:女',
  salary int(11) NOT NULL COMMENT '薪資',
    CONSTRAINT chk_sex_salary CHECK (sex=2 AND salary <= 50000)
);

-- 若t_user_salaries_check已創建,則補充上約束
ALTER TABLE t_user_salaries_check
ADD CONSTRAINT chk_sex_salary CHECK (sex=2 AND salary <= 50000);

這麼實現你會發現公司的男同事都會提着刀來找你了,因爲沒有他們的薪資,這個約束會導致錄入不了男性的薪資!因爲我們的約束是:sex=2 AND salary < = 50000 表示 “是女性,並且薪資不能高於50000”,而不是:“如果是女性,薪資不高於50000”。正確的約束條件應該這麼寫

-- 創建表的時候增加約束
CREATE TABLE t_user_salaries_check(
  name varchar(50) NOT NULL COMMENT '姓名',
    sex tinyint(1) NOT NULL COMMENT '性別,1:男,2:女',
  salary int(11) NOT NULL COMMENT '薪資',
  PRIMARY KEY (id),
    CONSTRAINT chk_sex_salary CHECK(
        CASE WHEN sex = 2 THEN
                        CASE WHEN salary <= 50000 THEN 1
                                ELSE 0
                        END
                ELSE 1
        END = 1 )
);

-- 若t_user_salaries_check已創建,則補充上約束
ALTER TABLE t_user_salaries_check
ADD CONSTRAINT chk_sex_salary CHECK(
    CASE WHEN sex = 2 THEN
                        CASE WHEN salary <= 50000 THEN 1
                                ELSE 0
                        END
                ELSE 1
        END = 1
);

CASE表達式還有很多其他的用處,強大的不得了,而且高度靈活;用好它,能讓我們寫出更加契合的 SQL。

總結

1、CASE表達式 是支撐 SQL 聲明式編程的根基之一,也是靈活運用 SQL 時不可或缺的基礎技能。作爲表達式,CASE 表達式在執行時會被判定爲一個固定值,因此它可以寫在聚合函數內部;也正因爲它是表達式,所以還可以寫在SELECE 子句、GROUP BY 子句、WHERE 子句、ORDER BY 子句裏。簡單點說,在能寫列名和常量的地方,通常都可以寫 CASE 表達式

2、寫 CASE表達式 的注意點

a、各個分支返回的數據類型要一致

b、養成寫 ELSE 的好習慣

c、不要忘了寫 END

來源:cnblogs.com/youzhibing/p/11240536.html

更多文章推薦:

1.Spring Boot 3.x 教程,太全了!

2.2,000+ 道 Java面試題及答案整理(2024最新版)

3.免費獲取 IDEA 激活碼的 7 種方式(2024最新版)

覺得不錯,別忘了隨手點贊+轉發哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章