Join的實現原理及優化思路


前言

前面我們已經了解了MySQLQueryOptimizer的工作原理,學習了Query優化的基本原則和思路,理解了索引選擇的技巧,這一節我們將圍繞Query語句中使用非常頻繁,且隨時可能存在性能隱患的Join語句,繼續我們的Query優化之旅。

 

Join 的實現原理

在尋找Join語句的優化思路之前,我們首先要理解在MySQL中是如何來實現Join的,只要理解了實現原理之后,優化就比較簡單了。下面我們先分析一下MySQL中Join的實現原理。

在MySQL中,只有一種Join算法,就是大名鼎鼎的NestedLoopJoin,他沒有其他很多數據庫所提供的HashJoin,也沒有SortMergeJoin。顧名思義,NestedLoopJoin實際上就是通過驅動表的結果集作為循環基礎數據,然后一條一條的通過該結果集中的數據作為過濾條件到下一個表中查詢數據,然后合並結果。如果還有第三個參與Join,則再通過前兩個表的Join結果集作為循環基礎數據,再一次通過循環查詢條件到第三個表中查詢數據,如此往復。

下面我們將通過一個三表Join語句示例來說明MySQL的NestedLoopJoin實現方式。

注意:由於要展示Explain中的一個在MySQL5.1.18才開始出現的輸出信息(在之前版本中只是沒有輸出信息,實際執行過程並沒有變化),所以下面的示例環境是MySQL5.1.26。

Query 如下:

select m.subject msg_subject, c.content msg_content
from user_group g,group_message m,group_message_content c
where g.user_id = 1

and m.group_id = g.group_id
and c.group_msg_id = m.id

為了便於示例,我們通過如下操作為group_message表增加了一個group_id的索引:

create index idx_group_message_gid_uid on group_message(group_id);

然后看看我們的Query的執行計划:

sky@localhost : example 11:17:04> explain select m.subject msg_subject, c.content

msg_content -> from user_group g,group_message m,group_message_content c -> where g.user_id = 1 -> and m.group_id = g.group_id -> and c.group_msg_id = m.id\G

*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: g
type: ref
possible_keys: user_group_gid_ind,user_group_uid_ind,user_group_gid_uid_ind
key: user_group_uid_ind
key_len: 4
ref: const

rows: 2
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: m
type: ref
possible_keys: PRIMARY,idx_group_message_gid_uid
key: idx_group_message_gid_uid
key_len: 4
ref: example.g.group_id
rows: 3
Extra:
*************************** 3. row ***************************
id: 1
select_type: SIMPLE
table: c
type: ref
possible_keys: idx_group_message_content_msg_id
key: idx_group_message_content_msg_id
key_len: 4
ref: example.m.id
rows: 2
Extra:

我們可以看出,MySQLQueryOptimizer選擇了user_group作為驅動表,首先利用我們傳入的條件user_id通過該表上面的索引user_group_uid_ind來進行const條件的索引ref查找,然后以user_group表中過濾出來的結果集的group_id字段作為查詢條件,對group_message循環查詢,然后再通過user_group和group_message兩個表的結果集中的group_message的id作為條件與group_message_content的group_msg_id比較進行循環查詢,才得到最終的結果。

這個過程可以通過如下表達式來表示:

for each record g_rec in table user_group that g_rec.user_id=1{

  for each record m_rec in group_message that m_rec.group_id=g_rec.group_id{

    for each record c_rec in group_message_content that c_rec.group_msg_id=m_rec.id

    pass the (g_rec.user_id, m_rec.subject, c_rec.content) row

    combination to output;

} }

 

下圖可以更清晰的標識出實際的執行情況:

 clip_image002

 

假設我們去掉group_message_content表上面的group_msg_id字段的索引,然后再看看執行計划會變成怎樣:

sky@localhost : example 11:25:36> drop index idx_group_message_content_msg_id on
group_message_content;
Query OK, 96 rows affected (0.11 sec)

sky@localhost : example 10:21:06> explain

-> select m.subject msg_subject, c.content msg_content

-> from user_group g,group_message m,group_message_content c

-> where g.user_id = 1

-> and m.group_id = g.group_id

-> and c.group_msg_id = m.id\G

*************************** 1. row *************************** 

id: 1
select_type: SIMPLE
table: g
type: ref
possible_keys: idx_user_group_uid
key: idx_user_group_uid
key_len: 4
ref: const
rows: 2 Extra: *************************** 2. row ***************************

id: 1
select_type: SIMPLE
table: m
type: ref
possible_keys: PRIMARY,idx_group_message_gid_uid
key: idx_group_message_gid_uid
key_len: 4
ref: example.g.group_id
rows: 3
Extra: *************************** 3. row ***************************

id: 1
select_type: SIMPLE
table: c
type: ALL
possible_keys: NULL key: NULL key_len: NULL ref: NULL rows:
96 Extra: Using where; Using join buffer

我們看到不僅僅user_group表的訪問從ref變成了ALL,此外,在最后一行的Extra信息從沒有任何內容變成為Usingwhere;Usingjoinbuffer,也就是說,對於從ref變成ALL很容易理解,沒有可以使用的索引的索引了嘛,當然得進行全表掃描了,Usingwhere也是因為變成全表掃描之后,我們需要取得的content字段只能通過對表中的數據進行where過濾才能取得,但是后面出現的Usingjoinbuffer是一個啥呢?

實際上,這里的Join正是利用到了我們在之前“MySQLServer性能優化”一章中所提到的一個Cache參數相關的內容,也就是我們通過join_buffer_size參數所設置的JoinBuffer。

實際上,JoinBuffer只有當我們的Join類型為ALL(如示例中),index,rang或者是index_merge的時候才能夠使用,所以,在我們去掉group_message_content表的group_msg_id字段的索引之前,由於Join是ref類型的,所以我們的執行計划中並沒有看到有使用JoinBuffer。

當我們使用了JoinBuffer之后,我們可以通過下面的這個表達式描述出示例中我們的Join完成過程:

for each record g_rec in table user_group{ 
  for each record m_rec in group_message that m_rec.group_id=g_rec.group_id
  { 
put (g_rec, m_rec) into the buffer
if (buffer is full) flush_buffer(); }
} flush_buffer(){
for each record c_rec in group_message_content that c_rec.group_msg_id = c_rec.id{ for each record in the buffer pass (g_rec.user_id, m_rec.subject, c_rec.content) row combination to output; } empty the buffer; }

 

當然,如果通過類似於上面的圖片來展現或許大家會覺得更容易理解一些,如下:

 

 clip_image002[5]

 

通過上面的示例,我想大家應該對MySQL中NestedJoin的實現原理有了一個了解了,也應該清楚MySQL使用JoinBuffer的方法了。當然,這里並沒有涉及到外連接的內容,實際對於外連接來說,可能存在的區別主要是連接順序以及組合空值記錄方面。


Join 語句的優化

在明白了MySQL中Join的實現原理之后,我們就比較清楚的知道該如何去優化一個一個Join語句了。
1.盡可能減少Join語句中的NestedLoop的循環總次數;如何減少NestedLoop的循環總次數?最有效的辦法只有一個,那就是讓驅動表的結果集盡可能的小,這也正是在本章第二節中的優化基本原則之一“永遠用小結果集驅動大的結果集”。


為什么?因為驅動結果集越大,意味着需要循環的次數越多,也就是說在被驅動結果集上面所需要執行的查詢檢索次數會越多。比如,當兩個表(表A和表B)Join的時候,如果表A通過WHERE條件過濾后有10條記錄,而表B有20條記錄。如果我們選擇表A作為驅動表,也就是被驅動表的結果集為20,那么我們通過Join條件對被驅動表(表B)的比較過濾就會有10次。反之,如果我們選擇表B作為驅動表,則需要有20次對表A的比較過濾。


當然,此優化的前提條件是通過Join條件對各個表的每次訪問的資源消耗差別不是太大。如果訪問存在較大的差別的時候(一般都是因為索引的區別),我們就不能簡單的通過結果集的大小來判斷需要Join語句的驅動順序,而是要通過比較循環次數和每次循環所需要的消耗的乘積的大小來得到如何驅動更優化。


2.優先優化NestedLoop的內層循環;

不僅僅是在數據庫的Join中應該做的,實際上在我們優化程序語言的時候也有類似的優化原則。內層循環是循環中執行次數最多的,每次循環節約很小的資源,在整個循環中就能節約很大的資源。


3.保證Join語句中被驅動表上Join條件字段已經被索引;
保證被驅動表上Join條件字段已經被索引的目的,正是針對上面兩點的考慮,只有讓被驅動表的Join條件字段被索引了,才能保證循環中每次查詢都能夠消耗較少的資源,這也正是優化內層循環的實際優化方法。


4.當無法保證被驅動表的Join條件字段被索引且內存資源充足的前提下,不要太吝惜JoinBuffer的設置;

當在某些特殊的環境中,我們的Join必須是All,Index,range或者是index_merge類型的時候,JoinBuffer就會派上用場了。在這種情況下,JoinBuffer的大小將對整個Join語句的消耗起到非常關鍵的作用。

 

轉自 《MySQL性能調優與架構設計》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM