mysql ORDER BY,GROUP BY 和DISTINCT原理


前言

除了常規的Join語句之外,還有一類Query語句也是使用比較頻繁的,那就是ORDERBY,GROUP BY以及DISTINCT這三類查詢。考慮到這三類查詢都涉及到數據的排序等操作,所以我將他們放在了一起,下面就針對這三類Query語句做基本的分析。

 

ORDER BY 的實現與優化

在MySQL中,ORDERBY的實現有如下兩種類型:

一種是通過有序索引而直接取得有序的數據,這樣不用進行任何排序操作即可得到滿足客戶端要求的有序數據返回給客戶端;

另外一種則需要通過MySQL的排序算法將存儲引擎中返回的數據進行排序然后再將排序后的數據返回給客戶端。

 

下面我們就針對這兩種實現方式做一個簡單的分析。首先分析一下第一種不用排序的實現方式。同樣還是通過示例來說話吧:

sky@localhost : example 09:48:41> 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: example.m.id
rows: 11
Extra:

看看上面的這個Query語句,明明有ORDER BY user_id,為什么在執行計划中卻沒有排序操作呢?其實這里正是因為MySQL Query Optimizer選擇了一個有序的索引來進行訪問表中的數據(idx_group_message_gid_uid),這樣,我們通過group_id的條件得到的數據已經是按照group_id和user_id進行排序的了。而雖然我們的排序條件僅僅只有一個user_id,但是我們的WHERE條件決定了返回數據的group_id全部一樣,也就是說不管有沒有根據group_id來進行排序,返回的結果集都是完全一樣的。

我們可以通過如下的圖示來描述整個執行過程:

 clip_image002

圖中的TableA和TableB分別為上面Query中的group_message和gruop_message_content這兩個表。

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

如果沒有索引利用的時候,MySQL又如何來實現排序呢?這時候MySQL無法避免需要通過相關的排序算法來將存儲引擎返回的數據進行排序運算了。下面我們再針對這種實現方式進行相應的分析。

在MySQL第二種排序實現方式中,必須進行相應的排序算法來實現數據的排序。MySQL目前可以通過兩種算法來實現數據的排序操作。

取出滿足過濾條件的用於排序條件的字段以及可以直接定位到行數據的行指針信息,在Sort Buffer中進行實際的排序操作,然后利用排好序之后的數據根據行指針信息返回表中取得客戶端請求的其他字段的數據,再返回給客戶端;

根據過濾條件一次取出排序字段以及客戶端請求的所有其他字段的數據,並將不需要排序的字段存放在一塊內存區域中,然后在SortBuffer中將排序字段和行指針信息進行排序,最后再利用排序后的行指針與存放在內存區域中和其他字段一起的行指針信息進行匹配合並結果集,再按照順序返回給客戶端。

 

上面第一種排序算法是MySQL一直以來就有的排序算法,而第二種則是從MySQL4.1版本才開始增加的改進版排序算法。第二種算法與第一種相比較,主要優勢就是減少了數據的二次訪問。在排序之后不需要再一次回到表中取數據,節省了IO操作。當然,第二種算法會消耗更多的內存,正是一種典型的通過內存空間換取時間的優化方式。下面我們同樣通過一個實例來看看當MySQL不得不使用排序算法的時候的執行計划,僅僅只是更改一下排序字段:

sky@localhost : example 10:09:06> 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: example.m.id
rows: 11
Extra:

 

大概一看,好像整個執行計划並沒有什么區別啊?但是細心的讀者朋友可能已經發現,在group_message表的Extra信息中,多了一個“Using filesort”的信息,實際上這就是MySQL Query Optimizer在告訴我們,他需要進行排序操作才能按照客戶端的要求返回有序的數據。執行圖示如下:

clip_image002[5] 

這里我們看到了,MySQL在取得第一個表的數據之后,先根據排序條件將數據進行了一次filesort,也就是排序操作。然后再利用排序后的結果集作為驅動結果集來通過Nested Loop Join訪問第二個表。當然,大家不要誤解,這個filesort並不是說通過磁盤文件進行排序,僅僅只是告訴我們進行了一個排序操作。

上面,我們看到了排序結果集來源僅僅只是單個表的比較簡單的filesort操作。而在我們實際應用中,很多時候我們的業務要求可能並不是這樣,可能需要排序的字段同時存在於兩個表中,或者MySQL在經過一次Join之后才進行排序操作。這樣的排序在MySQL中並不能簡單的里利用Sort Buffer進行排序,而是必須先通過一個臨時表將之前Join的結果集存放入臨時表之后在將臨時表的數據取到Sort Buffer中進行操作。下面我們通過再次更改排序要求來示例這樣的執行計划,當我們選擇通過group_message_content表上面的content字段來進行排序之后:

sky@localhost : example 10:22:42> 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:

這時候的執行計划中出現了“Using temporary”,正是因為我們的排序操作需要在兩個表Join之后才能進行,下圖展示了這個Query的執行過程:

 clip_image002[7]

首先是TableA和TableB進行Join,然后結果集進入臨時表,再進行filesort,最后得到有序的結果集數據返回給客戶端。

上面我們通過兩個不同的示例展示了當MySQL無法避免要使用相應的排序算法進行排序操作的時候的實現原理。雖然在排序過程中所使用的排序算法有兩種,但是兩種排序的內部實現機制大體上差不多。

當我們無法避免排序操作的時候,我們又該如何來優化呢?很顯然,我們應該盡可能讓MySQL選擇使用第二種算法來進行排序。這樣可以減少大量的隨機IO操作,很大幅度的提高排序工作的效率。

1.加大max_length_for_sort_data參數的設置;

在MySQL中,決定使用第一種老式的排序算法還是新的改進算法的依據是通過參數max_length_for_sort_data來決定的。當我們所有返回字段的最大長度小於這個參數值的時候,MySQL就會選擇改進后的排序算法,反之,則選擇老式的算法。所以,如果我們有充足的內存讓MySQL存放需要返回的非排序字段的時候,可以加大這個參數的值來讓MySQL選擇使用改進版的排序算法。

2. 去掉不必要的返回字段;

當我們的內存並不是很充裕的時候,我們不能簡單的通過強行加大上面的參數來強迫MySQL去使用改進版的排序算法,因為如果那樣可能會造成MySQL不得不將數據分成很多段然后進行排使用序,這樣的結果可能會得不償失。在這種情況下,我們就需要去掉不必要的返回字段,讓我們的返回結果長度適應max_length_for_sort_data參數的限制。

3.增大sort_buffer_size參數設置;

增大sort_buffer_size並不是為了讓MySQL可以選擇改進版的排序算法,而是為了讓MySQL可以盡量減少在排序過程中對需要排序的數據進行分段,因為這樣會造成MySQL不得不使用臨時表來進行交換排序。

 

GROUP BY 的實現與優化

由於GROUP BY實際上也同樣需要進行排序操作,而且與ORDER BY相比,GROUP BY主要只是多了排序之后的分組操作。當然,如果在分組的時候還使用了其他的一些聚合函數,那么還需要一些聚合函數的計算。所以,在GROUP BY的實現過程中,與ORDERBY一樣也可以利用到索引。

在MySQL中,GROUP BY的實現同樣有多種(三種)方式,其中有兩種方式會利用現有的索引信息來完成GROUP BY,另外一種為完全無法使用索引的場景下使用。下面我們分別針對這三種實現方式做一個分析。

 

1. 使用松散(Loose)索引掃描實現GROUP BY

何謂松散索引掃描實現GROUP BY呢?實際上就是當MySQL完全利用索引掃描來實現GROUP BY的時候,並不需要掃描所有滿足條件的索引鍵即可完成操作得出結果。

下面我們通過一個示例來描述松散索引掃描實現GROUP BY,在示例之前我們需要首先調整一下group_message表的索引,將gmt_create字段添加到group_id和user_id字段的索引中:

sky@localhost : example 08:49:45> create index idx_gid_uid_gc
-> on group_message(group_id,user_id,gmt_create);
Query OK, rows affected (0.03 sec)
Records: 96 Duplicates: 0 Warnings: 0
sky@localhost : example 09:07:30> drop index idx_group_message_gid_uid
-> on group_message;
Query OK, 96 rows affected (0.02 sec)
Records: 96 Duplicates: 0 Warnings: 0

然后再看如下Query的執行計划:

sky@localhost : example 09:26:15> 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
1 row in set (0.00 sec)

我們看到在執行計划的Extra信息中有信息顯示“Using index for group-by”,實際上這就是告訴我們,MySQLQueryOptimizer通過使用松散索引掃描來實現了我們所需要的GROUP BY操作。

下面這張圖片描繪了掃描過程的大概實現:要利用到松散索引掃描實現GROUP BY,需要至少滿足以下幾個條件:

clip_image002[9]

GROUP BY 條件字段必須在同一個索引中最前面的連續位置;

在使用GROUP BY的同時,只能使用MAX和MIN這兩個聚合函數;

如果引用到了該索引中GROUP BY條件之外的字段條件的時候,必須以常量形式存在;

 

為什么松散索引掃描的效率會很高?

因為在沒有WHERE子句,也就是必須經過全索引掃描的時候,松散索引掃描需要讀取的鍵值數量與分組的組數量一樣多,也就是說比實際存在的鍵值數目要少很多。而在WHERE子句包含范圍判斷式或者等值表達式的時候,松散索引掃描查找滿足范圍條件的每個組的第1個關鍵字,並且再次讀取盡可能最少數量的關鍵字。

 

2. 使用緊湊(Tight)索引掃描實現GROUP BY

緊湊索引掃描實現GROUP BY和松散索引掃描的區別主要在於他需要在掃描索引的時候,讀取所有滿足條件的索引鍵,然后再根據讀取惡的數據來完成GROUP BY操作得到相應結果。

sky@localhost : example 08:55:14> 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
1 row in set (0.01 sec)

這時候的執行計划的Extra信息中已經沒有“Using index for group-by”了,但並不是說MySQL的GROUP BY操作並不是通過索引完成的,只不過是需要訪問WHERE條件所限定的所有索引鍵信息之后才能得出結果。這就是通過緊湊索引掃描來實現GROUP BY的執行計划輸出信息。

下面這張圖片展示了大概的整個執行過程:

clip_image002[11] 

在MySQL中,MySQL Query Optimizer首先會選擇嘗試通過松散索引掃描來實現GROUP BY操作,當發現某些情況無法滿足松散索引掃描實現GROUP BY的要求之后,才會嘗試通過緊湊索引掃描來實現。

當GROUP BY條件字段並不連續或者不是索引前綴部分的時候,MySQL Query Optimizer無法使用松散索引掃描,設置無法直接通過索引完成GROUP BY操作,因為缺失的索引鍵信息無法得到。但是,如果Query語句中存在一個常量值來引用缺失的索引鍵,則可以使用緊湊索引掃描完成GROUP BY操作,因為常量填充了搜索關鍵字中的“差距”,可以形成完整的索引前綴。這些索引前綴可以用於索引查找。而如果需要排序GROUP BY結果,並且能夠形成索引前綴的搜索關鍵字,MySQL還可以避免額外的排序操作,因為使用有順序的索引的前綴進行搜索已經按順序檢索到了所有關鍵字。3. 使用臨時表實現GROUP BY

MySQL在進行GROUP BY操作的時候要想利用所有,必須滿足GROUP BY的字段必須同時存放於同一個索引中,且該索引是一個有序索引(如Hash索引就不能滿足要求)。而且,並不只是如此,是否能夠利用索引來實現GROUP BY還與使用的聚合函數也有關系。

前面兩種GROUP BY的實現方式都是在有可以利用的索引的時候使用的,當MySQL Query Optimizer無法找到合適的索引可以利用的時候,就不得不先讀取需要的數據,然后通過臨時表來完成GROUP BY操作。

sky@localhost : example 09:02:40> 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

這次的執行計划非常明顯的告訴我們MySQL通過索引找到了我們需要的數據,然后創建了臨時表,又進行了排序操作,才得到我們需要的GROUP BY結果。整個執行過程大概如下圖所展示:

clip_image002[13] 

當MySQL Query Optimizer發現僅僅通過索引掃描並不能直接得到GROUP BY的結果之后,他就不得不選擇通過使用臨時表然后再排序的方式來實現GROUP BY了。

在這樣示例中即是這樣的情況。group_id並不是一個常量條件,而是一個范圍,而且GROUP BY字段為user_id。所以MySQL無法根據索引的順序來幫助GROUP BY的實現,只能先通過索引范圍掃描得到需要的數據,然后將數據存入臨時表,然后再進行排序和分組操作來完成GROUP BY。

對於上面三種MySQL處理GROUP BY的方式,我們可以針對性的得出如下兩種優化思路:

1.盡可能讓MySQL可以利用索引來完成GROUP BY操作,當然最好是松散索引掃描的方式最佳。在系統允許的情況下,我們可以通過調整索引或者調整Query這兩種方式來達到目的;

2.當無法使用索引完成GROUP BY的時候,由於要使用到臨時表且需要filesort,所以我們必須要有足夠的sort_buffer_size來供MySQL排序的時候使用,而且盡量不要進行大結果集的GROUP BY操作,因為如果超出系統設置的臨時表大小的時候會出現將臨時表數據copy到磁盤上面再進行操作,這時候的排序分組操作性能將是成數量級的下降;

 

至於如何利用好這兩種思路,還需要大家在自己的實際應用場景中不斷的嘗試並測試效果,最終才能得到較佳的方案。此外,在優化GROUP BY的時候還有一個小技巧可以讓我們在有些無法利用到索引的情況下避免filesort操作,也就是在整個語句最后添加一個以null排序(ORDER BY null)的子句,大家可以嘗試一下試試看會有什么效果。

 

DISTINCT 的實現與優化

DISTINCT實際上和GROUP BY的操作非常相似,只不過是在GROUP BY之后的每組中只取出一條記錄而已。所以,DISTINCT的實現和GROUP BY的實現也基本差不多,沒有太大的區別。同樣可以通過松散索引掃描或者是緊湊索引掃描來實現,當然,在無法僅僅使用索引即能完成DISTINCT的時候,MySQL只能通過臨時表來完成。但是,和GROUP BY有一點差別的是,DISTINCT並不需要進行排序。也就是說,在僅僅只是DISTINCT操作的Query如果無法僅僅利用索引完成操作的時候,MySQL會利用臨時表來做一次數據的“緩存”,但是不會對臨時表中的數據進行filesort操作。當然,如果我們在進行DISTINCT的時候還使用了GROUP BY並進行了分組,並使用了類似於MAX之類的聚合函數操作,就無法避免filesort了。

下面我們就通過幾個簡單的Query示例來展示一下DISTINCT的實現。

 

1.首先看看通過松散索引掃描完成DISTINCT的操作:

sky@localhost :  example 11:03:41> 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
1  row  in  set  (0.00 sec)

 

我們可以很清晰的看到,執行計划中的Extra信息為“Using index for group-by”,這代表什么意思?為什么我沒有進行GROUP BY操作的時候,執行計划中會告訴我這里通過索引進行了GROUP BY呢?其實這就是於DISTINCT的實現原理相關的,在實現DISTINCT的過程中,同樣也是需要分組的,然后再從每組數據中取出一條返回給客戶端。而這里的Extra信息就告訴我們,MySQL利用松散索引掃描就完成了整個操作。當然,如果MySQL Query Optimizer要是能夠做的再人性化一點將這里的信息換成“Using index for distinct”那就更好更容易讓人理解了,呵呵。

 

2.  我們再來看看通過緊湊索引掃描的示例:

sky@localhost : example 11:03:53> 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
1 row in set (0.00 sec)

這里的顯示和通過緊湊索引掃描實現GROUP BY也完全一樣。實際上,這個Query的實現過程中,MySQL會讓存儲引擎掃描group_id=2的所有索引鍵,得出所有的user_id,然后利用索引的已排序特性,每更換一個user_id的索引鍵值的時候保留一條信息,即可在掃描完所有gruop_id=2的索引鍵的時候完成整個DISTINCT操作。

3.下面我們在看看無法單獨使用索引即可完成DISTINCT的時候會是怎樣:

sky@localhost : example 11:04:40> 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
1 row in set (0.00 sec)

當MySQL無法僅僅依賴索引即可完成DISTINCT操作的時候,就不得不使用臨時表來進行相應的操作了。但是我們可以看到,在MySQL利用臨時表來完成DISTINCT的時候,和處理GROUP BY有一點區別,就是少了filesort。實際上,在MySQL的分組算法中,並不一定非要排序才能完成分組操作的,這一點在上面的GROUP BY優化小技巧中我已經提到過了。實際上這里MySQL正是在沒有排序的情況下實現分組最后完成DISTINCT操作的,所以少了filesort這個排序操作。

4.最后再和GROUP BY結合試試看:

sky@localhost : example 11:05:06> 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
1 row in set (0.00 sec)

最后我們再看一下這個和GROUP BY一起使用帶有聚合函數的示例,和上面第三個示例相比,可以看到已經多了filesort排序操作了,因為我們使用了MAX函數的緣故。

對於DISTINCT的優化,和GROUP BY基本上一致的思路,關鍵在於利用好索引,在無法利用索引的時候,確保盡量不要在大結果集上面進行DISTINCT操作,磁盤上面的IO操作和內存中的IO操作性能完全不是一個數量級的差距。

 

總結

本章重點介紹了MySQL Query語句相關的性能調優的部分思路和方法,也列舉了部分的示例,希望能夠幫助讀者朋友在實際工作中開闊一點點思路。雖然本章涉及到的內容包含了最初的索引設計,到編寫高效Query語句的一些原則,以及最后對語句的調試,但Query語句的調優遠不只這些內容。很多的調優技巧,只有到在實際的調優經驗中才會真正體會,真正把握其精髓。所以,希望各位讀者朋友能多做實驗,以理論為基礎,以事實為依據,只有這樣,才能不斷提升自己對Query調優的深入認識。

 

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


免責聲明!

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



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