1. 程式人生 > >MySQL選錯索引導致的線上慢查詢事故

MySQL選錯索引導致的線上慢查詢事故

![](https://imgkr.cn-bj.ufileos.com/9c88fd76-48b2-45c3-aa35-c66c6817c273.png) ## 前言 又和大家見面了!又兩週過去了,我的雲筆記裡又多了幾篇寫了一半的文章草稿。有的是因為質量沒有達到預期還準備再加點內容,有的則完全是一個靈感而已,內容完全木有。羨慕很多大佬們,一週能產出五六篇文章,給我兩個肝我都不夠。好了,不多說廢話了... 最近在線上環境遇到了一次SQL慢查詢引發的資料庫故障,影響線上業務。經過排查後,確定原因是**SQL在執行時,MySQL優化器選擇了錯誤的索引(不應該說是“錯誤”,而是選擇了實際執行耗時更長的索引)**。在排查過程中,查閱了許多資料,也學習了下MySQL優化器選擇索引的基本準則,在本文中進行解決問題思路的分享。本人MySQL瞭解深度有限,如果錯誤歡迎理性討論和指正。 **在這次事故中也能充分看出深入瞭解MySQL執行原理的重要性,這是遇到問題時能否獨立解決問題的關鍵。** 試想一個月黑風高的夜晚,公司線上突然掛了,而你的同事們都不線上,就你一個人有條件解決問題,這時候如果被工程師的基本功把你卡住了,就問你尷不尷尬... **本文的主要內容:** - 故障描述 - 問題原因排查 - MySQL索引選擇原理 - 解決方案 - 思考與總結 > 請大家多多支援我的原創技術公眾號:後端技術漫談 ## 正文 ### 故障描述 在7月24日11點線上某資料庫突然收到大量告警,慢查詢數超標,並且引發了連線數暴增,導致資料庫響應緩慢,影響業務。看圖表慢查詢在高峰達到了每分鐘14w次,在平時正常情況下慢查詢數僅在兩位數以下,如下圖: ![](https://imgkr.cn-bj.ufileos.com/b0944764-3775-465f-bd9e-c355e7483d72.png) 趕緊檢視慢SQL記錄,發現都是同一類語句導致的慢查詢(隱私資料例如表名,我已經隱去): ``` select * from sample_table where 1 = 1 and (city_id = 565) and (type = 13) order by id desc limit 0, 1 ``` 看起來語句很簡單,沒什麼特別的。但是每個執行的查詢時間達到了驚人的44s。 ![](https://imgkr.cn-bj.ufileos.com/ec1a9abd-27f7-4929-8754-fb3fded355aa.png) 簡直聳人聽聞,這已經不是“慢”能形容的了... 接下來查看錶資料資訊,如下圖: ![](https://imgkr.cn-bj.ufileos.com/c7a9899f-b2b0-421a-98a1-3c9a79c23b57.png) 可以看到表資料量較大,預估行數在83683240,也就是8000w左右,**千萬資料量的表**。 大致情況就是這樣,下面進入排查問題的環節。 ### 問題原因排查 首先當然要懷疑會不會該語句沒走索引,檢視建表DML中的索引: ``` KEY `idx_1` (`city_id`,`type`,`rank`), KEY `idx_log_dt_city_id_rank` (`log_dt`,`city_id`,`rank`), KEY `idx_city_id_type` (`city_id`,`type`) ``` 請忽略idx_1和idx_city_id_type兩個索引的重複,這都是歷史遺留問題了。 **可以看到是有idx_city_id_type和idx_1索引的**,我們的查詢條件是city_id和type,這兩個索引都是能走到的。 但是,我們的查詢條件真的只要考慮city_id和type嗎?(機智的小夥伴應該注意到問題所在了,先往下講,留給大家思考) 既然有索引,接下來就該看該語句實際有沒有走到索引了,MySQL提供了Explain可以分析SQL語句。Explain 用來分析 SELECT 查詢語句。 Explain比較重要的欄位有: - select_type : 查詢型別,有簡單查詢、聯合查詢、子查詢等 - key : 使用的索引 - rows : 預計需要掃描的行數 更多詳細Explain介紹可以參考:[MySQL 效能優化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735) 我們使用Explain分析該語句: ``` select * from sample_table where city_id = 565 and type = 13 order by id desc limit 0,1 ``` 得到結果: ![](https://imgkr.cn-bj.ufileos.com/512f1be1-0d18-43ec-9ea0-5eed69e289b9.png) 可以看出,雖然possiblekey有我們的索引,但是最後走了主鍵索引。而表是千萬級別,**並且該查詢條件最後實際是返回的空資料**,也就是MySQL在主鍵索引上實際檢索時間很長,導致了慢查詢。 我們可以使用force index(idx_city_id_type)讓該語句選擇我們設定的聯合索引: ``` select * from sample_table force index(idx_city_id_type) where ( ( (1 = 1) and (city_id = 565) ) and (type = 13) ) order by id desc limit 0, 1 ``` 這次明顯執行的飛快,分析語句: ![](https://imgkr.cn-bj.ufileos.com/d0a655d9-d637-4f6e-9b02-f5e7494501c4.png) 實際執行時間0.00175714s,走了聯合索引後,不再是慢查詢了。 問題找到了,總結下來就是:**MySQL優化器認為在limit 1的情況下,走主鍵索引能夠更快的找到那一條資料,並且如果走聯合索引需要掃描索引後進行排序,而主鍵索引天生有序,所以優化器綜合考慮,走了主鍵索引。實際上,MySQL遍歷了8000w條資料也沒找到那個天選之人(符合條件的資料),所以浪費了很多時間。** ### MySQL索引選擇原理 #### 優化器索引選擇的準則 MySQL一條語句的執行流程大致如下圖,而**查詢優化器**則是選擇索引的地方: ![](https://imgkr.cn-bj.ufileos.com/c7ab6d9b-751b-4725-affc-fc0d6506ccd2.png) 引用參考文獻一段解釋: > 首先要知道,選擇索引是MySQL優化器的工作。 > > 而優化器選擇索引的目的,是找到一個最優的執行方案,並用最小的代價去執行語句。在資料庫裡面,掃描行數是影響執行代價的因素之一。掃描的行數越少,意味著訪問磁碟資料的次數越少,消耗的CPU資源越少。 > > **當然,掃描行數並不是唯一的判斷標準,優化器還會結合是否使用臨時表、是否排序等因素進行綜合判斷。** 總結下來,優化器選擇有許多考慮的因素:**掃描行數、是否使用臨時表、是否排序等等** 我們回頭看剛才的兩個explain截圖: ![](https://imgkr.cn-bj.ufileos.com/6bae73e2-feee-4393-a5f3-b1d8298f3da5.png) ![](https://imgkr.cn-bj.ufileos.com/47b9949f-b0df-4b4e-b1fc-720814c17601.png) 走了**主鍵索引**的查詢語句,rows預估行數1833,而強制走**聯合索引**行數是45640,並且Extra資訊中,顯示需要Using filesort進行額外的排序。所以在不加強制索引的情況下,**優化器選擇了主鍵索引,因為它覺得主鍵索引掃描行數少,而且不需要額外的排序操作,主鍵索引天生有序。** #### rows是怎麼預估出來的 同學們就要問了,為什麼rows只有1833,明明實際掃描了整個主鍵索引啊,行數遠遠不止幾千行。實際上explain的rows是MySQL**預估**的行數,**是根據查詢條件、索引和limit綜合考慮出來的預估行數。** ``` MySQL是怎樣得到索引的基數的呢?這裡,我給你簡單介紹一下MySQL取樣統計的方法。 為什麼要取樣統計呢?因為把整張表取出來一行行統計,雖然可以得到精確的結果,但是代價太高了,所以只能選擇“取樣統計”。 取樣統計的時候,InnoDB預設會選擇N個數據頁,統計這些頁面上的不同值,得到一個平均值,然後乘以這個索引的頁面數,就得到了這個索引的基數。 而資料表是會持續更新的,索引統計資訊也不會固定不變。所以,當變更的資料行數超過1/M的時候,會自動觸發重新做一次索引統計。 在MySQL中,有兩種儲存索引統計的方式,可以通過設定引數innodb_stats_persistent的值來選擇: 設定為on的時候,表示統計資訊會持久化儲存。這時,預設的N是20,M是10。 設定為off的時候,表示統計資訊只儲存在記憶體中。這時,預設的N是8,M是16。 由於是取樣統計,所以不管N是20還是8,這個基數都是很容易不準的。 ``` 我們可以使用`analyze table t `命令,可以用來重新統計索引資訊。但是這條命令生產環境需要聯絡DBA,所以我就不做實驗了,大家可以自行實驗。 #### 索引要考慮 order by 的欄位 為什麼這麼說?因為如果我這個表中的索引是`city_id`,`type`和`id`的聯合索引,那優化器就會走這個聯合索引,因為索引已經做好了排序。 #### 更改limit大小能解決問題? 把limit數量調大會影響預估行數rows,進而影響優化器索引的選擇嗎? 答案是會。 我們執行limit 10 ``` select * from sample_table where city_id = 565 and type = 13 order by id desc limit 0,10 ``` ![](https://imgkr.cn-bj.ufileos.com/47c989bd-aec4-4f63-a8c5-a661c396a03e.png) 圖中rows變為了18211,增長了10倍。如果使用limit 100,會發生什麼? ![](https://imgkr.cn-bj.ufileos.com/c98673d7-98eb-4014-b23b-e7aaa9c0a699.png) 優化器選擇了聯合索引。初步估計是rows還會翻倍,所以優化器放棄了主鍵索引。寧願用聯合索引後排序,也不願意用主鍵索引了。 #### 為何突然出現異常慢查詢 問:這個查詢語句已經在線上穩定運行了非常長的時間,為何這次突然出現了慢查詢? 答:以前的語句查詢條件返回結果都不為空,limit1很快就能找到那條資料,返回結果。而這次程式碼中查詢條件實際結果為空,導致了掃描了全部的主鍵索引。 ### 解決方案 知道了MySQL為何選擇這個索引的原因後,我們就可以根據上面的思路來列舉出解決辦法了。 主要有兩個大方向: 1. 強制指定索引 2. 干涉優化器選擇 #### 強制選擇索引:force index 就像上面我最開始的操作那樣,我們直接使用force index,讓語句走我們想要走的索引。 ``` select * from sample_table force index(idx_city_id_type) where ( ( (1 = 1) and (city_id = 565) ) and (type = 13) ) order by id desc limit 0, 1 ``` 這樣做的優點是見效快,問題馬上就能解決。 缺點也很明顯: - 高耦合,這種語句寫在程式碼裡,會變得難以維護,如果索引名變化了,或者沒有這個索引了,程式碼就要反覆修改。屬於硬編碼。 - 很多程式碼用框架封裝了SQL,`force index()`並不容易加進去。 **我們換一種辦法,我們去引導優化器選擇聯合索引。** #### 干涉優化器選擇:增大limit 通過增大limit,我們可以讓預估掃描行數快速增加,比如改成下面的limit 0, 1000 ``` SELECT * FROM sample_table where city_id = 565 and type = 13 order by id desc LIMIT 0,1000 ``` 這樣就會走上聯合索引,然後排序,但是這樣強行增長limit,其實總有種面向黑盒調參的感覺。我們還有更優美的解決方案嗎? #### 干涉優化器選擇:增加包含order by id欄位的聯合索引 我們這句慢查詢使用的是order by id,但是我們卻沒有在聯合索引中加入id欄位,導致了優化器認為聯合索引後還要排序,乾脆就不太想走這個聯合索引了。 我們可以新建`city_id`,`type`和`id`的聯合索引,來解決這個問題。 這樣也有一定的弊端,比如我這個表到了8000w資料,建立索引非常耗時,而且通常索引就有3.4個g,如果無限制的用索引解決問題,可能會帶來新的問題。表中的索引不宜過多。 #### 干涉優化器選擇:寫成子查詢 還有什麼辦法?我們可以用子查詢,在子查詢裡先走city_id和type的聯合索引,得到結果集後在limit1選出第一條。 但是子查詢使用有風險,一版DBA也不建議使用子查詢,會建議大家在程式碼邏輯中完成複雜的查詢。當然我們這句並不複雜啦~ ``` Select * From sample_table Where id in (Select id From `newhome_db`.`af_hot_price_region` where (city_id = 565 and type = 13)) limit 0, 1 ``` #### 還有很多解決辦法... SQL優化是個很大的工程,我們還有非常多的辦法能夠解決這句慢查詢問題,這裡就不一一展開了。留給大家做為思考題了。 ### 總結 本文帶大家回顧了一次MySQL優化器選錯索引導致的線上慢查詢事故,可以看出MySQL優化器對於索引的選擇並不單單依靠某一個標準,而是一個綜合選擇的結果。我自己也對這方面瞭解不深入,還需要多多學習,爭取能夠好好的做一個索引選擇的總結(挖坑)。不說了,拿起巨厚的《高效能MySQL》,開始... 壓住我的泡麵... **最後做個文章總結:** - 該慢查詢語句中使用order by id導致優化器在主鍵索引和city_id和type的聯合索引中有所取捨,最終導致選擇了更慢的索引。 - 可以通過強制指定索引,建立包含id的聯合索引,增大limit等方式解決問題。 - 平時開發時,尤其是對於特大資料量的表,要注意SQL語句的規範和索引的建立,避免事故的發生。 ## 參考 《高效能MySQL》 MySQL優化器 limit影響的case: https://www.cnblogs.com/xpchild/p/3878417.html mysql中走與不走索引的情況彙集(待全量實驗): https://www.cnblogs.com/gxyandwmm/p/13363100.html **MySQL ORDER BY主鍵id加LIMIT限制走錯索引:** https://www.jianshu.com/p/caf5818eca81 【業務學習】關於MySQL order by limit 走錯索引的探討: https://segmentfault.com/a/1190000020399424 MySQL為什麼有時候會選錯索引?: https://www.cnblogs.com/a-phper/p/10313888.html # 關注我 我是一名後端開發工程師。主要關注後端開發,資料安全,爬蟲,物聯網,邊緣計算等方向,歡迎交流。 ### 各大平臺都可以找到我 - **微信公眾號:後端技術漫談** - **Github:[@qqxx6661](https://github.com/qqxx6661)** - CSDN:[@蠻三刀把刀](http://blog.csdn.net/qqxx6661) - 知乎:[@後端技術漫談](https://www.zhihu.com/people/yang-zhen-dong-1/) - 簡書:[@蠻三刀把刀](https://www.jianshu.com/u/b5f225ca2376) - 掘金:[@蠻三刀把刀](https://juejin.im/user/5b48015ce51d45191462ba55) - 騰訊雲+社群:[@後端技術漫談](https://cloud.tencent.com/developer/user/1706868) ### 原創文章主要內容 - 後端開發 - Java面試 - 設計模式/資料結構/演算法題解 - 爬蟲/邊緣計算/物聯網 - 讀書筆記/逸聞趣事/程式人生 ### 個人公眾號:後端技術漫談 ![個人公眾號:後端技術漫談](http://ww1.sinaimg.cn/large/8de6a3d3gy1genhmip8aqg20go0gojz5.gif) **如果文章對你有幫助,不妨收藏,轉發,在看起來~**