前言
又和你們見面了!又兩週過去了,個人雲筆記裏又多了幾篇寫了一半的文章草稿。有的是由於質量沒有達到預期還準備再加點內容,有的則徹底是一個靈感而已,內容徹底木有。羨慕不少大佬們,一週能產出五六篇文章,給我兩個肝我都不夠。好了,很少說廢話了...php
最近在線上環境遇到了一次SQL慢查詢引起的數據庫故障,影響線上業務。通過排查後,肯定緣由是「SQL在執行時,MySQL優化器選擇了錯誤的索引(不該該說是「錯誤」,而是選擇了實際執行耗時更長的索引)」。在排查過程當中,查閱了許多資料,也學習了下MySQL優化器選擇索引的基本準則,在本文中進行解決問題思路的分享。本人MySQL瞭解深度有限,若是錯誤歡迎理性討論和指正。html
「在此次事故中也能充分看出深刻了解MySQL運行原理的重要性,這是遇到問題時可否獨立解決問題的關鍵。」 試想一個月黑風高的夜晚,公司線上忽然掛了,而你的同事們都不在線,就你一我的有條件解決問題,這時候若是被工程師的基本功把你卡住了,就問你尷不尷尬...mysql
「本文的主要內容:」
- 故障描述
- 問題緣由排查
- MySQL索引選擇原理
- 解決方案
- 思考與總結
❝ 請你們多多支持個人原創技術公衆號:後端技術漫談 ❞
正文
故障描述
在7月24日11點線上某數據庫忽然收到大量告警,慢查詢數超標,而且引起了鏈接數暴增,致使數據庫響應緩慢,影響業務。看圖表慢查詢在高峯達到了每分鐘14w次,在平時正常狀況下慢查詢數僅在兩位數如下,以下圖:
面試
趕忙查看慢SQL記錄,發現都是同一類語句致使的慢查詢(隱私數據例如表名,我已經隱去):算法
select * from sample_table where 1 = 1 and (city_id = 565) and (type = 13) order by id desc limit 0, 1
看起來語句很簡單,沒什麼特別的。可是每一個執行的查詢時間達到了驚人的44s。
sql
簡直聳人聽聞,這已經不是「慢」能形容的了...數據庫
接下來查看錶數據信息,以下圖:
segmentfault
能夠看到表數據量較大,預估行數在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 使用分析
咱們使用Explain分析該語句:
select * from sample_table where city_id = 565 and type = 13 order by id desc limit 0,1
獲得結果:
能夠看出,雖然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
此次明顯執行的飛快,分析語句:
實際執行時間0.00175714s,走了聯合索引後,再也不是慢查詢了。
問題找到了,總結下來就是:「MySQL優化器認爲在limit 1的狀況下,走主鍵索引可以更快的找到那一條數據,而且若是走聯合索引須要掃描索引後進行排序,而主鍵索引天生有序,因此優化器綜合考慮,走了主鍵索引。實際上,MySQL遍歷了8000w條數據也沒找到那個天選之人(符合條件的數據),因此浪費了不少時間。」
MySQL索引選擇原理
優化器索引選擇的準則
MySQL一條語句的執行流程大體以下圖,而「查詢優化器」則是選擇索引的地方:
引用參考文獻一段解釋:
❝ 首先要知道,選擇索引是MySQL優化器的工做。 而優化器選擇索引的目的,是找到一個最優的執行方案,並用最小的代價去執行語句。在數據庫裏面,掃描行數是影響執行代價的因素之一。掃描的行數越少,意味着訪問磁盤數據的次數越少,消耗的CPU資源越少。 「固然,掃描行數並非惟一的判斷標準,優化器還會結合是否使用臨時表、是否排序等因素進行綜合判斷。」
❞
總結下來,優化器選擇有許多考慮的因素:「掃描行數、是否使用臨時表、是否排序等等」
咱們回頭看剛纔的兩個explain截圖:
走了「主鍵索引」的查詢語句,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
圖中rows變爲了18211,增加了10倍。若是使用limit 100,會發生什麼?
優化器選擇了聯合索引。初步估計是rows還會翻倍,因此優化器放棄了主鍵索引。寧願用聯合索引後排序,也不肯意用主鍵索引了。
爲什麼忽然出現異常慢查詢
問:這個查詢語句已經在線上穩定運行了很是長的時間,爲什麼此次忽然出現了慢查詢?
答:之前的語句查詢條件返回結果都不爲空,limit1很快就能找到那條數據,返回結果。而此次代碼中查詢條件實際結果爲空,致使了掃描了所有的主鍵索引。
解決方案
知道了MySQL爲什麼選擇這個索引的緣由後,咱們就能夠根據上面的思路來列舉出解決辦法了。
主要有兩個大方向:
- 強制指定索引
- 干涉優化器選擇
強制選擇索引: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語句的規範和索引的創建,避免事故的發生。
往期推薦
交流羣 | 個人「惟一指定」技術交流羣創建了
SQL調優 | SQL 書寫規範及優化技巧(下)
開源實戰 | Canal生產環境常見問題總結與分析
系統設計 | 經過Binlog來實現系統間數據同步
MySQL | 敖丙的數據庫調優最佳實踐
參考
《高性能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 走錯索引的探討:
http://www.javashuo.com/article/p-wlkduonm-cm.html
MySQL爲何有時候會選錯索引?:
http://www.javashuo.com/article/p-shonegzz-hq.html
關注我
我是一名後端開發工程師。主要關注後端開發,數據安全,爬蟲,物聯網,邊緣計算等方向,歡迎交流。
各大平臺均可以找到我
- 「微信公衆號:後端技術漫談」
- 「Github:@qqxx6661」
- CSDN:@蠻三刀把刀
- 知乎:@後端技術漫談
- 簡書:@蠻三刀把刀
- 掘金:@蠻三刀把刀
- 騰訊雲+社區:@後端技術漫談
原創文章主要內容
- 後端開發
- Java面試
- 設計模式/數據結構/算法題解
- 爬蟲/邊緣計算/物聯網
- 讀書筆記/逸聞趣事/程序人生
我的公衆號:後端技術漫談
我的公衆號:後端技術漫談「若是文章對你有幫助,不妨收藏,轉發,在看起來~」