面試官:你們的項目中竟然還在用多表關(guān)聯(lián)!
本文中我們繼續(xù)介紹Hash Join(哈希連接),以及從技術(shù)原理的角度上給出最終結(jié)論 —— 我們還是應(yīng)該在數(shù)據(jù)庫中合理使用多表關(guān)聯(lián)查詢,而不是放到應(yīng)用程序中。
圖片
我們在上一篇文章中說過,從MySQL 8.0.20開始,就不再使用緩存塊嵌套循環(huán)連接了,將以前使用緩存塊嵌套循環(huán)連接的場景全部改為哈希連接。
因為在絕大多數(shù)被驅(qū)動表沒有創(chuàng)建索引的場景,哈希連接比緩存塊嵌套循環(huán)連接性能更高。
MySQL表連接算法
哈希連接(Hash Join)
市面上所有支持哈希連接算法的數(shù)據(jù)庫,無論是OLTP數(shù)據(jù)庫中的Oracle,還是OLAP數(shù)據(jù)庫中的ClickHouse、Doris,其實現(xiàn)方式都是將小表加載到內(nèi)存形成哈希表,再通過遍歷大表數(shù)據(jù)的方式與哈希表進行匹配,并返回匹配結(jié)果。
MySQL的實現(xiàn)方式也不例外,我們以下面的SQL語句舉例:
SELECT * FROM product p INNER JOIN order o
ON p.id = o.product_id WHERE p.id in(1,2);
圖片
Using where; Using join buffer (hash join)
SELECT * FROM product p INNER JOIN order o
ON p.id = o.product_id WHERE p.id in(1,2);
SELECT * FROM t1 JOIN t2 ON t1.c1 < t2.c1
SELECT * FROM t1 WHERE t1.c1 IN (SELECT t2.c2 FROM t2)
Antijoin:
SELECT * FROM t2 WHERE NOT EXISTS (SELECT * FROM t1 WHERE
t1.c1 = t2.c1)
Left outer join:
SELECT * FROM t1 LEFT JOIN t2 ON t1.c1 = t2.c1
Right outer join:
SELECT * FROM t1 RIGHT JOIN t2 ON t1.c1 = t2.c1
應(yīng)用程序表連接算法
接下來開始聊重頭戲了,我們看一下,如果真的禁止使用SQL語句進行多表關(guān)聯(lián),而是把各表的數(shù)據(jù)讀到應(yīng)用程序中來,再由程序進行數(shù)據(jù)merge操作,這該如何實現(xiàn)呢?
圖片
步驟其實很簡單,從product表和order表中分別讀出數(shù)據(jù)后,在應(yīng)用程序代碼中要么通過一個大嵌套循環(huán)來進行表數(shù)據(jù)的連接匹配,要么通過一個HashMap進行表的連接列匹配,跟MySQL中的實現(xiàn)基本上大同小異。
數(shù)據(jù)庫 VS 應(yīng)用程序
那么,到底是放在數(shù)據(jù)庫中實現(xiàn)多表關(guān)聯(lián)查詢比較好,還是放到應(yīng)用程序中比較好呢?
贊成后者的同學(xué)會說,數(shù)據(jù)庫是一個極為寶貴稀缺的資源,而應(yīng)用服務(wù)器我們可以以集群部署的方式無限擴容。因此,將多表關(guān)聯(lián)操作放到應(yīng)用服務(wù)器中可以減輕數(shù)據(jù)庫的壓力。
對于這種說法,我只能說:“呵呵,數(shù)據(jù)庫的主從復(fù)制了解一下?不僅可以搭從庫,還可以搭二級從庫呢?!?/p>
還有一種贊成后者說法是,單表查詢在應(yīng)用程序中進行merge,這樣更有利于后續(xù)的維護,并且代碼復(fù)用性高。
原因在于,SQL語句寫得越簡單,那就越容易被復(fù)用,而多表關(guān)聯(lián)的復(fù)雜SQL只能有一種應(yīng)用場景。
這種說法就相當(dāng)于,把香河肉餅變成面粉、肉餡和食用油之后復(fù)用性高了,因為面粉可以蒸饅頭烙餅、肉餡可以做包子和餃子,而食用油則可以炒任何菜。
最離譜的一種說法是,放到應(yīng)用服務(wù)器中進行merge操作,性能會高一些。
對于這種說法,我只想說,到底是梁靜茹給你的勇氣,張信哲給你的信仰,還是五月天給你的倔強呢?
你能在表連接算法優(yōu)化上比數(shù)據(jù)庫研發(fā)者做得更好?我怎么就這么不信呢?
接下來,我說說贊成后者的原因吧,有如下三點:
(1)研發(fā)效率更高,畢竟一條SQL語句就可以搞定的事情,沒必要寫完SQL再寫代碼,兩邊兒一起忙活。
(2)代碼性能更快,如果驅(qū)動表有1萬條數(shù)據(jù),而被驅(qū)動表也有1萬條,兩者關(guān)聯(lián)到一起后返回10條數(shù)據(jù)。這種場景下,到底是哪種方式更快一些,應(yīng)該顯然易見了吧?
畢竟把兩表中的各1萬條數(shù)據(jù)通過網(wǎng)絡(luò)返回給應(yīng)用服務(wù)器,再加載到內(nèi)存中進行merge,這些都是比較耗費性能的操作。
(3)可用性提升,如果驅(qū)動表有10萬條數(shù)據(jù),而被驅(qū)動表也有10萬條,哪怕在QPS為個位數(shù)的情況下,都可能將數(shù)據(jù)庫的網(wǎng)卡打滿,從而出現(xiàn)系統(tǒng)不可用的情況。
這種故障場景,我還真的在實際工作中遇到過,當(dāng)時整整掛了一個多小時呢。