轉 Join的實現原理及優化思路

前言

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

 

Join 的實現原理

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

在MySQL中,只有一種Join算法,就是大名鼎鼎的NestedLoopJoin,他沒有其餘不少數據庫所提供的HashJoin,也沒有SortMergeJoin。顧名思義,NestedLoopJoin實際上就是經過驅動表的結果集做爲循環基礎數據,而後一條一條的經過該結果集中的數據做爲過濾條件到下一個表中查詢數據,而後合併結果。若是還有第三個參與Join,則再經過前兩個表的Join結果集做爲循環基礎數據,再一次經過循環查詢條件到第三個表中查詢數據,如此往復。性能優化

下面咱們將經過一個三表Join語句示例來講明MySQL的NestedLoopJoin實現方式。oop

注意:因爲要展現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);ui

而後看看咱們的Query的執行計劃:spa

複製代碼
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比較進行循環查詢,才獲得最終的結果。3d

這個過程能夠經過以下表達式來表示:

複製代碼
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語句的消耗起到很是關鍵的做用。

相關文章
相關標籤/搜索