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: 1SELECT_type:SIMPLE
table:group_message
type:range
possible_keys:NULL
key: idx_gid_uid_gc
key_len:4ref: NULL
rows:10Extra: Using index for group-by
1 row in set (0.00sec)
我們可以很清晰的看到,執行計划中的Extra信息為“Usingindex for group-by”,這代表什么意思?為什么我沒有進行GROUP BY操作的時候,執行計划中會告訴我這里通過索引進行了GROUP BY呢?其實這就是於DISTINCT的實現原理相關的,在實現DISTINCT的過程中,同樣也是需要分組的,然后再從每組數據中取出一條返回給客戶端。而這里的Extra信息就告訴我們,MySQL利用松散索引掃描就完成了整個操作。當然,如果MySQLQuery 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:1SELECT_type: SIMPLE
table:group_message
type:ref
possible_keys:idx_gid_uid_gc
key:idx_gid_uid_gc
key_len:4ref: const
rows:4Extra: Using WHERE; Using index
1row 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:1SELECT_type: SIMPLE
table:group_message
type:range
possible_keys:idx_gid_uid_gc
key:idx_gid_uid_gc
key_len:4ref: NULL
rows:32Extra: Using WHERE; Using index; Using temporary
1row 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:1SELECT_type: SIMPLE
table:group_message
type:range
possible_keys:idx_gid_uid_gc
key:idx_gid_uid_gc
key_len:4ref: NULL
rows:32Extra: Using WHERE; Using index; Using temporary; Usingfilesort
1row in set (0.00 sec)
最后我們再看一下這個和GROUP BY一起使用帶有聚合函數的示例,和上面第三個示例相比,可以看到已經多了filesort排序操作了,因為我們使用了MAX函數的緣故。
對於DISTINCT的優化,和GROUP BY基本上一致的思路,關鍵在於利用好索引,在無法利用索引的時候,確保盡量不要在大結果集上面進行DISTINCT操作,磁盤上面的IO操作和內存中的IO操作性能完全不是一個數量級的差距。