千萬級數據表選錯索引致使的線上慢查詢事故

千萬級數據表選錯索引致使的線上慢查詢事故

前言

又和你們見面了!又兩週過去了,個人雲筆記裏又多了幾篇寫了一半的文章草稿。有的是由於質量沒有達到預期還準備再加點內容,有的則徹底是一個靈感而已,內容徹底木有。羨慕不少大佬們,一週能產出五六篇文章,給我兩個肝我都不夠。好了,很少說廢話了...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爲什麼選擇這個索引的緣由後,咱們就能夠根據上面的思路來列舉出解決辦法了。

主要有兩個大方向:

  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語句的規範和索引的創建,避免事故的發生。

往期推薦

交流羣 | 個人「惟一指定」技術交流羣創建了
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面試
  • 設計模式/數據結構/算法題解
  • 爬蟲/邊緣計算/物聯網
  • 讀書筆記/逸聞趣事/程序人生

    我的公衆號:後端技術漫談

    千萬級數據表選錯索引致使的線上慢查詢事故我的公衆號:後端技術漫談「若是文章對你有幫助,不妨收藏,轉發,在看起來~」

相關文章
相關標籤/搜索