神奇的 SQL 之團結的力量 → JOIN
前言
開心一刻
閨蜜家暴富,買了一棟大別野,喊我去吃飯,菜挺豐盛的,筷子有些不給力,銀筷子,好重,我說換個竹子的,閨蜜說,這種銀筷子我家總共才五雙,只有貴賓才能用~我咬著牙享受著貴賓待遇,終於,在第三次夾蝦排滑落盤子時,我爆發了:去它喵的貴賓,我要蝦排……不是……我要竹筷子!
連線
簡單來說,就是將其他表中的列新增過來,進行"新增列"的運算,如下圖所示。
為什麼需要進行"新增列"的操作 了? 因為我們在設計資料庫的時候,往往需要滿足正規化(具體滿足正規化幾,無法一概而論,這裡不做細究),會導致我們某個需求的全部列分散在不同的表中,所以為了滿足需求,我們需要將某些表的列進行連線。我們來看個簡單例子,假如我們有兩張表(t_user,t_login_log):
DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵', user_name VARCHAR(50) NOT NULL COMMENT '使用者名稱', sex TINYINT(1) NOT NULL COMMENT '性別, 1:男,0:女', age TINYINT(3) UNSIGNED NOT NULL COMMENT '年齡', phone_number VARCHAR(11) NOT NULL DEFAULT '' COMMENT '電話號碼', email VARCHAR(50) NOT NULL DEFAULT '' COMMENT '電子郵箱', create_time datetime NOT NULL COMMENT '建立時間', update_time datetime NOT NULL COMMENT '更新時間', PRIMARY KEY (id) ) COMMENT='使用者表'; DROP TABLE IF EXISTS t_login_log; CREATE TABLE t_login_log ( id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵', user_name VARCHAR(50) NOT NULL COMMENT '使用者名稱', ip VARCHAR(15) NOT NULL COMMENT '登入IP', client TINYINT(1) NOT NULL COMMENT '登入端, 1:android, 2:ios, 3:PC, 4:H5', create_time datetime NOT NULL COMMENT '建立時間', PRIMARY KEY (id) ) COMMENT='登入日誌'; INSERT INTO t_user(user_name, sex, age, phone_number,email,create_time,update_time) VALUES ('Bruce Lee', 1, 32, '15174480987', '[email protected]', NOW(), NOW()), ('Jackie Chan', 1, 65, '15174481234', '[email protected]', NOW(), NOW()), ('Jet Li', 1, 56, '15174481245', '[email protected]', NOW(), NOW()), ('Jack Ma', 1, 55, '15174481256', '[email protected]', NOW(), NOW()), ('Pony', 1, 48, '15174481278', '[email protected]', NOW(), NOW()), ('Robin Li', 1, 51, '15174481290', '[email protected]', NOW(), NOW()); INSERT INTO t_login_log(user_name, ip, client, create_time) VALUES ('Jackie Chan', '10.53.56.78',2, '2019-10-12 12:23:45'), ('Jackie Chan', '10.53.56.78',2, '2019-10-12 22:23:45'), ('Jet Li', '10.53.56.12',1, '2018-08-12 22:23:45'), ('Jet Li', '10.53.56.12',1, '2019-10-19 10:23:45'), ('Jack Ma', '198.11.132.198',2, '2018-05-12 22:23:45'), ('Jack Ma', '198.11.132.198',2, '2018-11-11 22:23:45'), ('Jack Ma', '198.11.132.198',2, '2019-06-18 22:23:45'), ('Robin Li', '220.181.38.148',3, '2019-10-21 09:45:56'), ('Robin Li', '220.181.38.148',3, '2019-10-26 22:23:45'), ('Pony', '104.69.160.60',4, '2019-10-12 10:23:45'), ('Pony', '104.69.160.60',4, '2019-10-15 20:23:45');
如果我們需要展示如下列表(需求:展示使用者列表,並顯示其最近登入時間、最近登入 IP),那麼就需要 t_user 和 t_login_log 連表查了
連線的型別有很多種,細分如下圖
交叉連線
講交叉連線之前了,我們先來看看笛卡爾積,假設我們兩個集合,集合A={a, b},集合B={0, 1, 2},則A與B的笛卡爾積為{(a, 0), (a, 1), (a, 2), (b, 0), (b, 1), (b, 2)},表示為AxB,也就是集合A中的任一元素與集合B的每個元素組合後的新集合則為A與B的笛卡爾積(AxB)。數學上的笛卡爾積反映到資料庫中就是交叉連線(CROSS JOIN),結合上述的案例如下:
SELECT * FROM t_user CROSS JOIN t_login_log; -- 與 CROSS JOIN 得到的結果相同 -- 過時的寫法,不符合 SQL標準,能讀懂就好,不推薦使用 SELECT * FROM t_user, t_login_log;
t_user 中有 6 條記錄, t_login_log 中有 11 條記錄,t_user CROSS JOIN t_login_log 的結果是 66( 6 乘以 11) 條記錄
交叉連線就是對兩張表中的全部記錄進行交叉組合,因此其結果是兩張表的乘積,這也是為什麼交叉連線無法使用內連線或外連線中所使用的 ON 子句的原因。交叉連線基本不會應用到實際業務之中,原因有兩個,一是其結果沒有實用價值,而是結果行數太多,需要花費大量的運算時間和硬體資源。雖說交叉連線的實際使用場景幾乎沒有,但還是有它的理論價值的,交叉連線是其他所有連線運算的基礎,內連線是交叉連線的一部分,其結果是交叉連線的一部分(子集),外連線有點特殊,其結果包含交叉連線之外的內容;更多詳情,我們接著往下看。
內連線
只返回兩張表匹配的記錄,就叫內連線,直觀的表現就是關鍵字:INNER JOIN ... ON,ON 表示兩張表連線所使用的列(連線鍵);而內連線中又屬等值連線最常用
等值連線
簡單點來說,就是連線鍵相等
-- 等值連線 SELECT * FROM t_user tu INNER JOIN t_login_log ttl ON tu.user_name = ttl.user_name; -- INNER JOIN 可以簡寫成 JOIN SELECT * FROM t_user tu JOIN t_login_log ttl ON tu.user_name = ttl.user_name; -- 不加連線鍵, 結果與 CROSS JOIN 一樣 SELECT * FROM t_user tu INNER JOIN t_login_log ttl
等值連線的結果中,每一條記錄的連線鍵的列的值是想等的,如上圖中的 user_name 和 user_name1(為了區別於第一個user_name,資料庫系統自動取的別名,我們可以顯示的指定)
不等值連線
連線鍵的比較謂詞除了 = 之外的所有情況,比如 >、<、<>(!=);不等值連線使用場景比較少,反正我在實際工作中幾乎沒用到過
SELECT * FROM t_user tu INNER JOIN t_login_log ttl ON tu.user_name <> ttl.user_name; SELECT * FROM t_user tu INNER JOIN t_login_log ttl ON tu.user_name > ttl.user_name;
自然連線
不需要指定連線條件,資料庫系統會自動用相同的欄位作為連線鍵,直觀的表現就是關鍵字:NATURAL JOIN,NATURAL LEFT JOIN、NATURAL RIGHT JOIN;
連線鍵不直觀,需要去看兩張表中相同的欄位有哪些;對於自然連線,瞭解即可,不推薦使用,反正我工作這麼久,一次都沒用過。
外連線
外連線的使用方式與內連線一樣,也是通過 ON 使用連線鍵將兩張表連線,從結果中獲取我們想要的資料,但是返回的結果與內連線有區別,具體我們往下看
左連線
返回匹配的記錄,以及左表多餘的記錄,關鍵字:LEFT JOIN(LEFT OUTER JOIN 的簡寫)
SELECT * FROM t_user tu LEFT OUTER JOIN t_login_log ttl ON tu.user_name = ttl.user_name; -- LEFT JOIN 是 LEFT OUTER JOIN 的簡寫 SELECT * FROM t_user tu LEFT JOIN t_login_log ttl ON tu.user_name = ttl.user_name;
上圖中,前 11 條記錄是匹配的記錄,而第 12 條是不匹配、左表的記錄
右連線
返回匹配的記錄,以及表 B 多餘的記錄,關鍵字:RIGHT JOIN(RIGHT OUTER JOIN 的簡寫)
SELECT * FROM t_login_log ttl RIGHT OUTER JOIN t_user tu ON tu.user_name = ttl.user_name; -- RIGHT JOIN 是 RIGHT OUTER JOIN 的簡寫 SELECT * FROM t_login_log ttl RIGHT JOIN t_user tu ON tu.user_name = ttl.user_name;
由於我們習慣了從左往右(閱讀方式、寫作方式),因此在實際專案中,基本上用的都是左連線
全連線
返回匹配的記錄,以及左表和右表各自的多餘記錄,關鍵字:FULL JOIN (FULL OUTER JOIN 的簡寫)
SELECT * FROM t_user tu FULL OUTER JOIN t_login_log ttl ON tu.user_name = ttl.user_name; -- FULL JOIN 是 FULL OUTER JOIN 的簡寫 SELECT * FROM t_user tu FULL JOIN t_login_log ttl ON tu.user_name = ttl.user_name;
注意:MySQL 不支援 全連線,我們可以通過 左連線、右連線之後,再 UNION 來實現全連線
自連線
一張表,自己連線自己,簡單點來理解就是,左表、右表是同一張表;連線方式可以是內連線、也可以是外連線
更多詳情大家可以去看:專案上線後,談一下感觸比較深的一點:查詢優化
需求:展示使用者列表,並顯示最近登入時間、最近登入 IP
對於此需求,大家會如何來寫這個 SQL ? 也許大家很容易想到左連線,如下所示
SELECT * FROM t_user tu LEFT JOIN t_login_log ttl ON tu.user_name = ttl.user_name;
可結果如下:
顯示的是每個使用者的所有登入日誌,不是我們想要的結果;原因是 t_user 中的一條記錄在 t_login_log 對應的記錄有多種情況:0 條對應、1 條對應、多條對應,那這個 SQL 要怎麼寫呢,方式有多種,不侷限於如下實現
-- 1、連線配合子查詢,注意 Bruce Lee 從未登陸過 SELECT tu.user_name, tu.sex,tu.age, tu.phone_number,tu.email,tll.create_time,tll.ip FROM t_user tu LEFT JOIN t_login_log tll ON tu.user_name = tll.user_name WHERE tll.id = (SELECT MAX(id) FROM t_login_log WHERE user_name = tu.user_name) OR tll.user_name IS NULL; -- 2、t_login_log分組統計出各個使用者的最近一次登入資訊後,再與 t_user 聯表 SELECT tu.user_name, tu.sex,tu.age, tu.phone_number,tu.email,tll.create_time,tll.ip FROM t_user tu LEFT JOIN ( SELECT tb.* FROM( SELECT user_name, MAX(id) id FROM t_login_log GROUP BY user_name ) ta LEFT JOIN t_login_log tb ON ta.id = tb.id ) tll ON tu.user_name = tll.user_name;
具體的實現還得結合具體的業務和需求來實現,那樣才能寫出高效的 SQL;另外結合執行計劃來建立合適的索引。總之,沒有一成不變的、通用的高效 SQL,結合具體的業務才能寫出最合適的 SQL。
總結
1、連線的描述方式
常用的維恩圖,描述如下
維恩圖描述有他的優勢,但它不好表示交叉連線,同時容易讓人誤解成 SQL 中的集合操作;這裡推薦另外一種描述方式,我覺得描述的更準確
CROSS JOIN
常用 JOIN
上圖中,顏色表示匹配關係,顏色相同表示匹配。返回結果中,如果另一張表沒有匹配的記錄,則用 null 填充, 在上圖中則表示為空白。
2、連線中 ON 指定連線鍵,連線鍵可以指定多個,而 WHERE 還是平時的作用,用來指定過濾條件;不推薦將連線鍵放於 WHERE 後;
3、實際工作中,用的最多的是 左連線 和 等值連線,其他的用的特別少
參考
《SQL基礎教程》
《SQL進階教