【MySQL】排序原理與案例分析

前言

排序是數據庫中的一個基本功能,MySQL也不例外。用戶經過Order by語句即能達到將指定的結果集排序的目的,其實不只僅是Order by語句,Group by語句,Distinct語句都會隱含使用排序。本文首先會簡單介紹SQL如何利用索引避免排序代價,而後會介紹MySQL實現排序的內部原理,並介紹與排序相關的參數,最後會給出幾個「奇怪」排序例子,來談談排序一致性問題,並說明產生現象的本質緣由。html

排序優化與索引使用

爲了優化SQL語句的排序性能,最好的狀況是避免排序,合理利用索引是一個不錯的方法。由於索引自己也是有序的,若是在須要排序的字段上面創建了合適的索引,那麼就能夠跳過排序的過程,提升SQL的查詢速度。下面我經過一些典型的SQL來講明哪些SQL能夠利用索引減小排序,哪些SQL不能。假設t1表存在索引key1(keypart1,keypart2),key2(key2)mysql

a. 能夠利用索引避免排序的SQL算法

SELECT * FROM t1 ORDER BY key_part1,key_part2;
SELECT * FROM t1 WHERE key_part1 = constant ORDER BY key_part2;
SELECT * FROM t1 WHERE key_part1 > constant ORDER BY key_part1 ASC;
SELECT * FROM t1 WHERE key_part1 = constant1 AND key_part2 > constant2 ORDER BY key_part2;

b. 不能利用索引避免排序的SQLsql

//排序字段在多個索引中,沒法使用索引排序
SELECT * FROM t1 ORDER BY key_part1,key_part2, key2;
 
//排序鍵順序與索引中列順序不一致,沒法使用索引排序
SELECT * FROM t1 ORDER BY key_part2, key_part1;
 
//升降序不一致,沒法使用索引排序
SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;
 
//key_part1是範圍查詢,key_part2沒法使用索引排序
SELECT * FROM t1 WHERE key_part1> constant ORDER BY key_part2;
排序實現的算法

對於不能利用索引避免排序的SQL,數據庫不得不本身實現排序功能以知足用戶需求,此時SQL的執行計劃中會出現「Using filesort」,這裏須要注意的是filesort並不意味着就是文件排序,其實也有多是內存排序,這個主要由sortbuffersize參數與結果集大小肯定。MySQL內部實現排序主要有3種方式,常規排序,優化排序和優先隊列排序,主要涉及3種排序算法:快速排序、歸併排序和堆排序。假設表結構和SQL語句以下:數據庫

CREATE TABLE t1(id int, col1 varchar(64), col2 varchar(64), col3 varchar(64), PRIMARY KEY(id),key(col1,col2));
SELECT col1,col2,col3 FROM t1 WHERE col1>100 ORDER BY col2;

a. 常規排序 緩存

  • 從表t1中獲取知足WHERE條件的記錄
  • 對於每條記錄,將記錄的主鍵+排序鍵(id,col2)取出放入sort buffer
  • 若是sort buffer能夠存放全部知足條件的(id,col2)對,則進行排序;不然sort buffer滿後,進行排序並固化到臨時文件中。(排序算法採用的是快速排序算法)
  • 若排序中產生了臨時文件,須要利用歸併排序算法,保證臨時文件中記錄是有序的
  • 循環執行上述過程,直到全部知足條件的記錄所有參與排序
  • 掃描排好序的(id,col2)對,並利用id去撈取SELECT須要返回的列(col1,col2,col3)
  • 將獲取的結果集返回給用戶。

從上述流程來看,是否使用文件排序主要看sort buffer是否能容下須要排序的(id,col2)對,這個buffer的大小由sortbuffersize參數控制。此外一次排序須要兩次IO,一次是撈(id,col2),第二次是撈(col1,col2,col3),因爲返回的結果集是按col2排序,所以id是亂序的,經過亂序的id去撈(col1,col2,col3)時會產生大量的隨機IO。對於第二次MySQL自己一個優化,即在撈以前首先將id排序,並放入緩衝區,這個緩存區大小由參數readrndbuffer_size控制,而後有序去撈記錄,將隨機IO轉爲順序IO。性能

b.優化排序測試

常規排序方式除了排序自己,還須要額外兩次IO。優化的排序方式相對於常規排序,減小了第二次IO。主要區別在於,放入sort buffer不是(id,col2),而是(col1,col2,col3)。因爲sort buffer中包含了查詢須要的全部字段,所以排序完成後能夠直接返回,無需二次撈數據。這種方式的代價在於,一樣大小的sort buffer,能存放的(col1,col2,col3)數目要小於(id,col2),若是sort buffer不夠大,可能致使須要寫臨時文件,形成額外的IO。固然MySQL提供了參數maxlengthforsortdata,只有當排序元組小於maxlengthforsortdata時,才能利用優化排序方式,不然只能用常規排序方式。優化

c. 優先隊列排序spa

爲了獲得最終的排序結果,不管怎樣,咱們都須要將全部知足條件的記錄進行排序才能返回。那麼相對於優化排序方式,是否還有優化空間呢?5.6版本針對Order by limit M,N語句,在空間層面作了優化,加入了一種新的排序方式--優先隊列,這種方式採用堆排序實現。堆排序算法特徵正好能夠解limit M,N 這類排序的問題,雖然仍然須要全部元素參與排序,可是只須要M+N個元組的sort buffer空間便可,對於M,N很小的場景,基本不會由於sort buffer不夠而致使須要臨時文件進行歸併排序的問題。對於升序,採用大頂堆,最終堆中的元素組成了最小的N個元素,對於降序,採用小頂堆,最終堆中的元素組成了最大的N的元素。

排序不一致問題

a. 案例1

Mysql從5.5遷移到5.6之後,發現分頁出現了重複值。

測試表與數據:

create table t1(id int primary key, c1 int, c2 varchar(128));
insert into t1 values(1,1,'a');
insert into t1 values(2,2,'b');
insert into t1 values(3,2,'c');
insert into t1 values(4,2,'d');
insert into t1 values(5,3,'e');
insert into t1 values(6,4,'f');
insert into t1 values(7,5,'g');

假設每頁3條記錄,第一頁limit 0,3和第二頁limit 3,3查詢結果以下:

root@test 07:58:32>select * from t1 order by c1 asc limit 0,3;
+----+------+------+
| id | c1   | c2   |
+----+------+------+
|  1 |    1 | a    |
|  3 |    2 | c    |
|  4 |    2 | d    |
+----+------+------+
root@test 07:59:11>select * from t1 order by c1 asc limit 3,3;
+----+------+------+
| id | c1   | c2   |
+----+------+------+
|  4 |    2 | d    |
|  5 |    3 | e    |
|  6 |    4 | f    |
+----+------+------+

咱們能夠看到 id爲4的這條記錄竟然同時出如今兩次查詢中,這明顯是不符合預期的,並且在5.5版本中沒有這個問題。產生這個現象的緣由就是5.6針對limit M,N的語句採用了優先隊列,而優先隊列採用堆實現,好比上述的例子order by c1 asc limit 0,3 須要採用大小爲3的大頂堆;limit 3,3須要採用大小爲6的大頂堆。因爲c1爲2的記錄有3條,而堆排序是非穩定的(對於相同的key值,沒法保證排序後與排序前的位置一致),因此致使分頁重複的現象。爲了不這個問題,咱們能夠在排序中加上惟一值,好比主鍵id,這樣因爲id是惟一的,確保參與排序的key值不相同。將SQL寫成以下:

select * from t1 order by c1,id asc limit 0,3;
select * from t1 order by c1,id asc limit 3,3;

b. 案例2

兩個相似的查詢語句,除了返回列不一樣,其它都相同,但排序的結果不一致。 測試表與數據

create table t2(id int primary key, status int, c1 varchar(255),c2 varchar(255),c3 varchar(255),key(c1));
insert into t2 values(7,1,'a',repeat('a',255),repeat('a',255));
insert into t2 values(6,2,'b',repeat('a',255),repeat('a',255));
insert into t2 values(5,2,'c',repeat('a',255),repeat('a',255));
insert into t2 values(4,2,'a',repeat('a',255),repeat('a',255));
insert into t2 values(3,3,'b',repeat('a',255),repeat('a',255));
insert into t2 values(2,4,'c',repeat('a',255),repeat('a',255));
insert into t2 values(1,5,'a',repeat('a',255),repeat('a',255));

分別執行SQL語句:

select id,status,c1,c2 from t2 force index(c1) where c1>='b' order by status;
select id,status from t2 force index(c1) where c1>='b' order by status;

執行結果

root@test 08:01:24>select id,status,c1,c2 from t2 force index(c1) where c1>='b' order by status;
+----+--------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| id | status | c1   | c2                                                                                                                                                                                                                                                              |
+----+--------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|  5 |      2 | c    | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
|  6 |      2 | b    | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
|  3 |      3 | b    | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
|  2 |      4 | c    | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
+----+--------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
4 rows in set (0.00 sec)

root@test 08:01:31>select id,status from t2 force index(c1) where c1>='b' order by status;
+----+--------+
| id | status |
+----+--------+
|  6 |      2 |
|  5 |      2 |
|  3 |      3 |
|  2 |      4 |
+----+--------+
4 rows in set (0.00 sec)

看看二者的執行計劃是否相同

root@test 08:08:10>explain select id,status,c1,c2 from t2 force index(c1) where c1>='b' order by status;     
+----+-------------+-------+-------+---------------+------+---------+------+------+---------------------------------------+
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra                                 |
+----+-------------+-------+-------+---------------+------+---------+------+------+---------------------------------------+
|  1 | SIMPLE      | t2    | range | c1            | c1   | 768     | NULL |    4 | Using index condition; Using filesort |
+----+-------------+-------+-------+---------------+------+---------+------+------+---------------------------------------+

root@test 08:08:17>explain select id,status from t2 force index(c1) where c1>='b' order by status;                            
+----+-------------+-------+-------+---------------+------+---------+------+------+---------------------------------------+
| id | select_type | table | type  | possible_keys | key  | key_len | ref  | rows | Extra                                 |
+----+-------------+-------+-------+---------------+------+---------+------+------+---------------------------------------+
|  1 | SIMPLE      | t2    | range | c1            | c1   | 768     | NULL |    4 | Using index condition; Using filesort |
+----+-------------+-------+-------+---------------+------+---------+------+------+---------------------------------------+

爲了說明問題,我在語句中加了force index的hint,確保能走上c1列索引。語句經過c1列索引撈取id,而後去表中撈取返回的列。根據c1列值的大小,記錄在c1索引中的相對位置以下:

(c1,id)===(b,6),(b,3),(5,c),(c,2),對應的status值分別爲2 3 2 4。從表中撈取數據並按status排序,則相對位置變爲(6,2,b),(5,2,c),(3,3,c),(2,4,c),這就是第二條語句查詢返回的結果,那麼爲何第一條查詢語句(6,2,b),(5,2,c)是調換順序的呢?這裏要看我以前提到的a.常規排序和b.優化排序中標紅的部分,就能夠明白緣由了。因爲第一條查詢返回的列的字節數超過了maxlengthforsortdata,致使排序採用的是常規排序,而在這種狀況下MYSQL將rowid排序,將隨機IO轉爲順序IO,因此返回的是5在前,6在後;而第二條查詢採用的是優化排序,沒有第二次撈取數據的過程,保持了排序後記錄的相對位置。對於第一條語句,若想採用優化排序,咱們將maxlengthforsortdata設置調大便可,好比2048。

root@test 08:11:24>select id,status,c1,c2 from t2 force index(c1) where c1>='b' order by status;
+----+--------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| id | status | c1   | c2                                                                                                                                                                                                                                                              |
+----+--------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|  6 |      2 | b    | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
|  5 |      2 | c    | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
|  3 |      3 | b    | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
|  2 |      4 | c    | aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
+----+--------+------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
參考
  1. MySQL排序原理與案例分析
  2. ORDER BY Optimization
相關文章
相關標籤/搜索