上一篇文章中,我們介紹了 SQL 中最基本的 DML 語法,包括 insert 的插入數據、update 的更新數據、delete 的刪除數據以及基本的查詢語法,但大多比較簡單不能解決我們日常項目中復雜的需求。
那么,本篇就來看一看較為復雜繁瑣的查詢語法,例如多表的連接查詢、嵌套的子查詢以及很多預定的功能函數。
連接查詢
關系型數據庫的一個特點就是,多張表之間存在關系,以致於我們可以連接多張表進行查詢操作,所以連接查詢會是關系型數據庫中最常見的操作。
連接查詢主要分為三種,交叉連接、內連接和外連接,我們一個個說。
1、交叉連接
交叉連接其實連接查詢的第一個階段,它簡單表現為兩張表的笛卡爾積形式,具體例子:
如果你沒學過數學中的笛卡爾積概念,你可以這樣簡單的理解這里的交叉連接:
兩張表的交叉連接就是一個連接合並的過程,T1 表中的每一行會分別與 T2 表的每一行做一個組合連接並單獨生成一行。例如 T1 的第一行會與 T2 的第一行合並生成一行,會與 T2 的第二行再合並生成一行,T2 的第三行合並生成一行,接着再以 T1 的第二行為基礎重復上述動作。
應該不難理解,我們看實現交叉連接的語法是什么樣的。
交叉連接使用關鍵字 CROSS JOIN 進行連接,例如:
select * from table1 cross join table2
也可以按照 ANSI SQL:1989 規范中指定的,使用逗號進行交叉連接,例如:
select * from table1,table2
通過交叉連接,我們可以兩張表的數據進行一個結合,但是你會發現同時也產生了很多冗余的垃圾數據行,所以我們往往也會結合 where 子句對結果集進行一個條件篩選。
例如我們有這么兩張表:
Students表:
+----+--------+------+----------+
| id | name | age | departId |
+----+--------+------+----------+
| 1 | 小明 | 12 | 1 |
| 2 | 胖虎 | 13 | 4 |
| 3 | 小新 | 15 | 2 |
| 4 | 曹操 | 1000 | 3 |
| 5 | 王安石 | 2000 | 5 |
| 6 | 杜甫 | 2000 | 2 |
+----+--------+------+----------+
departs 表:
+----+----------------+
| id | depart |
+----+----------------+
| 1 | 信息工程學院 |
| 2 | 文學院 |
| 3 | 化工學院 |
| 4 | 外國語學院 |
| 5 | 數學與統計學院 |
+----+----------------+
如果我們現在需要查詢一個學生的學院信息,我們就需要連接兩個表,而我們的交叉連接會為我們產生太多冗余數據行,我們可以使用 where 子句對笛卡爾積后的結果集進行一個條件篩選。
select * from students,departs
where students.departId = departs.id
這樣就過濾了那些冗余的垃圾數據行,得到我們需要的有效數據。
+----+--------+------+----------+----+----------------+
| id | name | age | departId | id | depart |
+----+--------+------+----------+----+----------------+
| 1 | 小明 | 12 | 1 | 1 | 信息工程學院 |
| 3 | 小新 | 15 | 2 | 2 | 文學院 |
| 6 | 杜甫 | 2000 | 2 | 2 | 文學院 |
| 4 | 曹操 | 1000 | 3 | 3 | 化工學院 |
| 2 | 胖虎 | 13 | 4 | 4 | 外國語學院 |
| 5 | 王安石 | 2000 | 5 | 5 | 數學與統計學院 |
+----+--------+------+----------+----+----------------+
但是我們仍然需要說一下,雖然交叉連接允許結合 where 子句過濾冗余數據,但是笛卡爾積本身就造成了很大的資源消耗,對於這種情況,內連接會有更好效率體現,同樣能實現該需求。
總歸一句,笛卡爾積式的交叉連接謹慎使用。
2、內連接
內連接也可以理解為條件連接,它使用關鍵字 INNER JOIN 連接兩張表並使用 ON 篩選器篩選組合合適的數據行。基本語法如下:
select * from table1 inner join table2 on [條件]
我們同樣以上述的 students 和 departs 表舉例子,看這條 SQL:
select * from students
inner join departs
on students.departId = departs.id;
會得到同樣的結果:
+----+--------+------+----------+----+----------------+
| id | name | age | departId | id | depart |
+----+--------+------+----------+----+----------------+
| 1 | 小明 | 12 | 1 | 1 | 信息工程學院 |
| 3 | 小新 | 15 | 2 | 2 | 文學院 |
| 6 | 杜甫 | 2000 | 2 | 2 | 文學院 |
| 4 | 曹操 | 1000 | 3 | 3 | 化工學院 |
| 2 | 胖虎 | 13 | 4 | 4 | 外國語學院 |
| 5 | 王安石 | 2000 | 5 | 5 | 數學與統計學院 |
+----+--------+------+----------+----+----------------+
我們也說了,雖然交叉連接也實現同樣的效果,但實現原理是不同的,效率也是不一樣的,交叉連接通過笛卡爾積返回結果集再結合 where 子句剔除冗余數據行,而內連接的 ON 篩選器工作在笛卡爾積過程中,只有符合條件才能合並生成新的數據行。
這兩者的效率是不同的,內連接的效率顯然是大於等於交叉連接的,所以我們也建議了盡量使用內連接取代交叉連接的使用。
3、外連接
我們說內連接主要是兩個步驟的結合,笛卡爾積加 ON 篩選器,一般情況下也是內連接使用的最為頻繁。外連接其實是基於內連接的兩個步驟,額外新增了另一個步驟,進一步苛刻化查詢操作,我們依然以上述的學生表和學院表營造這么一個場景:
我們的學生表中保存了學生的個人信息以及所屬的學院外鍵編號,假設其中有部分學生的學院還未分配,即為NULL。
那么我現在需要查詢出所有的學生及其所屬部門信息,包括那些未知學院信息的學生,請問你怎么做?
這個問題的核心點在於,我不僅要滿足連接條件成功合並的數據行,還要那些未成功匹配的行,也就是說學生表的所有行都得出現。
不賣關子了,使用左外連接即可實現:
我們該一下 students 表中數據,departs 表中數據不變:
+----+------+------+----------+
| id | name | age | departId |
+----+------+------+----------+
| 1 | 小明 | 12 | 1 |
| 2 | 胖虎 | 13 | NULL |
| 3 | 小新 | 15 | 2 |
| 4 | 李白 | 200 | NULL |
+----+------+------+----------+
我們執行 SQL :
select * from
students left outer join departs
on students.departId = departs.id;
得到結果:
+----+------+------+----------+------+--------------+
| id | name | age | departId | id | depart |
+----+------+------+----------+------+--------------+
| 1 | 小明 | 12 | 1 | 1 | 信息工程學院 |
| 2 | 胖虎 | 13 | NULL | NULL | NULL |
| 3 | 小新 | 15 | 2 | 2 | 文學院 |
| 4 | 李白 | 200 | NULL | NULL | NULL |
+----+------+------+----------+------+--------------+
你看,左連接相當於以左表為基准,成功連接匹配的就列出其對應的學院信息,不能匹配的就填充為 NULL。
當然,如果你想以右表為基准,你可以使用右連接,關鍵詞 right outer join/on。
除此之外,還有一種全外連接,這種模式下沒有以誰為基准,兩邊表的所有行都得出現。我們舉個例子:
兩邊的表都在看,自己哪些行成功的條件匹配了,哪些沒有,沒有成功匹配的行會在最后強制出現,未匹配的字段賦值為 NULL。
這就是外連接的本質,希望你理解了。
子查詢
子查詢,顧名思義就是嵌套的別的查詢語句中的查詢,因為很多時候查詢不是一蹴而就的,往往是需要一個中間結果集作一個過渡的,而我們的子查詢就是用於這種中間結果集過渡。
1、出現在選擇列表中
這種情況並不多見,但也是一種使用場景,可以應用在 insert,update,delete 和 select 語句中,我們分別來看。
insert:
insert into students
values(5,'yang',(select avg(id) from departs),1);
我們向 students 表中插入一條數據,age 字段的值為 departs 表 id 字段的平均值,這個例子本身沒多大意義,但是我為你演示的是語法。
update:
update students set age=(select avg(id) from departs)
where students.id = 1;
記住,子查詢一定要使用小括號括起來,強調一種優先級,不然會與外查詢產生沖突報錯。
delete:
delete from students
where id = (select avg(id) from departs)
select 語句的子查詢類似,不再贅述了。再次強調下,這種模式下使用子查詢並不多見,不要刻意為了裝逼寫成這樣,你會被打的!
2、含有in和exists的子查詢
in 這個關鍵字相信大家也不陌生,我們之前介紹 select 基本查詢的時候介紹過,它強調的是一種集合的概念,可以視作一種邏輯運算符。
比如:
select * from students
where id in(2,3,4,5,6)
我們換成子查詢就變成:
select * from students
where id in(select id from departs)
這時的子查詢返回的是一個集合,而不再是一個常量。
exists 帶來的子查詢主要思路:將主查詢的數據,放到子查詢中做條件驗證,根據驗證結果(TRUE 或 FALSE)來決定主查詢的數據結果是否得以保留。
只要子查詢有返回行,即返回 true,否則返回 false。
基本語法如下:
select [select_list] from [table]
where [not] exists [子查詢]
看起來挺復雜,我們舉個例子你就明白了:
以我們的學生表來說,每個學生都存儲了一個學院字段信息,通過這個字段的值可以具體查到學院的名字,那么假如現學生表中有大量陳腐數據,無效或為空的學院 id 對應的數據都是有些有問題的數據,現需要查出這些數據
departs 表如下:
+----+----------------+
| id | depart |
+----+----------------+
| 1 | 信息工程學院 |
| 2 | 文學院 |
| 3 | 化工學院 |
| 4 | 外國語學院 |
| 5 | 數學與統計學院 |
+----+----------------+
students 表如下:
+----+------+------+----------+
| id | name | age | departId |
+----+------+------+----------+
| 1 | 小明 | 12 | 1 |
| 2 | 小紅 | 13 | 2 |
| 3 | 王菲 | 14 | 10 |
| 4 | 張三 | 23 | NULL |
+----+------+------+----------+
顯然,學生表中的第三第四行數據有問題,他們的學院 id 非法,我們需要使用 SQL 找到這兩條數據。
select * from students
where not exists
(select * from departs where students.departId = departs.id)
執行 SQL,看看結果:
+----+------+------+----------+
| id | name | age | departId |
+----+------+------+----------+
| 3 | 王菲 | 14 | 10 |
| 4 | 張三 | 23 | NULL |
+----+------+------+----------+
首先外查詢拿到 students 表的所有數據,where 子句會遍歷每一行,執行子查詢過濾篩選,如果整個 where 子句返回為 true,代表該行記錄有效應該被查詢出來,否則將拋棄該行,繼續遍歷。
關於子查詢暫時句介紹到這,基本核心的內容都已經附帶例子的介紹了,什么嵌套子查詢不過是多套了一層而已,相信你能夠理解,這里不再贅述了。
常用函數
SQL 規范中定義了很多的函數方法,它們按照應用場景不同可以划分為以下類別,聚合函數、日期時間函數、字符串函數,邏輯函數等等,我們抽取幾個較為頻繁使用的方法介紹下,其余的留待大家自行研究。
聚合函數:
- AVG(column_name):求取結果集某一列的平均值,如果某條記錄行該字段NULL,將不參與平均值計算
- COUNT(column_name):統計結果集中數據記錄行數,即統計有多少條數據
- MAX(column_name):返回結果集中某列的最大值
- SUM(colunmn_name):求和結果集某列
- MIN(column_name):返回結果集中某列的最小值
- 除此之外,還有一些求標准差,方差
聚合函數的一個特點是,它往往是對整個結果集進行了某種數學運算並返回一個常量數值而非集合。
日期時間函數:
由於日期函數在不同的數據實現來說,很多相同功能的函數具有不同的函數名稱,我們這里僅以 MySql 來介紹這些函數,給你一個宏觀印象,不同的數據庫之間只不過語法差異,搜索引擎就可以解決。
- NOW():返回當前日期時間,精確到時分秒
- CURDATE():返回當前的日期
- CURTIME():返回當前的時間
- DATA(日期時間/日期表達式):該函數用於提取參數中的日期部分,參數可為一個時分秒的日期對象,或字符串表達式
- EXTRACT(unit FROM date):取出日期時間中指定部分,unit 可取值年、月、日、時分秒等等
- DATE_ADD(date,INTERVAL expr type):為指定參數的時間日期添加一個時間間隔
- DATEDIFF(date1,date2):返回兩個日期時間之間相隔的天數
- DATE_FORMAT(date,format):按照指定格式輸出指定的日期時間
我們詳細說一下最后一個函數,這個函數用於格式化輸出一個日期時間對象,format 為指定的格式,取值如下:
舉個例子:
select date_format(now(),'年:%Y-月:%m-日:%d');
執行 SQL,將輸出:
年:2019-月:02-日:25
雖然很奇怪的輸出格式,但我要說明的是這種可定制化的日期格式輸出函數。
數學函數:
- ABS(column_name):求絕對值
- PI():返回常量 3.141593
- 還有一些正弦、余弦,正切,平方根等函數
除此之外,還有很多函數,例如一些操作字符串、文本等等函數,這里不一一贅述了,用到的時候嘗試性的搜一搜,看有沒有預定義的函數能夠解決你的當下需求。
那么,繁雜的一篇又結束了,自認為干貨還是很足的,着重講了比較重要的連接查詢和子查詢,不知道你有啥想法呢,歡迎你和我討論!