MySQL join 學習

1. 數學基礎:笛卡爾乘積
笛卡爾乘積是一個數學概念:

笛卡爾乘積是指在數學中,兩個集合 X 和 Y 的笛卡爾積(Cartesian product),又稱直積,表示爲 X × Y,第一個對象是 X 的成員而第二個對象是 Y 的所有可能有序對的其中一個成員。公式表示就是如下:

如果對同一個數據庫的兩張表進行 join 操作,例如表 A 記錄 c~1,1~、c~1,2~、c~1,3~,表 B 有 c~2,1~ 以及 c~2,2~ 字段。

那麼笛卡爾乘積的結果是:

c~1,1~+c~2,1~、c~1,1~+c~2,2~、c~1,2~+c~2,1~、c~1,2~+c~2,2~、c~1,3~+c~2,1~、c~1,3~+c~2,2~ 共 6 條記錄。其中 + 的含義是兩條記錄並做一條記錄。

2. join 的作用是什麼?

join 是關係型數據庫在關係二字上的集中體現,其作用在於將兩張及以上表根據列中字段間的相關關係,將多表中的行融合在一起。

3. 不同的 join 類型的語義

也可以用集合的語言來表示,如下圖所示:

在 SQL 實際上又把 inner join 稱爲內連接,其餘所有 join 類型都稱爲外連接。因此 join 有等效別名關鍵字:

inner join:join

顯示(explicit) inner join 與隱式(implicit) inner join 性能上沒有區別。

left joinleft outer join

right joinright outer join

cross joincross outer join

full joinfull outer join

LEFT JOIN 和 RIGHT JOIN沒什麼差別,兩者的結果差異取決於左右表的放置順序。

4. 典型帶有 join 的 SQL 語法分析
典型帶有 join 的 SQL 語句如下所示:

我們按照 SQL 語句的執行順序來對上述 SQL 語句進行說明:

注意事項:下面的說法僅僅從 MySQL 執行語義上進行說明,實際上 MySQL 在內存中不會建立 vt1、vt2、vt3 表。

FROM:MySQL 中 FROM 子句總是第一個被執行的,FROM 的作用是對 join 涉及的多個表進行笛卡爾乘積 vt1 表,結果有 m*n 行(m 爲左表的行數,n 爲右表的行數);

ON:新建一張 vt2 表,並根據 ON 的條件篩選 vt1 表,符合條件的行加入到 vt2 中;

ON 只有對 Cross join 不是必須的。

JOIN:如果是 left join 或者 right join,那麼就需要添加外部行,如果是 inner join 就不需要添加外部行。添加外部行以 left join 爲例,首先遍歷左表的每一行,其中不在 vt2 中的行會被添加到 vt2 中,不屬於左表的字段會被置爲 NULL,最終形成 vt3;

WHERE:對 vt3 表按照條件進行過濾,滿足條件的行被輸出到 vt4;

SELECT:從 vt4 中取出指定的字段到 vt5;

ON 與 WHERE 的區別是什麼?

ON 與 WHERE 在使用 inner join 時,無論是在結果上還是在性能上都沒有區別。

從結果上看,inner join 中無論條件寫在 ON 還是 WHERE 後,結果相同。在使用 left/left join 時,結果有區別。例如,在 left join 中對 ON 後不符合條件的左表中的行還是會被納入到結果中,但是卻可以被 WHERE 後的條件過濾掉。
從效率的角度上看,雖然很多中文資源認爲有所區別,但實際上沒區別,可以參考:SQL JOIN - WHERE clause vs. ON clause,查詢優化器會避免寫法的不同導致執行效率的不同。

5. join 性能優化
5.1 join 可以跨庫嗎?
MySQL 可以利用 FEDERATED 引擎等方式實現跨庫 join,但查詢效率實際上並不高。通常認爲 MySQL join 操作指的同數據庫的多表 join。

5.2 join 內部執行過程與索引
在單表查詢中,我們通常會強調兩點:

WHERE 後的字段是否可以走索引,如果不行,那麼將直接走簇集索引,進行全表掃描,效率很差;
SELECT 後的字段是否可以走覆蓋索引,如果不行,那麼則需要回表到簇集索引;
但在 join 多表問題中,索引不僅僅需要考慮上述兩個問題。

MySQL 中的 join 操作並不會在內存中構造臨時表,第四節中的說法只是方便從語義上進行理解。join 具體如何執行取決於查詢優化器的選擇。

MySQL 支持如下三種 join 操作(以兩張表 join 爲例):

nested loop join:利用嵌套 for 循環對兩張表中的每一行數據進行兩兩比較。需要遍歷第一張表 n 行,每一行都需要進行時間複雜度爲 O(n) 的非索引查詢,因此總的比較的時間複雜度爲 O(n^2^)

block nested loop join:對 nested loop join 的優化,利用對第一張表的行進行查詢緩存,這樣內層 for 循環中第二張表的每一條行數據一次性與第一張表的多條行數據進行比較,減少了對內表的比較次數。需要遍歷第一張表 n 行,每 k 行都需要進行時間複雜度爲 O(n) 的非索引查詢,因此總的比較的時間複雜度爲 O(n^2^/k),k 爲常數。

index nested loop join:從第一張表讀一行,然後在第二張表的索引中查找這個數據,索引是 B+ 樹索引。需要遍歷第一張表 n 行,每一行都需要進行時間複雜度爲 O(logn) 的非索引查詢,因此總的比較的時間複雜度爲 O(nlogn)。

batched key access join:其也是利用對外循環表的字段進行緩存,減少對內循環表的訪問次數。比較次數得到一定減少,但是比較的時間複雜度還是爲 O(nlogn/k),k 爲常數。
可見,join 操作的性能非常取決於第二張表是否基於索引進行查詢。不過,爲什麼不要求第一張表也使用索引?

實際上,第一張表被稱爲驅動表,亦可稱之爲基表,MySQL 總是要遍歷該表的所有行,每一行都去第二張表中進行匹配查詢。遍歷可以不建立索引,走簇集索引即可,而查詢操作則需要依賴於二級索引。

那麼,MySQL 如何決定將哪一張表作爲驅動表呢?

MySQL 選擇驅動表的原則是:在對最終結果集沒影響的前提下,優先選擇結果集最少的那張表作爲驅動表。原因在於驅動表的行數決定了在非驅動表中進行查詢的次數,驅動錶行數越少,進行查詢的次數越少。

如果是 left join,那麼基表通常是 left join 左側表,right join 的基表通常爲 right join 右側表。

因此,我們要非常注意非驅動表的索引,在 ON 以及 WHERE 後的字段都應該被索引覆蓋。

5.3 join 與數據庫範式
數據庫範式有若干條[4],定義偏於學術性,但核心思路是簡潔明瞭的:數據庫範式目的是使結構更合理,消除存儲異常,使數據冗餘儘量小,便於插入、刪除和更新。

join 操作的原因就在於多表之間有關係並且多個表之間數據幾乎沒有冗餘。

舉一個例子,我們有三個表:

student(id,name)
class(id,description)
student_class(student_id,class_id)
如果要查詢一個學生對應的班級描述,那麼就需要對上述三標進行 join,join 的性能問題可能會使我們產生擔心。

爲此,我們可以故意破壞範式,製造出一張存在冗餘的“大表”:

student_class_full(student_id, class_id, name, description)
你會發現,class 的 description 可能存儲在兩個表中(student_class_full 與 class),這不符合範式,並且爲寫操作帶來了一致性問題以及寫性能下降。另一方面,我們不再需要使用 join 來完成查詢,讀性能得到提高。

可見,在一些場景下,我們可以選擇破壞數據庫範式,避免使用 join 來提高讀性能。代價是不同表之間出現的字段冗餘、寫性能下降,寫操作出現多表間的一致性問題。

5.4 join 來代替子查詢
join 比子查詢在空間複雜度上要低,因此很多人建議利用 join 來代替子查詢:

子查詢:執行子查詢時,MYSQL 需要創建臨時表,查詢完畢後再刪除這些臨時表,所以,子查詢的速度會受到一定的影響,這裏多了一個創建和銷燬臨時表的過程。
join:正如 5.3 小節所述,join 走嵌套查詢。小表驅動大表,通過索引字段進行關聯。

6. 是否應當使用 join?
阿里巴巴在 Java 開發手冊中建議[8]:超過三個表禁止 join。需要 join 的字段,數據類型保持絕對一致。

可見,阿里巴巴的意思是可以用 join,但是不要超過3張表。

(1)爲什麼 join 表的個數不能太多?

雖然我們可以利用索引來優化查詢,但是如果是 k 張 n 行的數據庫進行 join 查詢,最壞的情況下時間複雜度爲 O(n*(logn)^k-1^),因此 join 表的數量應當得到控制。

例如,我們假設每一張表的行數爲 1000,000 行,那麼時間複雜度有:

(2)爲什麼可以使用 join?

很多場景下 join 是最優選擇。例如兩張表各有 10W 條數據,我們的確可以利用 service 層,分兩步向兩個數據庫索要對應的行數據,然後在 service 層完成數據行的關聯與過濾。但是 2*10 W 行數據有很大的網絡傳輸壓力,並且會對 service 層所在的服務器內存有一定壓力。而 join 在 mysql server 處實際可能僅僅會得到 100 條符合要求的記錄,那麼對比起來,在 service 層的額外開銷更難以接受。

當然,分庫的 join 避免不了網絡傳輸的額外開銷(排除一機多庫)。

SUMMARY
基於笛卡爾乘積,我們能夠方便地從語義上理解 MySQL 各種 join 語義;
第 4 節從語義上說明了典型帶有 join 的 SQL 語法的執行過程,但是注意其內部並不會建立多個虛擬表;
第 5 節分析了 join 操作的內部機制:join 基於小表驅動大表地進行嵌套查詢,被驅動表是否能夠走索引進行查詢將決定整個 join 語句的執行效率;
第 6 節分析了 join 使用建議,並給出其時間複雜度模型,解釋了阿里巴巴建議 join 表數量不應當超過 3 張的原因;

REFERENCE
[1] Mysql-JOIN詳解
[2] SQL Joins
[3] 分佈式數據庫如何實現跨庫join?
[4] 百度百科-數據庫範式
[5] 《阿里巴巴JAVA開發手冊》裏面寫超過三張表禁止join 這是爲什麼?這樣的話那sql要怎麼寫?
[6] 神奇的 SQL 之 聯表細節 → MySQL JOIN 的執行過程(一)
[7] 神奇的 SQL 之 聯表細節 → MySQL JOIN 的執行過程(二)
[8] 嵩山版Java開發手冊-阿里雲開發者社區
[9] [MySQL多表關聯查詢效率高點還是多次單表查詢效率高,爲什麼?]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章