Mysql 排序優化與索引使用(轉)


  為了優化SQL語句的排序性能,最好的情況是避免排序,合理利用索引是一個不錯的方法。因為索引本身也是有序的,如果在需要排序的字段上面建立了合適的索引,那么就可以跳過排序的過程,提高SQL的查詢速度。下面我通過一些典型的SQL來說明哪些SQL可以利用索引減少排序,哪些SQL不能。假設t1表存在索引key1(key_part1,key_part2),key2(key2)

a.可以利用索引避免排序的SQL

1
2
3
4
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.不能利用索引避免排序的SQL

1
2
3
4
5
6
7
8
9
10
11
//排序字段在多個索引中,無法使用索引排序
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;

2.排序實現的算法
      對於不能利用索引避免排序的SQL,數據庫不得不自己實現排序功能以滿足用戶需求,此時SQL的執行計划中會出現“Using filesort”,這里需要注意的是filesort並不意味着就是文件排序,其實也有可能是內存排序,這個主要由sort_buffer_size參數與結果集大小確定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.常規排序,雙路排序
(1).從表t1中獲取滿足WHERE條件的記錄
(2).對於每條記錄,將記錄的主鍵+排序鍵(id,col2)取出放入sort buffer
(3).如果sort buffer可以存放所有滿足條件的(id,col2)對,則進行排序;否則sort buffer滿后,進行排序並寫到臨時文件中。(排序算法采用的是快速排序算法)
(4).若排序中產生了臨時文件,需要利用歸並排序算法,保證臨時文件中記錄是有序的
(5).循環執行上述過程,直到所有滿足條件的記錄全部參與排序
(6).掃描排好序的(id,col2)隊,即sort buffer,並利用主鍵id去取SELECT需要返回的其他列(col1,col2,col3)
(7).將獲取的結果集返回給用戶。
      從上述流程來看,是否使用文件排序主要看sort buffer是否能容下需要排序的(id,col2)的結果集,這個buffer的大小由sort_buffer_size參數控制。此外一次排序還需要兩次IO一次是取排序字段(id,col2)到sort buffer中,第二次是通過上面取出的主鍵id再來取其他所需要返回列(col1,col2,col3),由於返回的結果集是按col2排序,因此id是亂序的,通過亂序的id取(col1,col2,col3)時會產生大量的隨機IO。對於第二次IO取MySQL本身會優化,即在取之前先將主鍵id排序,並放入緩沖區,這個緩存區大小由參數read_rnd_buffer_size控制,然后有序去取記錄,將隨機IO轉為順序IO
b.優化排序,單路排序,max_length_for_sort_data
     常規排序方式除了排序本身,還需要額外兩次IO。優化排序方式相對於常規排序,減少了第二次IO主要區別在於,一次性取出sql中出現的所有字段放入sort buffer中而不是只取排序需要的字段(id,col2)。由於sort buffer中包含了查詢需要的所有字段,因此排序完成后可以直接返回,無需二次取數據。這種方式的代價在於,同樣大小的sort buffer,能存放的(col1,col2,col3)數目要小於(id,col2),如果sort buffer不夠大,可能導致需要寫臨時文件,造成額外的IO。當然MySQL提供了參數max_length_for_sort_data,只有當排序sql里出現的所有字段小於max_length_for_sort_data時,才能利用優化排序方式,否則只能用常規排序方式。
c.優先隊列排序
     為了得到最終的排序結果,我們都需要將所有滿足條件的記錄進行排序才能返回。那么相對於優化排序方式,是否還有優化空間呢?5.6版本針對Order by limit M,N語句,在空間層面做了優化,加入了一種新的排序方式--優先隊列,這種方式采用堆排序實現。堆排序算法特征正好可以解limit M,N 這類排序的問題,雖然仍然需要所有字段參與排序,但是只需要M+N個元組的sort buffer空間即可,對於M,N很小的場景,基本不會因為sort buffer不夠而導致需要臨時文件進行歸並排序的問題。對於升序,采用大頂堆,最終堆中的元素組成了最小的N個元素,對於降序,采用小頂堆,最終堆中的元素組成了最大的N的元素。

3.排序不一致問題

案例1:order by no_index limit n在MySQL5.5和5.6中的不一致

MySQL從5.5遷移到5.6以后,發現分頁出現了重復值(排序字段沒有用索引,或則直接是全表掃描),MariaDB已經是優化后的方案,和5.6一致。

問題源頭:https://bbs.aliyun.com/read/248026.html,解決辦法:http://mysql.taobao.org/monthly/2015/06/04/

測試表與數據:

復制代碼
復制代碼
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查詢結果如下:

我們可以看到 id為4的這條記錄居然同時出現在兩次查詢中,這明顯是不符合預期的,而且在5.5版本中沒有這個問題。

使用優先隊列排序的目的就是在不能使用索引有序性的時候,如果要排序,並且使用了limit n,那么只需要在排序的過程中,保留n條記錄即可,這樣雖然不能解決所有記錄都需要排序的開銷,但是只需要 sort buffer 少量的內存就可以完成排序,上面已經說明。

之所以MySQL5.6出現了第二頁數據重復的問題,是因為使用了優先隊列排序,其使用了堆排序的排序方法,而堆排序是一個不穩定的排序方法,也就是相同的值(例子中的值2)可能排序出來的數據和讀出來的數據順序不一致,無法保證排序前后數據位置的一致,所以導致分頁重復的現象

為了避免這個問題,有幾種方法:

①:索引排序字段

利用索引的有序性,在字段添加上索引,就直接按照索引的有序性進行讀取並分頁,從而可以規避遇到的這個問題。

②:利用多列索引,對於單列相同無法排序的,利用其主鍵進行排序:

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

案例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;

執行結果如下:

看看兩者的執行計划是否相同

    為了說明問題,因為測試數據不多,確保能走上c1列索引,加了force index的hint。語句通過c1列索引取id,然后去表中撈取返回的列。根據c1列值的大小,記錄在c1索引中的相對位置如下:

(c1,id)<===>(b,6),(b,3),(c,5),(c,2),

對應的status值分別為2,3,2,4。從表中取數據並按status排序,則相對位置變為(6,2,b),(5,2,c),(3,3,b),(2,4,c),這就是第二條語句查詢返回的結果,那么為什么第一條查詢語句(6,2,b),(5,2,c)是調換順序的呢?

這里說明下:
1. Query 語句所取出的字段類型大小總和小於max_length_for_sort_data 。
2. 排序的字段不包含TEXT和BLOB類型。

之前提到的優化排序就可以明白了:由於第一條查詢返回的列的字節數超過了max_length_for_sort_data,導致排序采用常規排序,而在這種情況下第二次IO時,MYSQL本身優化會對id排序,將隨機IO轉為順序IO,所以返回的先是5,后是6;而第二條查詢采用的是優化排序,沒有第二次取數據的過程,保持了排序后記錄的相對位置,直接在sort buffer里取出。對於第一條語句,若想采用優化排序,我們將max_length_for_sort_data設置調大即可,比如2048。


免責聲明!

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



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