Table of Contents
前言
SQL 是基於關系代數的查詢語言,假如學習過 SQL 和關系代數,你就會發現,在 SQL 的查詢語句中你會發現很多關系代數的影子。
然而,雖然知道 SQL 和關系代數密切相關,但是我也不知道學了關系代數對學習 SQL 有什么好處,因此,如果你對關系代數沒興趣的話,現在就可以關掉這篇博客了。
關系與表
我們可以把數據庫中的表和關系代數中的關系看做是一個東西,這里給出接下來會用到的兩個關系(表):
User
+------+---------+--------+
| id | account | passwd |
+------+---------+--------+
| 1 | 123 | ****** |
| 2 | 456 | ****** |
+------+---------+--------+
Profile
+------+------+------+
| id | name | age |
+------+------+------+
| 1 | tony | 16 |
| 3 | john | 2 |
+------+------+------+
關系代數的基本運算
關系代數的基本運算包括:選擇、投影、並、集合差、笛卡爾積和更名。
投影
這里我們可以先來看一看 投影 運算,它的作用和 SQL 中的 SELECT
基本相同。
比如說我們要選擇 User
中的 account
, 用 SQL 編寫的話就是:
SELECT account FROM user;
如果用關系代數來寫的話,就可以寫成 \(\prod_{account}(user)\).
選擇多列就可以這樣: \(\prod_{id,account}(user)\).
選擇
由於一些歷史原因,關系代數中的選擇和 SQL 中的 SELECT
不是一個意思,而是更接近 WHERE
, 我們可以通過選擇運算選擇關系中符合指定條件的部分。
比如說 \(\sigma_{id=1}(user)\) 可以選擇關系 User
中 id
等於 1
的用戶,其等價的 SQL 語句如下:
SELECT * FROM user WHERE id = 1;
選擇運算中可以使用的謂詞包括: \(=, \neq, <, \leqslant, >, \geqslant\). 同時還可以使用連詞 \(and(\land), or(\lor), not(\lnot)\) 將多個謂詞合並為一個較大的連詞。
比如說 \(\sigma_{id \geqslant 1 \land id < 3}\) 選擇 id
范圍在 [1, 3)
之間的用戶,等價於:
SELECT * FROM user WHERE id >= 1 AND id < 3;
同時,由於關系運算的結果依然是一個關系,因此,我們可以將關系運算組合起來,比如:選擇 id 為一的用戶的 account 可以表示為 \(\prod_{account}(\sigma_{id=1}(user))\)
並運算
並運算可以將兩個集合並起來,對應到 SQL 中就是 UNION
操作,比如說獲取 User 和 Profile 中的所有 ID:
SELECT id FROM user UNION SELECT id FROM profile;
用關系代數來表示的話就是: \(\prod_{id}(user) \cup \prod_{id}(profile)\).
關系代數的並運算和 SQL 中的 UNION
一樣,要求需要並起來的關系的 列 是相同的,同時,比 SQL 更嚴格的是,關系代數的並運算還要求列的位置相同。
集合差運算
集合差運算可以從一個集合中排除另一個集合中的內容,對於到 SQL 中就是 EXCEPT
操作,比如獲取 User 不在 Profile 中的所有 ID1:
SELECT id FROM user EXCEPT SELECT id FROM profile;
用關系代數來表示的話就是: \(\prod_{id}(user) - \prod_{id}(profile)\).
集合差運算對不同關系的要求和並運算是相同的。
笛卡爾積
笛卡爾積是一個很重要的運算,通過笛卡爾積我們可以將 任意 兩個關系的信息結合在一起,笛卡爾積的運算結果會將兩個關系的所有列作為新的關系的列,將兩個關系的所有行的組合作為新的關系的行。
對應到 SQL 中便是 CROSS JOIN
, 比如說如下 SQL 語句便可以表示為 \(user \times profile\):
SELECT * FROM user CROSS JOIN profile;
運算結果如下:
+------+---------+--------+------+------+------+
| id | account | passwd | id | name | age |
+------+---------+--------+------+------+------+
| 1 | 123 | ****** | 1 | tony | 16 |
| 2 | 456 | ****** | 1 | tony | 16 |
| 1 | 123 | ****** | 3 | john | 2 |
| 2 | 456 | ****** | 3 | john | 2 |
+------+---------+--------+------+------+------+
更名運算
關系代數中的更名運算對應到 SQL 中便等價於 AS
操作,可以對關系進行更名也可以對列進行更名操作:
- 更名關系 - \(\rho_{users}(user)\)
- 更名列 - \(\rho_{users(uid,account,password)}(user)\)
在進行連接操作的時候常常會用到更名操作,而 SQL 中的更名操作用起來比關系代數中的方便一些,形象一些。
關系代數的附加運算
關系代數的附加運算是可以通過基本運算推導得出的,包括集合交運算和各類連接運算。
集合交運算
集合交運算計算兩個關系中都存在的部分,可以通過基本運算表示: \(r \cap s = r - (r - s)\).
集合交運算對於的 SQL 語句是 INTERSECT
, 比如:
SELECT id FROM user INTERSECT SELECT id FROM profile;
表示為關系代數便是 \(\prod_{id}(user) \cap \prod_{id}(profile)\).
連接運算
個人認為連接運算是所有運算中最難的一種,它存在很多分類,比如:自然連接、內連接、外連接等。
同時,不同的連接運算之間還存在不淺的關系,因此,需要好好理解才行。
自然連接
首先是自然連接,自然連接將兩個關系的 屬性集 的 並集 作為新的關系的屬性,同時會對兩個關系中的相同屬性進行比較篩選。
假如兩個關系不存在相同的屬性,那么自然連接的結果便和 笛卡爾積 相同:
+------+---------+--------+------+------+
| id | account | passwd | name | age |
+------+---------+--------+------+------+
| 1 | 123 | ****** | tony | 16 |
+------+---------+--------+------+------+
如上便是 自然連接 的運算結果,它將關系 User 和 Profile 的屬性的並集作為新關系的屬性,同時篩選具有相同 ID 值的行。
連接運算的關系代數形式都很復雜,這里就簡單列出對應的 SQL 語句好了2:
SELECT * FROM user NATURAL JOIN profile;
內連接
可以把內連接3 看做添加了選擇語句的笛卡爾積,也就是說,計算內連接時需要先行計算出笛卡爾積,然后在根據選擇條件進行選擇。
比如這樣的內連接操作:
SELECT * FROM user INNER JOIN profile ON user.id >= profile.id;
其結果為:
+------+---------+--------+------+------+------+
| id | account | passwd | id | name | age |
+------+---------+--------+------+------+------+
| 1 | 123 | ****** | 1 | tony | 16 |
| 2 | 456 | ****** | 1 | tony | 16 |
+------+---------+--------+------+------+------+
這里可以對照笛卡爾積的計算結果進行理解:
+------+---------+--------+------+------+------+
| id | account | passwd | id | name | age |
+------+---------+--------+------+------+------+
| 1 | 123 | ****** | 1 | tony | 16 |
| 2 | 456 | ****** | 1 | tony | 16 |
| 1 | 123 | ****** | 3 | john | 2 |
| 2 | 456 | ****** | 3 | john | 2 |
+------+---------+--------+------+------+------+
外連接
我們可以把外連接看做是 內連接 的擴展4,首先計算出兩個關系內連接的結果,然后根據外連接的類型補充數據到內連接的結果上。
比如說左外連接,首先可以計算出 User 和 Profile 的內連接,然后用空值來填充在左側關系中存在而右側關系中不存在的項就可以了。
SELECT * FROM user LEFT JOIN profile on user.id = profile.id;
這條 SQL 語句的執行結果為:
+------+---------+--------+------+------+------+
| id | account | passwd | id | name | age |
+------+---------+--------+------+------+------+
| 1 | 123 | ****** | 1 | tony | 16 |
| 2 | 456 | ****** | NULL | NULL | NULL |
+------+---------+--------+------+------+------+
如果將其替換為內連接的話便是:
+------+---------+--------+------+------+------+
| id | account | passwd | id | name | age |
+------+---------+--------+------+------+------+
| 1 | 123 | ****** | 1 | tony | 16 |
+------+---------+--------+------+------+------+
可以看到,ID 為 2 的項只存在於 User 中而不存在與 Profile 中,因此,左外連接時使用了空值來填充 Profile 對應的部分,保證 User 的每項都存在。
依次類推,右外連接、全外連接也就好理解了:
右外連接的執行結果:
+------+---------+--------+------+------+------+
| id | account | passwd | id | name | age |
+------+---------+--------+------+------+------+
| 1 | 123 | ****** | 1 | tony | 16 |
| NULL | NULL | NULL | 3 | john | 2 |
+------+---------+--------+------+------+------+
全外連接的執行結果:
+------+---------+--------+------+------+------+
| id | account | passwd | id | name | age |
+------+---------+--------+------+------+------+
| 1 | 123 | ****** | 1 | tony | 16 |
| 2 | 456 | ****** | NULL | NULL | NULL |
| NULL | NULL | NULL | 3 | john | 2 |
+------+---------+--------+------+------+------+
其實,這三個外連接是可以互相轉換的,將兩個關系的位置換一下就可以將左外連接轉換為右外連接,而將左右外連接的結果並起來就可以得到全外連接了。
結語
雖然說關系代數和 SQL 有不淺的關系,但是學了關系代數,對編寫 SQL 也沒有多大的幫助 @_@
而且,不同的數據庫實現 SQL 的語法還存在細微的差別……
也許,可以借助關系代數表達式來生成 SQL 語句!
其實關系代數還有一些擴展運算,對應到 SQL 中便是聚集、分組之類的,博客中沒有說到,有興趣的話可以去了解一下。
或者什么時候有時間了補上 @_@
Footnotes
1 不同數據庫對 EXCEPT
子句的支持存在區別,這里的 SQL 語句不一定能運行通過
3 其實在關系代數中內連接應該叫做 theta 連接, 這里主要是為了和 SQL 相對應
4 其實按照書《數據庫系統概念》中的描述的話應該是 自然連接, 但是實際的操作結果更符合 內連接, 雖然說,內連接也可以看做是自然連接