表的集合運算
上一節我們介紹了 SQL 中各種形式的子查詢,以及與子查詢相關的 IN、ALL、ANY/SOME、EXISTS 運算符。
我們已經學習了兩種涉及多個表的查詢語句,今天我們來討論另一種從多個查詢中返回組合結果的方法:集合運算。
集合運算:
數據庫中的表與集合理論中的集合非常類似,表是由行組成的集合。因此, SQL 支持基於行的各種集合操作:並集運算(UNION)、交集運算(INTERSECT)和差集運算(EXCEPT)。它們都用於將兩個查詢的結果集合並成一個結果集,但是合並的規則各不相同。
需要注意的是,SQL 集合操作中的兩個查詢結果需要滿足以下條件:
- 結果集中字段的數量和順序必須相同;
- 結果集中對應字段的類型必須匹配或兼容。
也就是說,對於參與運算的兩個查詢結果,要求它們的字段結構相同。如果一個查詢返回 2 個字段,另一個查詢返回 3 個字段,肯定無法合並。如果一個查詢返回數字類型的字段,另一個查詢返回字符類型的字段,通常也無法合並;不過數據庫可能會嘗試執行隱式的類型轉換。
交集運算:
INTERSECT 操作符用於返回兩個查詢結果中的共同部分,即同時出現在第一個查詢結果和第二個查詢結果中的數據,並且對最終結果進行了去重操作。交集運算的示意圖如下:
SELECT *
FROM girl_info
WHERE id IN (1002, 1003, 1004)
INTERSECT
SELECT *
FROM girl_info
WHERE id IN (1002, 1003)
/*
1002 古明地戀 15
1003 椎名真白 17
*/
並集運算:
UNION 操作符用於將兩個查詢結果相加,返回出現在第一個查詢結果或者第二個查詢結果中的數據。並集運算的示意圖如下:
SELECT *
FROM girl_info
WHERE id IN (1002, 1003, 10040)
UNION
SELECT *
FROM girl_info
WHERE id IN (1002, 1003)
/*
1002 古明地戀 15
10040 芙蘭朵露 400
1003 椎名真白 17
*/
但我們發現這個結果是去重了的,還有 UNION 等同於 UNION DISTINCT,除此之外還有 UNION ALL,用於返回所有記錄,也就是不去重。
SELECT *
FROM girl_info
WHERE id IN (1002, 1003, 10040)
UNION ALL
SELECT *
FROM girl_info
WHERE id IN (1002, 1003)
/*
1002 古明地戀 15
1003 椎名真白 17
10040 芙蘭朵露 400
1002 古明地戀 15
1003 椎名真白 17
*/
差集運算:
EXCEPT 操作符用於返回出現在第一個查詢結果中,但不在第二個查詢結果中的記錄,並且對最終結果進行了去重操作。差集運算的示意圖如下:
SELECT *
FROM girl_info
WHERE id IN (1002, 1003, 10040)
EXCEPT
SELECT *
FROM girl_info
WHERE id IN (1002, 1003)
/*
10040 芙蘭朵露 400
*/
Oracle 使用關鍵字 MINUS 表示差集運算,MySQL 不支持差集運算。
集合操作中的排序:
如果要對集合操作的結果進行排序,需要將 ORDER BY 子句寫在最后;集合操作符之前的查詢語句中不能出現排序操作。以下是一個錯誤的語法示例:
SELECT *
FROM girl_info
WHERE id IN (1002, 1003, 10040)
ORDER BY age
UNION
SELECT *
FROM girl_info
WHERE id IN (1002, 1003);
-- 我們union的時候就寫了排序,不單單是因為union之前排序跟union之后是否有序沒有任何關系
-- 而是這么寫壓根就是錯誤的語法,集合操作的兩端不能出現order by關鍵字
SELECT *
FROM girl_info
WHERE id IN (1002, 1003, 10040)
UNION
SELECT *
FROM girl_info
WHERE id IN (1002, 1003);
ORDER BY age; -- 這么寫是正確的
/*
1002 古明地戀 15
1003 椎名真白 17
10040 芙蘭朵露 400
*/
-- 此時的 ORDER BY 不是對下面的 SELECT 查詢結果進行排序,而是對整個 UNION 之后的結果進行排序
-- 但是在 UNION 上面的 SELECT 語句后面使用 ORDER BY 進行排序的話,則肯定是不符合語法規范的
另外除了 ORDER BY 子句的位置,還有一個常見的問題就是集合操作符的優先級。
集合操作符的優先級:
SQL 提供了 3 種集合操作符:UNION [ALL]、INTERSECT 以及 EXCEPT。我們可以通過多個集合操作符將多個查詢的結果進行組合。此時,需要注意它們之間的優先級和執行順序:
- INTERSECT 的優先級高於 UNION 和 EXCEPT,但是 Oracle 中所有集合操作符的優先級相同;
- 相同的集合操作符按照從左至右的順序執行;
- 使用括號可以明確指定執行的順序。
因此很多操作符,包括編程語言也是,盡量還是使用括號進行限定,盡管不加括號,根據優先級,結果也是正確的,但是如果多了的話,還是要加上。一方是為了避免錯誤,另一方面則是更加的直觀。
WITH 語句把表變成一個變量
上一節我們討論了如何利用 SQL 集合運算符(UNION [ALL]、INTERSECT 以及 EXCEPT)將多個查詢結果合並成一個結果。
接下來我們介紹 SQL 中一個非常強大的功能:通用表表達式(Common Table Expression)。
表即變量:
在編程語言中,通常會定義一些變量和函數(方法);變量可以被重復使用,函數(方法)可以將代碼模塊化並且提高程序的可讀性與可維護性。
與此類似,SQL 中的通用表表達式能夠實現查詢結果的重復利用,簡化復雜的連接查詢和子查詢;並且可以完成數據的遞歸處理和層次遍歷。
舉個例子:
SELECT * FROM girl_info
WHERE id in (SELECT id FROM girl_score WHERE id > 1001);
/*
1002 古明地戀 15
1003 椎名真白 17
1006 坂上智代 19
*/
這個是我們之前的例子,我們可以使用with語句,將子查詢的結果變成一張臨時表。
WITH tmp AS (
SELECT id FROM girl_score WHERE id > 1001
)
SELECT * FROM girl_info
WHERE id IN (SELECT * FROM tmp);
/*
1002 古明地戀 15
1003 椎名真白 17
1006 坂上智代 19
*/
我們將子查詢的邏輯寫在了 WITH 里面,把子查詢返回的內容賦給了變量 tmp。此時 tmp 就相當於是一張表,但是它在數據庫中並不真實存在,是一張臨時表,但我們是可以當成普通的表來使用的,tmp表的內容就是里面語句返回的結果,那么我們直接對 tmp 進行 SELECT * 即可。
專業一點的話就是,WITH 關鍵字用於定義通用表表達式(cte);它實際上是定義了一個臨時結果集(表),名稱為 tmp;AS 關鍵字指定了 tmp 的結構和數據。
由於我們這里的示例比較簡單,所以沒啥區別,但如果子查詢一多、或者發生了嵌套,WITH 語句就很有用了。
另外 WITH 語句中不要出現分號,因為出現了分號就表示結束了,而我們的 WITH 顯然和下面的 SELECT 是分不開了,所以在 WITH xxx AS () 的后面不要出現分號。另外,with也可以同時創建多張臨時表。
-- 多個臨時表之間使用逗號分隔,tmp1和tmp2就相當於普通的表
-- 可以進行union、join等等
WITH tmp1 AS (
SELECT id FROM girl_score WHERE id > 1001
), tmp2 as (
SELECT id FROM girl_score WHERE id > 1001
)
SELECT * FROM tmp1
UNION ALL
SELECT * FROM tmp2
;
/*
1002
1003
1004
1005
1006
1002
1002
1003
1004
1005
1006
1002
*/
-- 獨立的語句之間若想一次性全部執行,需要使用分隔進行分隔
-- 因為獨立的語句之間沒有關系,需要使用分號標志結束,才能執行下一行語句
-- 而一旦 WITH 下面的語句結束,那么 WITH 創建的臨時表的生命周期也結束了
-- 也就是說,接下來我們是無法使用 tmp1 和 tmp2 的,因為它們已經不存在了
WITH 子句相當於定義了一個變量,變量的值是一個表,所以稱為通用表表達式。CTE 和子查詢類似,可以用於 SELECT、INSERT、UPDATE 以及 DELETE 語句中。Oracle 中稱之為子查詢因子(subquery factoring)
因此普通的通用表表達式可以將 SQL 語句進行模塊化,便於閱讀和理解;而遞歸形式的通用表表達式可以非常方便地遍歷具有層次結構或者樹狀結構的數據,例如組織結構遍歷和航班中轉信息查詢。
下面來看看遞歸查詢。
遞歸查詢:
通用表表達式支持在定義中調用自己,也就是實現編程中的遞歸調用。接下來我們就介紹一些常用的遞歸 CTE 案例。
以下是一個簡單的遞歸查詢示例,該語句用於生成一個 1 到 10 的數字序列:
-- MySQL 以及 PostgreSQL需要加上 recursive 才表示遞歸
-- 而Oracle 以及 SQL Server不需要recursive,直接還是使用with即可
WITH RECURSIVE recursion(n) AS (
SELECT 1
UNION all
SELECT n + 1 FROM recursion WHERE n < 10
)
SELECT * FROM recursion;
/*
1
2
3
4
5
6
7
8
9
10
*/
我們這里不再只使用 WITH,而是使用 WITH RECURSIVE,這樣后面定義的 recursion 臨時表才是可以遞歸的,我們下面才能 FROM recursion。但是我們注意到,我們定義的時候還加上了一個參數。不加參數的話,里面的內容是從其它地方過來的,但是我們這里是 FROM recursion,也就是說我們定義了一張表 recursion,然后這張表的內容還是從 recurion 里面獲取,而我們的數據是 n 取不同的值進行 UNION 得到的,顯然這個 n 得有地方接收,那么接收的位置顯然就是參數了。而這個 n 則是自動生成的,作為參數的它初始值的時候為 1。
- 運行初始化語句,生成數字 1;
- 第 1 次運行遞歸部分,此時 n 等於 1,返回數字 2( n+1 );
- 第 2 次運行遞歸部分,此時 n 等於 2,返回數字 3( n+1 );
- 第 9 次運行遞歸部分,此時 n 等於 9,返回數字 10( n+1 );
- 第 10 次運行遞歸部分,此時 n 等於 10;由於查詢不滿足條件( WHERE n < 10 ),不返回任何結果,並且遞歸結束;
- 最后的查詢語句返回 t 中的全部數據,也就是一個 1 到 10 的數字序列。
顯然,遞歸 CTE 非常合適用於生成具有某種規律的數字序列,例如斐波那契數列(Fibonacci series)。
斐波那契數列指的是這樣一個數列:0、1、1、2、3、5、8、13、...。從數字 0 和 1 開始,每個數字都等於它前面的兩個數字之和。如果遞歸查詢中的每一行都基於前面的兩個數值求和,就能生成一個斐波那契數列:
WITH RECURSIVE fibonacci (n, fib_n, next_fib_n) AS (
SELECT 1, 0, 1
UNION ALL
SELECT n + 1, next_fib_n, fib_n + next_fib_n
FROM fibonacci
WHERE n < 10
)
SELECT *
FROM fibonacci;
n,fib_n,next_fib_n,三者初始值均為1。
該語句的執行過程如下:
- 初始化第一個斐波那契數列值。字段 fib_n 表示第 n 個斐波那契數列值,第 1 個值為 0;字段 next_fib_n 表示下一個斐波那契數列值,第 2 個數列值為 1;
- 第一次運行遞歸部分,字段 n 等於 2(1 + 1);字段 fib_n 的值為 1(上一次的 next_fib_n);字段 next_fib_n 的值為 1(0 + 1);
- 繼續執行遞歸部分,字段 n 加上 1;使用上一條記錄中的 next_fib_n 作為此次的斐波那契數列值,並且使用 fib_n + next_fib_n 作為下一個斐波那契數列值;
- 不斷迭代該過程,當 n 到達 10 之后結束遞歸過程;
- 最后的查詢語句返回所有的數列值。
關於這里的遞歸,個人覺得不是很重要,了解一下即可。但是有時候遞歸卻有很重要,因為使用遞歸最大的特點就是會用的話,那么問題就變得非常簡單,幾行就寫完了。但是換來的代價就是非常的不好理解,因此關於遞歸可以自己平時多練習,至於工作中是否使用遞歸就看你自己的了。只是希望不要為了耍帥為用遞歸,普通辦法能夠高效率解決的話,那么還是使用普通辦法。
遞歸限制:
通常來說,遞歸 CTE 的定義中需要包含一個終止遞歸的條件;否則的話,遞歸將會進入死循環。遞歸終止的條件可以是遍歷完表中的所有數據,不再返回結果;或者是一個 WHERE 終止條件。
WITH RECURSIVE t (n) AS
(
SELECT 1
UNION ALL
SELECT n + 1 FROM t
)
SELECT n FROM t;
我們上面沒有終止條件,因為沒有使用 WHERE 對 n 進行限定,那么執行該語句時,Oracle 能夠檢測到查詢語句中的問題並提示錯誤;MySQL 默認遞歸 1000 次后提示錯誤;SQL Server 默認遞歸 100 次后提示錯誤;PostgreSQL 沒有進行控制,而是進入死循環。
小結
SQL 中的集合操作符可以將多個查詢的結果組合成一個結果。本節討論了三種集合操作符:UNION [ALL]、INTERSECT 以及 EXCEPT。但是有時候,可以利用連接查詢實現與集合操作相同的效果,有興趣可以自己嘗試一下。
SQL 中的通用表表達式(CTE)相當於定義了一個表的變量,能夠將復雜的查詢結構化,並且實現結果集的重復利用。CTE 比子查詢更易閱讀和理解,遞歸 CTE 則提供了遍歷層次數據和樹狀結構圖的編程功能。