<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          千萬級數(shù)據(jù)表選錯索引導(dǎo)致的線上慢查詢事故

          共 5475字,需瀏覽 11分鐘

           ·

          2020-08-21 00:07

          前言

          最近在線上環(huán)境遇到了一次SQL慢查詢引發(fā)的數(shù)據(jù)庫故障,影響線上業(yè)務(wù)。經(jīng)過排查后,確定原因是「SQL在執(zhí)行時,MySQL優(yōu)化器選擇了錯誤的索引(不應(yīng)該說是“錯誤”,而是選擇了實際執(zhí)行耗時更長的索引)」。在排查過程中,查閱了許多資料,也學(xué)習(xí)了下MySQL優(yōu)化器選擇索引的基本準(zhǔn)則,在本文中進(jìn)行解決問題思路的分享。本人MySQL了解深度有限,如果錯誤歡迎理性討論和指正。

          「在這次事故中也能充分看出深入了解MySQL運(yùn)行原理的重要性,這是遇到問題時能否獨(dú)立解決問題的關(guān)鍵?!?/strong> 試想一個月黑風(fēng)高的夜晚,公司線上突然掛了,而你的同事們都不在線,就你一個人有條件解決問題,這時候如果被工程師的基本功把你卡住了,就問你尷不尷尬...

          「本文的主要內(nèi)容:」

          • 故障描述
          • 問題原因排查
          • MySQL索引選擇原理
          • 解決方案
          • 思考與總結(jié)

          ?

          正文

          故障描述

          在7月24日11點線上某數(shù)據(jù)庫突然收到大量告警,慢查詢數(shù)超標(biāo),并且引發(fā)了連接數(shù)暴增,導(dǎo)致數(shù)據(jù)庫響應(yīng)緩慢,影響業(yè)務(wù)??磮D表慢查詢在高峰達(dá)到了每分鐘14w次,在平時正常情況下慢查詢數(shù)僅在兩位數(shù)以下,如下圖:

          趕緊查看慢SQL記錄,發(fā)現(xiàn)都是同一類語句導(dǎo)致的慢查詢(隱私數(shù)據(jù)例如表名,我已經(jīng)隱去):

          select
          ??*
          from
          ??sample_table
          where
          ????1?=?1
          ????and?(city_id?=?565)
          ????and?(type?=?13)
          order?by
          ??id?desc
          limit
          ??0,?1

          看起來語句很簡單,沒什么特別的。但是每個執(zhí)行的查詢時間達(dá)到了驚人的44s。

          簡直聳人聽聞,這已經(jīng)不是“慢”能形容的了...

          接下來查看表數(shù)據(jù)信息,如下圖:

          可以看到表數(shù)據(jù)量較大,預(yù)估行數(shù)在83683240,也就是8000w左右,「千萬數(shù)據(jù)量的表」

          大致情況就是這樣,下面進(jìn)入排查問題的環(huán)節(jié)。

          問題原因排查

          首先當(dāng)然要懷疑會不會該語句沒走索引,查看建表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兩個索引的重復(fù),這都是歷史遺留問題了。

          「可以看到是有idx_city_id_type和idx_1索引的」,我們的查詢條件是city_id和type,這兩個索引都是能走到的。

          但是,我們的查詢條件真的只要考慮city_id和type嗎?(機(jī)智的小伙伴應(yīng)該注意到問題所在了,先往下講,留給大家思考)

          既然有索引,接下來就該看該語句實際有沒有走到索引了,MySQL提供了Explain可以分析SQL語句。Explain 用來分析 SELECT 查詢語句。

          Explain比較重要的字段有:

          • select_type : 查詢類型,有簡單查詢、聯(lián)合查詢、子查詢等
          • key : 使用的索引
          • rows : 預(yù)計需要掃描的行數(shù)

          更多詳細(xì)Explain介紹可以參考:MySQL 性能優(yōu)化神器 Explain 使用分析

          我們使用Explain分析該語句:

          select?*?from?sample_table?where?city_id?=?565?and?type?=?13?order?by?id?desc?limit?0,1

          得到結(jié)果:

          可以看出,雖然possiblekey有我們的索引,但是最后走了主鍵索引。而表是千萬級別,「并且該查詢條件最后實際是返回的空數(shù)據(jù)」,也就是MySQL在主鍵索引上實際檢索時間很長,導(dǎo)致了慢查詢。

          我們可以使用force index(idx_city_id_type)讓該語句選擇我們設(shè)置的聯(lián)合索引:

          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

          這次明顯執(zhí)行的飛快,分析語句:

          實際執(zhí)行時間0.00175714s,走了聯(lián)合索引后,不再是慢查詢了。

          問題找到了,總結(jié)下來就是:「MySQL優(yōu)化器認(rèn)為在limit 1的情況下,走主鍵索引能夠更快的找到那一條數(shù)據(jù),并且如果走聯(lián)合索引需要掃描索引后進(jìn)行排序,而主鍵索引天生有序,所以優(yōu)化器綜合考慮,走了主鍵索引。實際上,MySQL遍歷了8000w條數(shù)據(jù)也沒找到那個天選之人(符合條件的數(shù)據(jù)),所以浪費(fèi)了很多時間。」

          MySQL索引選擇原理

          優(yōu)化器索引選擇的準(zhǔn)則

          MySQL一條語句的執(zhí)行流程大致如下圖,而「查詢優(yōu)化器」則是選擇索引的地方:

          引用參考文獻(xiàn)一段解釋:

          ?

          首先要知道,選擇索引是MySQL優(yōu)化器的工作。

          而優(yōu)化器選擇索引的目的,是找到一個最優(yōu)的執(zhí)行方案,并用最小的代價去執(zhí)行語句。在數(shù)據(jù)庫里面,掃描行數(shù)是影響執(zhí)行代價的因素之一。掃描的行數(shù)越少,意味著訪問磁盤數(shù)據(jù)的次數(shù)越少,消耗的CPU資源越少。

          「當(dāng)然,掃描行數(shù)并不是唯一的判斷標(biāo)準(zhǔn),優(yōu)化器還會結(jié)合是否使用臨時表、是否排序等因素進(jìn)行綜合判斷?!?/strong>

          ?

          總結(jié)下來,優(yōu)化器選擇有許多考慮的因素:「掃描行數(shù)、是否使用臨時表、是否排序等等」

          我們回頭看剛才的兩個explain截圖:

          走了「主鍵索引」的查詢語句,rows預(yù)估行數(shù)1833,而強(qiáng)制走「聯(lián)合索引」行數(shù)是45640,并且Extra信息中,顯示需要Using filesort進(jìn)行額外的排序。所以在不加強(qiáng)制索引的情況下,「優(yōu)化器選擇了主鍵索引,因為它覺得主鍵索引掃描行數(shù)少,而且不需要額外的排序操作,主鍵索引天生有序?!?/strong>

          rows是怎么預(yù)估出來的

          同學(xué)們就要問了,為什么rows只有1833,明明實際掃描了整個主鍵索引啊,行數(shù)遠(yuǎn)遠(yuǎn)不止幾千行。實際上explain的rows是MySQL「預(yù)估」的行數(shù),「是根據(jù)查詢條件、索引和limit綜合考慮出來的預(yù)估行數(shù)。」

          MySQL是怎樣得到索引的基數(shù)的呢?這里,我給你簡單介紹一下MySQL采樣統(tǒng)計的方法。

          為什么要采樣統(tǒng)計呢?因為把整張表取出來一行行統(tǒng)計,雖然可以得到精確的結(jié)果,但是代價太高了,所以只能選擇“采樣統(tǒng)計”。

          采樣統(tǒng)計的時候,InnoDB默認(rèn)會選擇N個數(shù)據(jù)頁,統(tǒng)計這些頁面上的不同值,得到一個平均值,然后乘以這個索引的頁面數(shù),就得到了這個索引的基數(shù)。

          而數(shù)據(jù)表是會持續(xù)更新的,索引統(tǒng)計信息也不會固定不變。所以,當(dāng)變更的數(shù)據(jù)行數(shù)超過1/M的時候,會自動觸發(fā)重新做一次索引統(tǒng)計。

          在MySQL中,有兩種存儲索引統(tǒng)計的方式,可以通過設(shè)置參數(shù)innodb_stats_persistent的值來選擇:

          設(shè)置為on的時候,表示統(tǒng)計信息會持久化存儲。這時,默認(rèn)的N是20,M是10。
          設(shè)置為off的時候,表示統(tǒng)計信息只存儲在內(nèi)存中。這時,默認(rèn)的N是8,M是16。
          由于是采樣統(tǒng)計,所以不管N是20還是8,這個基數(shù)都是很容易不準(zhǔn)的。

          我們可以使用analyze table t命令,可以用來重新統(tǒng)計索引信息。但是這條命令生產(chǎn)環(huán)境需要聯(lián)系DBA,所以我就不做實驗了,大家可以自行實驗。

          索引要考慮 order by 的字段

          為什么這么說?因為如果我這個表中的索引是city_id,typeid的聯(lián)合索引,那優(yōu)化器就會走這個聯(lián)合索引,因為索引已經(jīng)做好了排序。

          更改limit大小能解決問題?

          把limit數(shù)量調(diào)大會影響預(yù)估行數(shù)rows,進(jìn)而影響優(yōu)化器索引的選擇嗎?

          答案是會。

          我們執(zhí)行l(wèi)imit 10

          select?*?from?sample_table?where?city_id?=?565?and?type?=?13?order?by?id?desc?limit?0,10

          圖中rows變?yōu)榱?8211,增長了10倍。如果使用limit 100,會發(fā)生什么?

          優(yōu)化器選擇了聯(lián)合索引。初步估計是rows還會翻倍,所以優(yōu)化器放棄了主鍵索引。寧愿用聯(lián)合索引后排序,也不愿意用主鍵索引了。

          為何突然出現(xiàn)異常慢查詢

          問:這個查詢語句已經(jīng)在線上穩(wěn)定運(yùn)行了非常長的時間,為何這次突然出現(xiàn)了慢查詢?

          答:以前的語句查詢條件返回結(jié)果都不為空,limit1很快就能找到那條數(shù)據(jù),返回結(jié)果。而這次代碼中查詢條件實際結(jié)果為空,導(dǎo)致了掃描了全部的主鍵索引。

          解決方案

          知道了MySQL為何選擇這個索引的原因后,我們就可以根據(jù)上面的思路來列舉出解決辦法了。

          主要有兩個大方向:

          1. 強(qiáng)制指定索引
          2. 干涉優(yōu)化器選擇

          強(qiáng)制選擇索引: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

          這樣做的優(yōu)點是見效快,問題馬上就能解決。

          缺點也很明顯:

          • 高耦合,這種語句寫在代碼里,會變得難以維護(hù),如果索引名變化了,或者沒有這個索引了,代碼就要反復(fù)修改。屬于硬編碼。
          • 很多代碼用框架封裝了SQL,force index()并不容易加進(jìn)去。

          「我們換一種辦法,我們?nèi)ヒ龑?dǎo)優(yōu)化器選擇聯(lián)合索引?!?/strong>

          干涉優(yōu)化器選擇:增大limit

          通過增大limit,我們可以讓預(yù)估掃描行數(shù)快速增加,比如改成下面的limit 0, 1000

          SELECT?*?FROM?sample_table?where?city_id?=?565?and?type?=?13?order?by?id?desc?LIMIT?0,1000

          這樣就會走上聯(lián)合索引,然后排序,但是這樣強(qiáng)行增長limit,其實總有種面向黑盒調(diào)參的感覺。我們還有更優(yōu)美的解決方案嗎?

          干涉優(yōu)化器選擇:增加包含order by id字段的聯(lián)合索引

          我們這句慢查詢使用的是order by id,但是我們卻沒有在聯(lián)合索引中加入id字段,導(dǎo)致了優(yōu)化器認(rèn)為聯(lián)合索引后還要排序,干脆就不太想走這個聯(lián)合索引了。

          我們可以新建city_id,typeid的聯(lián)合索引,來解決這個問題。

          這樣也有一定的弊端,比如我這個表到了8000w數(shù)據(jù),建立索引非常耗時,而且通常索引就有3.4個g,如果無限制的用索引解決問題,可能會帶來新的問題。表中的索引不宜過多。

          干涉優(yōu)化器選擇:寫成子查詢

          還有什么辦法?我們可以用子查詢,在子查詢里先走city_id和type的聯(lián)合索引,得到結(jié)果集后在limit1選出第一條。

          但是子查詢使用有風(fēng)險,一版DBA也不建議使用子查詢,會建議大家在代碼邏輯中完成復(fù)雜的查詢。當(dāng)然我們這句并不復(fù)雜啦~

          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優(yōu)化是個很大的工程,我們還有非常多的辦法能夠解決這句慢查詢問題,這里就不一一展開了。留給大家做為思考題了。

          總結(jié)

          本文帶大家回顧了一次MySQL優(yōu)化器選錯索引導(dǎo)致的線上慢查詢事故,可以看出MySQL優(yōu)化器對于索引的選擇并不單單依靠某一個標(biāo)準(zhǔn),而是一個綜合選擇的結(jié)果。我自己也對這方面了解不深入,還需要多多學(xué)習(xí),爭取能夠好好的做一個索引選擇的總結(jié)(挖坑)。不說了,拿起巨厚的《高性能MySQL》,開始...

          壓住我的泡面...

          「最后做個文章總結(jié):」

          • 該慢查詢語句中使用order by id導(dǎo)致優(yōu)化器在主鍵索引和city_id和type的聯(lián)合索引中有所取舍,最終導(dǎo)致選擇了更慢的索引。
          • 可以通過強(qiáng)制指定索引,建立包含id的聯(lián)合索引,增大limit等方式解決問題。
          • 平時開發(fā)時,尤其是對于特大數(shù)據(jù)量的表,要注意SQL語句的規(guī)范和索引的建立,避免事故的發(fā)生。

          參考

          《高性能MySQL》

          MySQL優(yōu)化器 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

          【業(yè)務(wù)學(xué)習(xí)】關(guān)于MySQL order by limit 走錯索引的探討:

          https://segmentfault.com/a/1190000020399424

          MySQL為什么有時候會選錯索引?:

          https://www.cnblogs.com/a-phper/p/10313888.html


          ? ? ? ?
          ???
          手把手教你 SQL 優(yōu)化的正確姿勢!
          為什么 MySQL 的自增主鍵不單調(diào)也不連續(xù)?
          為什么數(shù)據(jù)庫不應(yīng)該使用外鍵?

          覺得不錯,點個在看~

          瀏覽 51
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    大香蕉网免费伊人 | 在线观看亚州视频 | 大香蕉伊人电影 | 日本一二三区豆花视频 | 青娱乐国产极品 |