1. 程式人生 > >mysql的order by,group by和distinct優化

mysql的order by,group by和distinct優化

order by,group by和distinct三類操作是在mysql中經常使用的,而且都涉及到排序,所以就把這三種操作放在一起介紹。

order by的實現與優化

order by的實現有兩種方式,主要就是按用沒用到索引來區分:

1. 根據索引欄位排序,利用索引取出的資料已經是排好序的,直接返回給客戶端;

2. 沒有用到索引,將取出的資料進行一次排序操作後返回給客戶端。

下面通過示例來介紹這兩種方式間的差異,首先利用索引進行order by操作,使用explain分析得出的執行計劃:

EXPLAIN SELECT m.id,m.subject,c.content FROM group_message m,group_message_content c WHERE m.group_id = 1 AND m.id = c.group_msg_id ORDER BY m.user_id\G;
*************************** 1. 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: const
rows: 4
Extra: Using where
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: c
type: ref
possible_keys: group_message_content_msg_id
key: group_message_content_msg_id
key_len: 4
ref: m.id
rows: 11
Extra:

從執行計劃裡可以看出,進行了order by操作但是執行計劃裡並沒有排序操作,因為optimizer對query進行了優化,它會按照m.user_id上的索引順序來訪問資料,這樣獲取的資料已經是排好序的。

這種利用索引實現資料排序的方法是 MySQL 中實現結果集排序的最佳做法,可以完全避免因為排序所帶來的資源消耗。所以,在我們優化 query 語句中的 order by 的時候,儘可能利用已有的索引避免實際的排序計算,可以很大幅度的提升 order by 操作的效能。在有些 query 的優化過程中,為了避免實際的排序操作而調整索引欄位的順序,甚至是增加索引欄位也是值得的。當然,在調整索前,同時還需要評估調整該索引對其他 query 所帶來的影響,平衡整體得失。

如果沒有用到索引,mysql就會將取出的資料按照一定的排序演算法進行排序,然後再把排好序的資料返回給客戶端。mysql中主要使用兩種排序演算法:

1. 取出用於排序的條件欄位和指向相應資料行的指標,在sort buffer中對條件進行排序,排好序之後利用指標取出資料行中的請求資料,然後返回給客戶端;

2. 取出用於排序的條件欄位和其它所有請求資料,將不用於排序的欄位存放在一塊記憶體中,然後在sort buffer中對條件欄位進行排序,排好序後利用行指標將在記憶體中的資料進行匹配合並結果集,然後將排好序的資料返回給客戶端。

第二種排序演算法是mysql4.1版本才開始支援的,第一種演算法是各個版本都支援的。兩種演算法的比較,第二種利用記憶體減少了資料的二次訪問,節省了IO操作,是一種空間換時間的優化方式。

下面用一個例項來分析不使用索引時的執行計劃:

EXPLAIN SELECT m.id,m.subject,c.content FROM group_message m,group_message_content c WHERE m.group_id = 1 AND m.id = c.group_msg_id ORDER BY m.subject\G;
*************************** 1. 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: const
rows: 4
Extra: Using where; Using filesort
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: c
type: ref
possible_keys: group_message_content_msg_id
key: group_message_content_msg_id
key_len: 4
ref: m.id
rows: 11
Extra:

從執行計劃裡可以看出,在對group_message進行資料訪問的時候,extra資訊裡顯示Using filesort,這就表示從group_message獲取資料後需要對資料進行排序操作,然後再利用排序後的結果集來驅動第二個表。

對於更復雜的情況,比如用於排序的欄位存在在多個表中,或者在排序之前要先經過join操作,這樣mysql必須先把join的結果集放入一個臨時表,之後再把臨時表中的資料取到sort buffer裡進行排序。下面再用一個簡單的例項來分析optimizer給出的執行計劃。

explain select m.id,m.subject,c.content FROM group_message m,group_message_content c 
WHERE m.group_id = 1 AND m.id = c.group_msg_id ORDER BY c.content\G;

給出的執行計劃:

*************************** 1. 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: const
rows: 4
Extra: Using temporary; Using filesort
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: c
type: ref
possible_keys: group_message_content_msg_id
key: group_message_content_msg_id
key_len: 4
ref: example.m.id
rows: 11
Extra:

可以看見Extra資訊顯示Using temporary,這就表示將兩個表的join內容取出並放進到一個臨時表中之後再進行filesort。

通過以上介紹知道order by的優化很簡單,就是讓mysql使用第二種排序演算法,這樣可以減少大量的IO操作,提高效能,但是如何做到呢:

1. 加大max_length_for_sort_data引數的設定。mysql通過該引數來決定使用哪種排序演算法,當需要取出的所有資料長度小於這個引數的值的時候,mysql將採用第二中改進的排序演算法,否則,使用第一種演算法,所以只要記憶體充足就可以設定足夠大的值來讓mysql採用改進的排序演算法;

2. 去掉不必要的返回欄位,很容易從上一點知道原因;

3. 增大sort_buffer_size引數的值,當mysql對條件欄位進行排序時,如果需要排序欄位的總長度大於該引數的值的時候,mysql就會對需要排序的欄位使用臨時表進行分段,這樣也會有效能的消耗。

group by的實現與優化

group by的實現過程除了要使用排序操作外,還要進行分組操作,如果使用到一些聚合函式,還要進行相應的聚合計算。group by的實現方式根據是否使用到索引分為三種:

1. 使用鬆散(Loose)索引掃描實現group by,所謂的鬆散索引掃描,就是mysql不需要掃描所有滿足條件的索引鍵即可完成group by操作,下面通過一個簡單的例項來分析一下這個過程。

create index idx_gid_uid_gc on group_message(group_id,user_id,gmt_create);
drop index idx_group_message_gid_uid on group_message;
EXPLAIN SELECT user_id,max(gmt_create) FROM group_message WHERE group_id < 10 GROUP BY group_id,user_id\G;

得到的執行計劃:

*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: group_message
type: range
possible_keys: idx_gid_uid_gc
key: idx_gid_uid_gc
key_len: 8
ref: NULL
rows: 4
Extra: Using where; Using index for group-by

可以看見Extra資訊顯示了Using index for group-by,這就是說mysql使用了鬆散索引掃描來實現group by操作。

要利用到鬆散索引掃描實現group by,需要滿足以下幾個條件:

  • group by條件欄位必須在同一個索引中最前面的連續位置;
  • 只能使用max和min這兩個聚合函式;
  • 索引的任何其它部分(除了那些來自查詢中引用的GROUP BY)必須為常數(也就是說,必須按常量數量來引用它們),但min或max 函式的引數例外。

為什麼鬆散索引的效率會高很多?

當沒有where條件,即必須經過全索引掃描的時候,鬆散索引掃描的鍵值數量與分組的分組量一樣多,也就是比實際存在的鍵值數量小很多。而當有where條件包含範圍判斷式或者等值表示式的時候,鬆散索引掃描查詢滿足範圍條件的每個組的第1個關鍵字。

2. 使用緊湊(Tight)索引掃描實現group by,緊湊索引與鬆散索引最主要的區別就是在需要掃描索引的時候,緊湊索引讀取所有滿足條件的索引鍵,然後再來使用group by操作得到相應的結果。下面同樣用一個例子來分析。

EXPLAIN SELECT max(gmt_create) FROM group_message WHERE group_id = 2 GROUP BY user_id\G;

得出的執行計劃如下:

*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: group_message
type: ref
possible_keys: idx_group_message_gid_uid,idx_gid_uid_gc
key: idx_gid_uid_gc
key_len: 4
ref: const
rows: 4
Extra: Using where; Using index

從執行計劃裡看見extra資訊顯示Using index而不是Using index for group by,意味著需要訪問where條件所限定的所有索引鍵資訊之後才能得出結果。optimizer首先會嘗試通過鬆散索引掃描來完成group by操作,當發現有些情況(當group by 條件欄位不連續或者不是索引字首部分的時候,optimizer無法使用鬆散索引掃描)不滿足鬆散索引時,才會選擇緊湊索引掃描來實現。

3. 使用臨時表實現group by

group by操作想要利用索引,必須滿足group by欄位必須同時存放於同一個索引中,且該索引是一個有序索引,而且,使用不同的聚合函式也會影響是否使用索引來實現group by操作。當optimizer無法找到合適的索引可以利用的時候,就會選擇將讀取的資料放入臨時表中來完成group by操作。下面通過一個簡單的例子來演示這個過程。

EXPLAIN SELECT max(gmt_create) FROM group_message WHERE group_id > 1 and group_id < 10 GROUP BY user_id\G;
得到的執行計劃如下:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: group_message
type: range
possible_keys: idx_group_message_gid_uid,idx_gid_uid_gc
key: idx_gid_uid_gc
key_len: 4
ref: NULL
rows: 32
Extra: Using where; Using index; Using temporary; Using filesort
從執行計劃的extra資訊中可以看見,對這次的group by操作進行了使用臨時表(Using temporary)然後進行了排序操作才完成的。因為group_id並不是一個常量,而是一個範圍,並且group by的欄位為user_id,mysql無法根據索引的順序來完成group by的實現,只能先通過索引範圍掃描得到需要的資料,然後將資料放入一個臨時表,然後再進行排序和分組操作最終完成group by操作。

上面介紹了實現group by操作的三種方式,可以得出以下幾點用於group by優化:

1. 儘可能利用索引並且是鬆散索引來完成group by操作,這的依靠調整索引或者調整query來實現;

2. 當無法利用索引的時候,必須要提供足夠的sort_buffer_size來供mysql完成排序操作,之前介紹過,不然mysql會將需要排序的欄位進行分段排序,會影響效能。除此之外儘量不要對大結果集進行group by操作,因為一旦資料量超過系統最大臨時表大小時,mysql會將臨時表裡的資料copy到磁碟上然後再進行操作,效能會成數量級的下降。

distinct的實現與優化

distinct的實現原理同group by類似,實現過程只是在group by之後只取出每一組中的第一條記錄,所以distinct同樣可以利用鬆散或者緊湊索引來實現,不同的是,當無法利用索引實現distinct時,mysql同樣會將資料取出放進一個臨時表,不過不會對臨時表進行排序操作。下面同樣通過一些簡單的例子來顯示其實現過程。

1.使用鬆散索引完成distinct操作:

EXPLAIN SELECT DISTINCT group_id FROM group_message\G;
得到的執行計劃如下:
*************************** 1. row ***************************
id: 1
SELECT_type: SIMPLE
table: group_message
type: range
possible_keys: NULL
key: idx_gid_uid_gc
key_len: 4
ref: NULL
rows: 10
Extra: Using index for group-by
從extra資訊裡可以看見Using index for group by,意味著mysql使用了鬆散索引來完成group by操作,然後取出每組中的第一條資料來完成distinct操作。

2. 使用緊湊索引完成distinct操作:

EXPLAIN SELECT DISTINCT user_id FROM group_message where group_id =2\G;
得到的執行計劃如下:
*************************** 1. row ***************************
id: 1
SELECT_type: SIMPLE
table: group_message
type: ref
possible_keys: idx_gid_uid_gc
key: idx_gid_uid_gc
key_len: 4
ref: const
rows: 4
Extra: Using WHERE; Using index
extra資訊顯示Using index,說明使用了緊湊索引。mysql讓儲存引擎掃描group_id=2的所有索引鍵,得出所有的user_id,因為是聯合索引,所以取出的user_id已經是排好序的,對相同的user_id取出一條記錄即完成了本次distinct操作。

3. 無法利用索引完成distinct操作:

EXPLAIN SELECT DISTINCT user_id FROM group_message WHERE group_id > 1 AND group_id < 10\G;
得到的執行計劃如下:
*************************** 1. row ***************************
id: 1
SELECT_type: SIMPLE
table: group_message
type: range
possible_keys: idx_gid_uid_gc
key: idx_gid_uid_gc
key_len: 4
ref: NULL
rows: 32
Extra: Using WHERE; Using index; Using temporary
從extra資訊看出,mysql使用的臨時表,但並沒有進行排序操作。

4. 同時使用distinct和group by:

EXPLAIN SELECT DISTINCT max(user_id) FROM group_message WHERE group_id > 1 AND group_id < 10 GROUP BY group_id\G;
得到的執行計劃如下:
*************************** 1. row ***************************
id: 1
SELECT_type: SIMPLE
table: group_message
type: range
possible_keys: idx_gid_uid_gc
key: idx_gid_uid_gc
key_len: 4
ref: NULL
rows: 32
Extra: Using WHERE; Using index; Using temporary; Using filesort
從extra裡看出mysql使用了排序操作,因為進行了group by操作。

因為distinct的實現原理同group by類似,所以優化手段也一樣,儘量使用索引,無法使用索引的時候,確保不要在大結果集上進行distinct操作,磁碟上的IO操作和記憶體中的IO操作效能完全不是一個數量級的差距。