楔子
到目前為止,我們的查詢都是從單個表中獲取數據。下面我們開始探討一下如何從多個表中獲取相關的數據。因為在關系數據庫中,通常將不同的信息和它們之間的聯系存儲到多個表中。比如產品表、用戶表、用戶訂單表、以及關聯的訂單明細表等。當我們想要查看某個訂單時,需要同時從這幾個表中查找關於訂單的全部信息。
除了連接查詢,SQL 還提供了另一種同時查詢多個表的方法:子查詢(Subquery)。本節我們就來了解一下各種類型的子查詢和相關的運算符。
假設我們要計算年齡大於平均年齡的員工:
-- 一種笨辦法就是先計算年齡的平均值,然后拿到這個平均值再去查詢
SELECT AVG(age) FROM staff; -- 37.0000
SELECT * FROM staff WHERE age > 37;
我們使用了兩個查詢來解決這個簡單的問題,然而實際應用中的需求往往更加復雜;顯然我們需要更加高級的查詢功能。
SQL 提供了一種查詢方式叫做子查詢,可以非常容易地解決這個問題:
SELECT * FROM staff WHERE age > (SELECT AVG(age) FROM staff);
簡單來說,子查詢是指嵌套在其他語句(SELECT、INSERT、UPDATE、DELETE 等)中的 SELECT 語句;子查詢也稱為內查詢(inner query)或者嵌套查詢(nested query);子查詢必須位於括號之中。
SQL 中的子查詢可以分為以下三種類型:
- 標量子查詢(Scalar Subquery):返回單個值(一行一列)的子查詢。上面的示例就是一個標量子查詢。
- 行子查詢(Row Subquery):返回單行結果(一行多列)的子查詢,標量子查詢是行子查詢的一個特例。
- 標表子查詢(Table Subquery):返回一個虛擬表(多行多列)的子查詢,行子查詢是表子查詢的一個特例。
標量子查詢
標量子查詢的結果就像一個常量一樣,可以用於 SELECT、WHERE、GROUP BY、HAVING 以及 ORDER BY 等子句中。我們計算一下員工的年齡和平均年齡之差。
SELECT id, age, CAST(age - (SELECT AVG(age) FROM staff) AS SIGNED) AS age1
FROM staff
LIMIT 10
/*
01010011868 25 -12
01010010306 32 -5
01010001867 44 7
01010007780 39 2
01010002647 33 -4
01010002350 30 -7
01010001563 34 -3
01010010898 27 -10
01010003113 47 10
01010007477 39 2
*/
估計有人會這么干:
SELECT id, age, CAST((age - AVG(age)) AS SIGNED) AS age1
FROM staff
LIMIT 10
-- 直接用 age - avg(age),這樣寫雖然人很容易理解
-- 但是不好意思,這樣寫SQL不允許,因為一旦出現了聚合函數
-- 那么 SELECT 后面的字段要么出現在聚合函數中,要么出現在 GROUP BY 字句中
同理如果尋找年齡第二大的,我們可以這么做:
SELECT MAX(age) FROM staff
WHERE age < (SELECT MAX(age) FROM staff); -- 60
-- 先把最大的age選出來,然后讓age小於這個最大值,然后在剩余的記錄中再選擇最大值
-- 得到的不就是第二大的了嗎
-- 但是這要求,最大值不能有重復,假設最大值是50,但是有兩個50,這樣的話選擇就是第三大的了
-- 但是不管怎樣,我們肯定不可以這么寫
SELECT MAX(age) FROM staff
WHERE age < MAX(age);
-- 這樣寫是錯的,先不說 MAX(age)中的 age 有可能是其它表中的 age
-- 即使是一張表的 age,也不可以這么寫。
-- 因為 WHERE 是表過濾,WHERE 邏輯里面不能包含聚合,如果包含,那么聚合一定是子查詢里面的聚合。
行子查詢
行子查詢可以當作一個一行多列的臨時表使用,顧名思義就是返回一行。
我們以之前的 girl_info 和 girl_score 為例:
-- 選擇 id 出現在 girl_score 中的 girl_info 表的記錄
SELECT * FROM girl_info
WHERE id IN (SELECT id FROM girl_score);
/*
1002 古明地戀 15
1003 椎名真白 17
1004 芙蘭朵露 400
1005 霧雨魔理沙
1006 坂上智代 19
1001 古明地覺 16
1001 古明地覺 21
*/
-- 當然我們這里是全部記錄
表子查詢
當子查詢返回的結果包含多行、多列的時候,成為表子查詢,表子查詢通常用於查詢條件或者FROM 子句中。
SELECT * FROM (SELECT id, score FROM girl_score WHERE id > 1002) AS g;
/*
1003 95
1004 81
1005 100
1006 86
*/
我們這里把子查詢返回的結果直接當成一張表來用,當然標量子查詢、行子查詢也是可以這么做的,跟在from的后面充當一張表的作用。另外,如果是這么做的話,那么必須要起一個別名。
小心陷阱
對於 WHERE 中的子查詢,需要注意返回的數據要進行匹配。比如:
-- 這個語句就是不合法的,因為id=的后面需要跟一個標量,會返回girl_info的id字段中和這個標量相等的值所以對應的記錄
-- 而我們返回的是多條數據,所以不匹配
SELECT * FROM girl_info
WHERE id = (SELECT id FROM girl_score);
-- 這樣也是不合法的,因為還是返回了多條
SELECT *
FROM girl_info
WHERE id = (SELECT id FROM girl_score WHERE id != 1002);
-- 合法
SELECT *
FROM girl_info
WHERE id = (SELECT id FROM girl_score WHERE id != 1002 AND id != 1001 AND id != 1003 AND id != 1004 AND id != 1005);
/*
這樣是合法的,因為我們只保留了一條數據,所以返回的是標量
*/
-- 一般這種情況,我們會使用in,in后面需要跟一列
-- 把 = 改成in是沒問題的
SELECT * FROM girl_info
WHERE id IN (SELECT id FROM girl_score);
-- 但是,下面的語句也是可以的
SELECT *
FROM girl_info
WHERE id IN (SELECT id FROM girl_score WHERE id != 1002 AND id != 1001 AND id != 1003 AND id != 1004 AND id != 1005);
-- 我們說,雖然只返回了一條數據,但是它即可以看成是標量,也可以看成行,只不過這個行只有一列數據
-- 同理,如下是不合法的
SELECT * FROM girl_info
WHERE id IN (SELECT id, age FROM girl_score);
/*
提示我們,子查詢有太多的字段,前面的id是一個字段,但是我們子查詢返回了兩個
*/
因此在涉及子查詢的時候,要小心,可以自己下去多嘗試一下。
子查詢返回的內容可以在很多地方使用,只要遵循之前的語法規范,比如 JOIN 是連接表,而子查詢返回的內容也可以看成是一張表,那么它就可以跟在 JOIN 后面,只是當它作為表的時候需要起一個別名。
再比如子查詢返回的內容可以看成是一個標量,那么標量能在什么地方用,子查詢范返回的結果也可以在什么地方用,前提返回的得是一個標量。同理對於行、表也是一樣的,根據返回的內容判斷子查詢是什么身份,該身份能在什么地方使用,那么子查詢返回的結果就可以在什么地方使用。所以子查詢能作用的返回很廣,也正因為如此,才可以用SQL做更多的事情,如果不支持子查詢,可以說,SQL 算是 "沒了兩條腿"。也正因為如此,SQL學好了也是很厲害的,因為表之間層層連接、子查詢之間層層嵌套的邏輯也不是那么簡單的。
子查詢和普通查詢本質上沒什么兩樣,所以里面也可以嵌套 GROUP BY、JOIN、LIMIT 等邏輯。
ALL、ANY/SOME 運算符:
ALL 運算符一般與比較運算符(=、!=、<、<=、>、>=)結合,表示等於、不等於、小於、小於等於、大於或者大於等於子查詢結果中的所有值。
SELECT * FROM girl_info
WHERE id >= all(SELECT id FROM girl_score); -- 1006 坂上智代 19
-- 因為girl_score中id的最大值為1006,girl_info中id的最大值也為1006,所以只有id=1006的記錄返回
-- all要求必須和子查詢中所有值都滿足相應的關系,所以這里是選擇girl_info中的id 大於等於 girl_score中的所有id的記錄
ANY/SOME 運算符與比較運算符(=、!=、<、<=、>、>=)結合表示等於、不等於、小於、小於等於、大於或者大於等於子查詢結果中的某個值即可。
SELECT * FROM girl_info
WHERE id > any(SELECT id FROM girl_score);
/*
1002 古明地戀 15
1003 椎名真白 17
1004 芙蘭朵露 400
1005 霧雨魔理沙
1006 坂上智代 19
*/
-- girl_score中id最小的值為1001,因為girl_info中,只要id大於1001就滿足條件
-- some和any一樣,不再演示
EXISTS 操作符:
EXISTS 操作符用於判斷子查詢結果的存在性。如果子查詢存在任何結果,EXISTS 返回真;否則,返回假。
我們將 girl_info 表修改一下,為了能看到效果:
1001 古明地覺 16
1002 古明地戀 15
1003 椎名真白 17
10040 芙蘭朵露 400
10050 霧雨魔理沙
1006 坂上智代 19
1001 古明地覺 16
我們將1004和1005后面加上了一個0
girl_score表不變
SELECT *
FROM girl_info AS gi
WHERE EXISTS(SELECT 1
FROM girl_score AS gs
WHERE gi.id = gs.id)
/*
1002 古明地戀 15
1003 椎名真白 17
1006 坂上智代 19
1001 古明地覺 16
1001 古明地覺 21
*/
我們來分析一下邏輯,我們exists只是判斷子查詢有沒有返回內容,至於返回的內容是什么不關心,只要返回了東西即可。
我們先執行外部查詢,找到 gi.id,然后傳遞給子查詢,一旦在 gs 中找到個 gi.id 相等的 id,那么就會返回。我們返回的是1,我們說返回的是什么不重要,重要的是有沒有返回。而一旦返回了,那么 EXISTS 函數的執行結果便為 true,那么 gi 的這條記錄就是符合的。而 10040 和 10050 在 gs 的 id 字段中不存在,所以 EXISTS 函數執行結果為 false。所以這個和我們之前的 JOIN、也就是內連接之間沒有什么區別,只不過我們用 EXISTS 和 子查詢 重新實現了。
另外我們看到子查詢當中也是可以使用外部查詢的表的,比如我們這里的子查詢使用了外部查詢的 girl_info 表。
NOT EXISTS 執行相反的操作。
現在,我們知道 [NOT] EXISTS 和 [NOT] IN 都可以用於判斷子查詢返回的結果。但是它們之間存在一個重要的區別:[NOT] EXISTS 只檢查存在性,[NOT] IN 需要比較實際的值是否相等。因此,當子查詢的結果包含 NULL 值時,EXISTS 仍然返回結果,NOT EXISTS 不返回結果;但是此時 IN 和 NOT IN 都不會返回結果,因為 (X = NULL) 和 NOT (X = NULL) 的結果都是未知。其實說白了,因為 EXISTS 前面不需要加字段,不會進行比較,只能判斷肚子里面的子查詢是否返回了東西。
我們還可以在子查詢中包含其他的子查詢,實現嵌套子查詢。我們之前說過了。
小結
子查詢語句為我們提供了在一個查詢中訪問多個表的另一種方式,很多時候可以實現與連接查詢相同的效果。本篇我們討論了各種形式的子查詢,包括相關的操作符和注意事項。