資料庫查詢優化:通過 EXPLAIN 淺析資料庫查詢優化方法
通過 EXPLAIN 淺析資料庫查詢優化方法
EXPLAIN 思維導圖
知識點
explain 或者 desc 命令
獲取Myswl如何執行SELECT語句的資訊,包括在SELECT語句執行過程中表如何連線和連線的順序。
- index:
這種型別表示是mysql會對整個該索引進行掃描。
要想用到這種型別的索引,對這個索引並無特別要求,只要是索引,或者某個複合索引的一部分,mysql都可能會採用index型別的方式掃描。
缺點是效率不高,mysql會從索引中的第一個資料一個個的查詢到最後一個數據,直到找到符合判斷條件的某個索引。
- ref:
這種型別表示mysql會根據特定的演算法快速查詢到某個符合條件的索引,而不是會對索引中每一個數據都進行一 一的掃描判斷,也就是平常理解的使用索引查詢會更快的取出資料。而要想實現這種查詢,索引卻是有要求的,要實現這種能快速查詢的演算法,索引就要滿足特定的資料結構。
索引欄位的資料必須有序才能實現這種型別的查詢,才能利用到索引。
- Using index
列資料是從僅僅使用了索引中的資訊而沒有讀取實際的行動的表返回的,這發生在對錶的全部的請求列都是同一個索引的部分的時候。
- Using where
使用了WHERE從句來限制哪些行將與下一張表匹配或者是返回給使用者。
在一般稍大的系統中,基本儘可能的減少join ,子查詢等等。mysql就使用最簡單的查詢,這樣效率最高。至於 join 等,可以放在應用層去解決。
- Using temporary
優化。MYSQL需要建立一個臨時表來儲存結果,這通常發生在對不同的列集進行ORDER BY上,而不是GROUP BY上
- Using filesort
優化。MYSQL需要進行額外的步驟來發現如何對返回的行排序。它根據連線型別以及儲存排序鍵值和匹配條件的全部行的行指標來排序全部行
執行計劃中什麼情況下會出現using filesort和using tempatory?
通常當查詢中出現連線(JOIN)且連線中的所有列都來自連線的第一個表時,就會出現using filesort。
除此之外的所有情況,mysql都會先將連線結果放到臨時表中,然後在所有的連線都結束後,再進行檔案排序(實際上是快速排序),不管這種排序是在記憶體中還是在硬碟中進行的。
即使查詢中出現limit 子句,也會在排序結束後才做分頁處理,所以實際上臨時表和需要排序的資料量依舊會非常大。
當執行計劃中出現使用臨時表的情況時,首先要檢查一下是不是ORDER BY 和GROUP BY 語句中欄位的問題。
如果調整過欄位還是出現使用臨時表的情況,考慮聯合索引來覆蓋這些欄位,並儘可能的使用小結果集驅動大結果集。
explain 示例程式碼
mysql > explain select A.id , A.title , B.title from base_content A left join base_categories B on A.catid = B.id left join base_sections C on A.sectionid = C.id order by B.id ; +----+-------------+-------+--------+---------------+---------+---------+-------------------------+-------+---------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+---------------+---------+---------+-------------------------+-------+---------------------------------+ | 1 | SIMPLE | A | ALL | NULL | NULL | NULL | NULL | 46585 | Using temporary ; Using filesort | | 1 | SIMPLE | B | eq_ref | PRIMARY | PRIMARY | 4 | joomla_test . A . catid | 1 | | | 1 | SIMPLE | C | eq_ref | PRIMARY | PRIMARY | 4 | joomla_test . A . sectionid | 1 | Using index | +----+-------------+-------+--------+---------------+---------+---------+-------------------------+-------+---------------------------------+ 3 rows in set ( 0.00 sec )
更改排序欄位後:
mysql > explain select A.id , A.title , B.title from base_content A left join base_categories B on A.catid = B.id left join base_sections C on A.sectionid = C.id order by A.id ; +----+-------------+-------+--------+---------------+---------+---------+-------------------------+-------+----------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+---------------+---------+---------+-------------------------+-------+----------------+ | 1 | SIMPLE | A | ALL | NULL | NULL | NULL | NULL | 46585 | Using filesort | | 1 | SIMPLE | B | eq_ref | PRIMARY | PRIMARY | 4 | joomla_test . A . catid | 1 | | | 1 | SIMPLE | C | eq_ref | PRIMARY | PRIMARY | 4 | joomla_test . A . sectionid | 1 | Using index | +----+-------------+-------+--------+---------------+---------+---------+-------------------------+-------+----------------+
第一個使用了Using temporary,而第二個沒有用呢?
因為如果有ORDER BY子句和一個不同的GROUP BY子句,或者如果ORDER BY或GROUP BY中的欄位都來自其他的表而非連線順序中的第一個表的話,就會建立一個臨時表了。
那麼,對於上面例子中的第一條語句,我們需要對base_categories的id進行排序,可以將SQL做如下改動:
mysql > explain select B.id , B.title , A.title from base_categories A left join base_content B on A.id = B.catid left join base_sections C on B.sectionid = C.id order by A.id ; +----+-------------+-------+--------+---------------+-----------+---------+-------------------------+------+----------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+---------------+-----------+---------+-------------------------+------+----------------+ | 1 | SIMPLE | A | ALL | NULL | NULL | NULL | NULL | 18 | Using filesort | | 1 | SIMPLE | B | ref | idx_catid | idx_catid | 4 | joomla_test . A . id | 3328 | | | 1 | SIMPLE | C | eq_ref | PRIMARY | PRIMARY | 4 | joomla_test . B . sectionid | 1 | Using index | +----+-------------+-------+--------+---------------+-----------+---------+-------------------------+------+----------------+ 3 rows in set ( 0.00 sec )
這樣我們發現,不會再有Using temporary了,而且在查詢base_content時,查詢的記錄明顯有了數量級的降低,這是因為base_content的idx_catid起了作用。
結論:
為提高查詢效率,應儘量對第一個表的索引鍵進行排序
擴充套件:
Using filesort 字面意思:使用檔案進行排序或中檔案中進行排序。
這是不正確的,當我們試圖對一個沒有索引的欄位進行排序時,就是filesoft。它跟檔案沒有任何關係,實際上是內部的一個快速排序。
我們再看:
mysql > explain select A.id , A.title , B.title from base_content A , base_categories B , base_sections C where A.catid = B.id and A.sectionid = C.id order by C.id ; +----+-------------+-------+--------+-----------------------+-------------+---------+---------------------+-------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+-----------------------+-------------+---------+---------------------+-------+-------------+ | 1 | SIMPLE | C | index | PRIMARY | PRIMARY | 4 | NULL | 1 | Using index | | 1 | SIMPLE | A | ref | idx_catid , idx_section | idx_section | 4 | joomla_test . C . id | 23293 | Using where | | 1 | SIMPLE | B | eq_ref | PRIMARY | PRIMARY | 4 | joomla_test . A . catid | 1 | Using where | +----+-------------+-------+--------+-----------------------+-------------+---------+---------------------+-------+-------------+ 3 rows in set ( 0.00 sec )
我們會發現Using filesort沒有了,而這條語句中C表的主鍵對排序起了作用。 而儘管在上面的語句中也是對第一個表的主鍵進行排序,卻沒有得到想要的效果(第一個表的主鍵沒有用到),這是為什麼呢?
實際上以上執行過的所有left join的語句中,第一個表的索引都沒有用到,儘管對第一個表的主鍵進行了排序也無濟於事。不免有些奇怪!
測試:
於是我們繼續測試了下一條SQL:
mysql > explain select A.id , A.title , B.title from base_content A left join base_categories B on A.catid = B.id left join base_sections C on A.sectionid = C.id where A.id < 100 ; +----+-------------+-------+--------+----------------+---------+---------+-------------------------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+----------------+---------+---------+-------------------------+------+-------------+ | 1 | SIMPLE | A | range | PRIMARY | PRIMARY | 4 | NULL | 90 | Using where | | 1 | SIMPLE | B | eq_ref | PRIMARY | PRIMARY | 4 | joomla_test . A . catid | 1 | | | 1 | SIMPLE | C | eq_ref | PRIMARY | PRIMARY | 4 | joomla_test . A . sectionid | 1 | Using index | +----+-------------+-------+--------+----------------+---------+---------+-------------------------+------+-------------+ 3 rows in set ( 0.05 sec )
再次進行排序操作的時候,Using filesoft也沒有再出現
mysql > explain select A.id , A.title, B.title from base_content A left join base_categories B on A.catid = B.id left join base_sections C on A.sectionid = C.id where A.id < 100 order by A.id ; +----+-------------+-------+--------+---------------+---------+---------+-------------------------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+--------+---------------+---------+---------+-------------------------+------+-------------+ | 1 | SIMPLE | A | range | PRIMARY | PRIMARY | 4 | NULL | 105 | Using where | | 1 | SIMPLE | B | eq_ref | PRIMARY | PRIMARY | 4 | joomla_test . A . catid | 1 | | | 1 | SIMPLE | C | eq_ref | PRIMARY | PRIMARY | 4 | joomla_test . A . sectionid | 1 | Using index | +----+-------------+-------+--------+---------------+---------+---------+-------------------------+------+-------------+ 3 rows in set ( 0.00 sec )
結論:
對where條件裡涉及到的欄位,Mysql會使用索引進行搜尋,而這個索引的使用也對排序的效率有很好的提升
測試
分別讓以下兩個SQL語句執行200次:
select A.id , A.title , B.title from base_content A left join base_categories B on A.catid = B.id left join base_sections C on A.sectionid = C.id select A.id, A.title, B.title from base_content A , base_categories B, base_sections C where A.catid = B.id and A.sectionid = C.id select A.id , A.title , B.title from base_content A left join base_categories B on A.catid = B.id left join base_sections C on A.sectionid = C.id order by rand () limit 10 select A.id from base_content A left join base_categories B on B.id = A.catid left join base_sections C on A.sectionid = C.id order by A.id
結果是第(1)條平均用時27s ,第(2)條平均用時54s ,第(3)條平均用時80s ,第(4)條平均用時3s 。
用explain觀察第(3)條語句的執行情況,會發現它建立了temporary臨時表來進行排序。
知識點:
- 對需要查詢和排序的欄位要加索引。
- 儘量少地連線表。left join 比普通連線查詢效率要高,注意觀察索引是否起了作用。
- 排序儘量對第一個表的索引欄位進行,可以避免mysql建立臨時表,這是非常耗資源的。
- 對where條件裡涉及到的欄位,應適當地新增索引,這樣會對排序操作有優化的作用。
- 如果說在分頁時我們能先得到主鍵,再根據主鍵查詢相關內容,也能得到查詢的優化效果。
- 避免使用order by rand()。在執行過程中用show processlist檢視,會發現第(3)條有Copying to tmp table on disk。
- Slow queries 檢查一下是哪些語句降低的Mysql 的執行效率,並進行定期優化。
優化GROUP BY語句
如果查詢包括GROUP BY 但想要避免排序結果的消耗,則可以指定ORDER By NULL禁止排序
例如:
explain select id, sum(moneys) from sales2 group by id \G explain select id, sum(moneys) from sales2 group by id order by null \G
比較發現第一條語句會比第二句在Extra:裡面多了Using filesort.而恰恰filesort是最耗時的。
優化ORDER BY語句
在某些情況中,MySQL可以使用一個索引來滿足ORDER BY子句,而不需要額外的排序。WHERE 條件和 ORDER BY使用相同的索引,並且ORDER BY的順序和索引順序相同,並且ORDER BY的欄位都是升序或者都是降序。
例如:
SELECT * FROM t1 ORDER BY key_part1,key_part2,....: SELECT * FROM t1 WHERE key_part1 = 1 ORDER BY key_part1 DESC,key_part2 DESC; SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 DESC;
但是以下的情況不使用索引:
SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC; --ORDER by的欄位混合ASC 和 DESC SELECT * FROM t1 WHERE key2=constant ORDER BY key1; --用於查詢行的關鍵字與ORDER BY 中所使用的不相同 SELECT * FROM t1 ORDER BY key1, key2; --對不同的關鍵字使用ORDER BY
優化LIMIT分頁
當需要分頁操作時,通常會使用LIMIT加上偏移量的辦法實現,同時加上合適的ORDER BY字句。
如果有對應的索引,通常效率會不錯,否則,MySQL需要做大量的檔案排序操作。
當偏移量非常大的時候,比如:LIMIT 20000 20這樣的查詢,MySQL需要查詢120020條記錄然後只返回20條記錄,前面的20000條都將被拋棄,這樣的代價非常高。
優化這種查詢一個最簡單的辦法就是儘可能的使用覆蓋索引掃描,而不是查詢所有的列。然後根據需要做一次關聯查詢再返回所有的列。
測試:
select film_id, description from base_film order by title limit 50,5;
如果這張表非常大,那麼這個查詢最好改成下面的樣子:
select film.film_id, film.description from base_film INNER JOIN( select film_id from base_film order by title limit 50,5 ) as tmp USING(film_id);
這裡的延遲關聯將大大提升查詢效率,讓MySQL掃描儘可能少的頁面,獲取需要訪問的記錄後在根據關聯列回原表查詢所需要的列。
有時候如果可以使用書籤記錄上次取資料的位置,那麼下次就可以直接從該書籤記錄的位置開始掃描,這樣就可以避免使用OFFSET,比如下面的查詢:
select id from t limit 10000, 10; select id from t where id > 10000 limit 10;
其他優化的辦法還包括使用預先計算的彙總表,或者關聯到一個冗餘表,冗餘表中只包含主鍵列和需要做排序的列。
優化UNION
MySQL處理UNION的策略是先建立臨時表,然後再把各個查詢結果插入到臨時表中,最後再來做查詢。因此很多優化策略在UNION查詢中都沒有辦法很好的時候。經常需要手動將WHERE、LIMIT、ORDER BY等字句“下推”到各個子查詢中,以便優化器可以充分利用這些條件先優化。
除非確實需要伺服器去重,否則就一定要使用UNION ALL,如果沒有ALL關鍵字,MySQL會給臨時表加上DISTINCT選項,這會導致整個臨時表的資料做唯一性檢查,這樣做的代價非常高。當然即使使用ALL關鍵字,MySQL總是將結果放入臨時表,然後再讀出,再返回給客戶端。雖然很多時候沒有這個必要,比如有時候可以直接把每個子查詢的結果返回給客戶端。
特定型別查詢優化
優化COUNT()查詢
COUNT()有兩種不同的作用,其一是統計某個列值的數量,其二是統計行數。
統計列值時,要求列值是非空的,它不會統計NULL。如果確認括號中的表示式不可能為空時,實際上就是在統計行數。
最簡單的就是當使用COUNT(*)時,並不是我們所想象的那樣擴充套件成所有的列,實際上,它會忽略所有的列而直接統計行數。
我們最常見的誤解也就在這兒,在括號內指定了一列卻希望統計結果是行數,而且還常常誤以為前者的效能會更好。
但實際並非這樣,如果要統計行數,直接使用COUNT(*),意義清晰,且效能更好。
有時候某些業務場景並不需要完全精確的COUNT值,可以用近似值來代替,EXPLAIN出來的行數就是一個不錯的近似值,而且執行EXPLAIN並不需要真正地去執行查詢,所以成本非常低。通常來說,執行COUNT()都需要掃描大量的行才能獲取到精確的資料,因此很難優化,MySQL層面還能做得也就只有覆蓋索引了。如果不還能解決問題,只有從架構層面解決了,比如新增彙總表,或者使用redis這樣的外部快取系統。
優化關聯查詢
在大資料場景下,表與表之間通過一個冗餘欄位來關聯,要比直接使用JOIN有更好的效能。如果確實需要使用關聯查詢的情況下,需要特別注意的是:
確保ON和USING字句中的列上有索引。在建立索引的時候就要考慮到關聯的順序。當表A和表B用列c關聯的時候,如果優化器關聯的順序是A、B,那麼就不需要在A表的對應列上建立索引。沒有用到的索引會帶來額外的負擔,一般來說,除非有其他理由,只需要在關聯順序中的第二張表的相應列上建立索引(具體原因下文分析)。
確保任何的GROUP BY和ORDER BY中的表示式只涉及到一個表中的列,這樣MySQL才有可能使用索引來優化。 要理解優化關聯查詢的第一個技巧,就需要理解MySQL是如何執行關聯查詢的。當前MySQL關聯執行的策略非常簡單,它對任何的關聯都執行巢狀迴圈關聯操作,即先在一個表中迴圈取出單條資料,然後在巢狀迴圈到下一個表中尋找匹配的行,依次下去,直到找到所有表中匹配的行為為止。然後根據各個表匹配的行,返回查詢中需要的各個列。
太抽象了?以上面的示例來說明,比如有這樣的一個查詢:
select A.xName, b.yName from A INNER JOIN B USING(c) WHERE A.xName IN (5,6)
假設MySQL按照查詢中的關聯順序A、B來進行關聯操作,那麼可以用下面的偽程式碼表示MySQL如何完成這個查詢:
outer_iterator = select A.xName, A.c from A where A.xName IN(5,6); outer_row = outer_iterator.next; while(outer_row){ inner_iterator = SELECT B.yName from B WHERE B.c = outer_row.c; inner_row = inner_iterator.next; while(outer_row) { outpur[inner_row.yName, outer_row.xx]; inner_row = inner_iterator.next; } outer_row = outer_iterator.next; }
可以看到,最外層的查詢是根據A.xName,A.c上如果有索引的話,整個關聯查詢也不會使用。再看內層的查詢,很明顯B.c上如果有索引的話,能夠加速查詢,因此只需要在關聯順序中的第二張表的相應列上建立索引即可。
1、MySQL不會使用索引的情況:非獨立的列 “獨立的列”是指索引列不能是表示式的一部分,也不能是函式的引數。比如:
select * from t where id + 1 = 15
其等價於 id = 14,但是MySQL無法自動解析這個表示式,使用函式是同樣的道理。
2、字首索引
如果列很長,通常可以索引開始的部分字元,這樣可以有效節約索引空間,從而提高索引效率。
3、多列索引和索引順序
在多數情況下,在多個列上建立獨立的索引並不能提高查詢效能。
因為MySQL不知道選擇哪個索引的查詢效率更好,所以在老版本,比如MySQL5.0之前就會隨便選擇一個列的索引,而新的版本會採用合併索引的策略。
示例:
在一張電影演員表中,在actor_id和film_id兩個列上都建立了獨立的索引,然後有如下查詢:
select film_id, actor_id from film_actor where actor_id = 1 or film_id = 1
老版本的MySQL會隨機選擇一個索引,但新版本做如下的優化:
select film_id, actor_id from film_actor where actor_id = 1 union all select film_id, actor_id from film_actor where film_id and actor_id <> 1
當出現多個索引做相交操作時(多個AND條件),通常來說一個包含所有相關列的索引要優於多個獨立索引。 當出現多個索引做聯合操作時(多個OR條件),對結果集的合併、排序等操作需要耗費大量的CPU和記憶體資源,特別是當其中的某些索引的選擇性不高,需要返回合併大量資料時,查詢成本更高。所以這種情況下還不如走全表掃描。
結論:
如果發現有索引合併(Extra欄位出現Using union),檢查查詢和表結構,檢查索引(或許一個包含所有相關列的多列索引更適合)。
多列索引時索引的順序對於查詢是至關重要的,應該把選擇性更高的欄位放到索引的前面,這樣通過第一個欄位就可以過濾掉大多數不符合條件的資料。
索引選擇性
索引選擇性是指不重複的索引值和資料表的總記錄數的比值,選擇性越高查詢效率越高,因為選擇性越高的索引可以讓MySQL在查詢時過濾掉更多的行。 唯一索引的選擇性是1,這是最好的索引選擇性,效能也是最好的。
示例:
select * from base_payment where staff_id = 2 and customer_id = 785
是應該建立(staff_id,customer_id)的索引還是應該顛倒一下順序?
selct count(distinct staff_id)/count(*) as staff_id_selectivity, count(distinct customer_id)/count(*) as customer_id_selectivity, count(*) from base_payment
哪個欄位的選擇性更接近1就把哪個欄位索引前面,多數情況下使用這個原則沒有任何問題,但仍然注意你的資料中是否存在一些特殊情況。
示例:
比如要查詢某個使用者組下有過交易的使用者資訊:
select user_id from base_trade where user_group_id = 1 and trade_amount > 0
MySQL為這個查詢選擇了索引(user_group_id,trade_amount),看起來沒有任何問題。
但實際情況是這張表的大多數資料都是從老系統中遷移過來的,由於新老系統的資料不相容,所以就給老系統遷移過來的資料賦予了一個預設的使用者組。 這種情況下,通過索引掃描的行數跟全表掃描基本沒什麼區別,索引也就起不到任何作用。
經驗法則可以指導我們開發和設計,但實際業務場景下的某些特殊情況可能會摧毀你的整個設計。
4、避免多個範圍條件
實際開發中,我們會經常使用多個範圍條件,比如想查詢某個時間段內登入過的使用者:
select USER.* from base_user USER where login_time > '2019-01-01' and age between 18 and 30
這個查詢有一個問題:它有兩個範圍條件,login_time列和age列,MySQL可以使用login_time列的索引或者age列的索引,但無法同時使用它們。
5、覆蓋索引
如果一個索引包含或者說覆蓋所有需要查詢的欄位的值,那麼就沒有必要再回表查詢,這就稱為覆蓋索引。
覆蓋索引可以極大的提高效能,因為查詢只需要掃描索引會帶來許多好處:
- 索引條目遠小於資料行大小,如果只讀取索引,極大減少資料訪問量
- 索引是有按照列值順序儲存的,對於I/O密集型的範圍查詢要比隨機從磁碟讀取每一行資料的IO要少的多
6、使用索引掃描來排序
MySQL有兩種方式可以生產有序的結果集
- 其一是對結果集進行排序的操作,
- 其二是按照索引順序掃描得出的結果自然是有序的。
如果type列的值為index表示使用了索引掃描來做排序。掃描索引本身很快,因為只需要從一條索引記錄移動到相鄰的下一條記錄。
但如果索引本身不能覆蓋所有需要查詢的列,那麼就不得不每掃描一條索引記錄就回表查詢一次對應的行。 這個讀取操作基本上是隨機I/O,因此按照索引順序讀取資料的速度通常要比順序地全表掃描要慢。
知識點:
- 索引最好既能夠滿足排序,又滿足查詢。只有當索引的列順序和ORDER BY子句的順序完全一致,並且所有列的排序方向也一樣時,才能夠使用索引來對結果做排序。
- 如果查詢需要關聯多張表,則只有ORDER BY子句引用的欄位全部為第一張表時,才能使用索引做排序。
- ORDER BY子句和查詢的限制是一樣的,都要滿足最左字首的要求,其他情況下都需要執行排序操作,而無法利用索引排序。
示例
有一種情況例外,就是最左的列被指定為常數
// 最左列為常數,索引:(date,staff_id,customer_id) select staff_id,customer_id from base_staff where date = '2015-06-01' order by staff_id,customer_id
7、冗餘和重複索引
立即刪除。冗餘索引是指在相同的列上按照相同的順序建立的相同型別的索引。比如有一個索引(A,B),再建立索引(A)就是冗餘索引。
大多數情況下都應該儘量擴充套件已有的索引而不是建立新索引,冗餘索引經常發生在為表新增新索引時,比如有人新建了索引(A,B),但這個索引不是擴充套件已有的索引(A)。
但有極少情況下出現效能方面的考慮需要冗餘索引,比如擴充套件已有索引而導致其變得過大,從而影響到其他使用該索引的查詢。
8、定期檢查,可刪除長期未使用的索引
只有當索引幫助提高查詢速度帶來的好處大於其帶來的額外工作時,索引才是有效的。所以 explain 後再提測是一種美德。
talk is easy, show me the code.
【一隻阿木