數據庫調優都不會?回家等通知去吧!

前言

面試官:你簡歷上寫了你會數據庫調優,你都是怎麼調優的?

我:加索引。

面試官:還有麼?

我:沒了。

面試官:我們公司的門你知道在哪裏吧,自己走還是我送你?

哈哈開頭這個場景是我臆想的一個面試場景,但是大家是不是覺得很真實,每個人的簡歷上但凡寫到了數據庫,都會在後面順便寫一句,會數據庫調優。

但是問題就來了,面試官一問到數據庫調優的,大家就說加索引,除了加索引大家還知道別的麼?

或者索引相關的點你全部都知道麼?聚簇索引,非聚簇索引,普通索引,唯一索引,change buffer,表鎖、行鎖、間隙鎖以及行鎖併發情況下的最大TPS是多少?還有索引爲啥會選擇錯誤?這些大家知道嘛?

我覺得調優能回答的點還是很多很多的,我自己看了《MySQL實戰》、《高性能MySQL》、《丁奇MySQL47講》之後總結了自己去面試回答的一套邏輯,個人覺得是比較不錯的一套combo,這套連招下來,一般面試官都會暗自對你豎起大拇指,反正我面試的時候基本上就是這一套。

內容就是個人理解的總結,還有書中內容的複述,需要一定的數據庫基礎知識,不過我想大家都點進來了,肯定都會了。

正文

數據庫調優其實一般情況都是我們的SQL調優,SQL的調優就可以解決大部分問題了,當然也不排除SQL執行環節的調優。

我之前在索引和數據庫基礎環節有介紹過相關的基礎知識,這裏就不過多的贅述了,但是數據庫的組成可能很多小夥伴都忘記了,那我們再看一遍結構圖吧。

我們所謂的調優也就是在,執行器執行之前的分析器,優化器階段完成的,那我們開發工作中怎麼去調優的呢?

我一般在開發涉及SQL的業務都會去本地環境跑一遍SQL,用explain去看一下執行計劃,看看分析的結果是否符合自己的預期,用沒用到相關的索引,然後再去線上環境跑一下看看執行時間(這裏只有查詢語句,修改語句也無法在線上執行)。

遇SQL不決explain,但是這裏就要說到第一個坑了。

排除緩存干擾

因爲在MySQL8.0之前我們的數據庫是存在緩存這樣的情況的,我之前就被坑過,因爲存在緩存,我發現我sql怎麼執行都是很快,當然第一次其實不快但是我沒注意到,以至於上線後因爲緩存經常失效,導致rt(Response time)時高時低。

後面就發現了是緩存的問題,我們在執行SQL的時候,記得加上SQL NoCache去跑SQL,這樣跑出來的時間就是真實的查詢時間了。

我說一下爲什麼緩存會失效,而且是經常失效。

如果我們當前的MySQL版本支持緩存而且我們又開啓了緩存,那每次請求的查詢語句和結果都會以key-value的形式緩存在內存中的,大家也看到我們的結構圖了,一個請求會先去看緩存是否存在,不存在纔會走解析器。

緩存失效比較頻繁的原因就是,只要我們一對錶進行更新,那這個表所有的緩存都會被清空,其實我們很少存在不更新的表,特別是我之前的電商場景,可能靜態表可以用到緩存,但是我們都走大數據離線分析,緩存也就沒用了。

大家如果是8.0以上的版本就不用擔心這個問題,如果是8.0之下的版本,記得排除緩存的干擾。

Explain

最開始提到了用執行計劃去分析,我想explain是大家SQL調優都會回答到的吧。

因爲這基本上是寫SQL的必備操作,那我現在問大家一個我去阿里面試被問過的一個問題:explain你記得哪些字段,分別有什麼含義?

當時我就回答上來三個,我默認大家都是有數據庫基礎的,所以每個我這裏不具體討論每個字段,怕大家忘記我貼一遍圖大家自己回憶一下。

那我再問大家一下,你們認爲統計這個統計的行數就是完全對的麼?索引一定會走到最優索引麼?

當然我都這麼問了,你們肯定也知道結果了,行數只是一個接近的數字,不是完全正確的,索引也不一定就是走最優的,是可能走錯的。

我的總行數大概有10W行,但是我去用explain去分析sql的時候,就會發現只得到了9.4W,爲啥行數只是個近視值呢?

看過基礎章節的小夥伴都知道,MySQL中數據的單位都是頁,MySQL又採用了採樣統計的方法,採樣統計的時候,InnoDB默認會選擇N個數據頁,統計這些頁面上的不同值,得到一個平均值,然後乘以這個索引的頁面數,就得到了這個索引的基數。

我們數據是一直在變的,所以索引的統計信息也是會變的,會根據一個閾值,重新做統計。

至於MySQL索引可能走錯也很好理解,如果走A索引要掃描100行,B所有隻要20行,但是他可能選擇走A索引,你可能會想MySQL是不是有病啊,其實不是的。

一般走錯都是因爲優化器在選擇的時候發現,走A索引沒有額外的代價,比如走B索引並不能直接拿到我們的值,還需要回到主鍵索引纔可以拿到,多了一次回表的過程,這個也是會被優化器考慮進去的。

他發現走A索引不需要回表,沒有額外的開銷,所有他選錯了。

如果是上面的統計信息錯了,那簡單,我們用analyze table tablename 就可以重新統計索引信息了,所以在實踐中,如果你發現explain的結果預估的rows值跟實際情況差距比較大,可以採用這個方法來處理。

還有一個方法就是force index強制走正確的索引,或者優化SQL,最後實在不行,可以新建索引,或者刪掉錯誤的索引。

覆蓋索引

上面我提到了,可能需要回表這樣的操作,那我們怎麼能做到不回表呢?在自己的索引上就查到自己想要的,不要去主鍵索引查了。

覆蓋索引

如果在我們建立的索引上就已經有我們需要的字段,就不需要回表了,在電商裏面也是很常見的,我們需要去商品表通過各種信息查詢到商品id,id一般都是主鍵,可能sql類似這樣:

select itemId from itemCenter where size between 1 and 6

因爲商品id itemId一般都是主鍵,在size索引上肯定會有我們這個值,這個時候就不需要回主鍵表去查詢id信息了。

由於覆蓋索引可以減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是一個常用的性能優化手段。

聯合索引

還是商品表舉例,我們需要根據他的名稱,去查他的庫存,假設這是一個很高頻的查詢請求,你會怎麼建立索引呢?

大家可以思考上面的回表的消耗對SQL進行優化。

是的建立一個,名稱和庫存的聯合索引,這樣名稱查出來就可以看到庫存了,不需要查出id之後去回表再查詢庫存了,聯合索引在我們開發過程中也是常見的,但是並不是可以一直建立的,大家要思考索引佔據的空間。

剛纔我舉的例子其實有點生硬,正常通過商品名稱去查詢庫存的請求是不多的,但是也不代表沒有哈,真來了,難道我們去全表掃描?

最左匹配原則

大家在寫sql的時候,最好能利用到現有的SQL最大化利用,像上面的場景,如果利用一個模糊查詢 itemname like ’張三%‘,這樣還是能利用到這個索引的,而且如果有這樣的聯合索引,大家也沒必要去新建一個商品名稱單獨的索引了。

很多時候我們索引可能沒建對,那你調整一下順序,可能就可以優化到整個SQL了。

索引下推

你已經知道了前綴索引規則,那我就說一個官方幫我們優化的東西,索引下推。

select * from itemcenter where name like '敖%' and size=22 and age = 20;

所以這個語句在搜索索引樹的時候,只能用 “敖”,找到第一個滿足條件的記錄ID1,當然,這還不錯,總比全表掃描要好。

然後呢?

當然是判斷其他條件是否滿足,比如size。

在MySQL 5.6之前,只能從ID1開始一個個回表,到主鍵索引上找出數據行,再對比字段值。

而MySQL 5.6 引入的索引下推優化(index condition pushdown), 可以在索引遍歷過程中,對索引中包含的字段先做判斷,直接過濾掉不滿足條件的記錄,減少回表次數。

唯一索引普通索引選擇難題

這個在我的面試視頻裏面其實問了好幾次了,核心是需要回答到change buffer,那change buffer又是個什麼東西呢?

當需要更新一個數據頁時,如果數據頁在內存中就直接更新,而如果這個數據頁還沒有在內存中的話,在不影響數據一致性的前提下,InooDB會將這些更新操作緩存在change buffer中,這樣就不需要從磁盤中讀入這個數據頁了。

在下次查詢需要訪問這個數據頁的時候,將數據頁讀入內存,然後執行change buffer中與這個頁有關的操作,通過這種方式就能保證這個數據邏輯的正確性。

需要說明的是,雖然名字叫作change buffer,實際上它是可以持久化的數據。也就是說,change buffer在內存中有拷貝,也會被寫入到磁盤上。

將change buffer中的操作應用到原數據頁,得到最新結果的過程稱爲merge。

除了訪問這個數據頁會觸發merge外,系統有後臺線程會定期merge。在數據庫正常關閉(shutdown)的過程中,也會執行merge操作。

顯然,如果能夠將更新操作先記錄在change buffer,減少讀磁盤,語句的執行速度會得到明顯的提升。而且,數據讀入內存是需要佔用buffer pool的,所以這種方式還能夠避免佔用內存,提高內存利用率

那麼,什麼條件下可以使用change buffer呢?

對於唯一索引來說,所有的更新操作都要先判斷這個操作是否違反唯一性約束。

要判斷表中是否存在這個數據,而這必須要將數據頁讀入內存才能判斷,如果都已經讀入到內存了,那直接更新內存會更快,就沒必要使用change buffer了。

因此,唯一索引的更新就不能使用change buffer,實際上也只有普通索引可以使用。

change buffer用的是buffer pool裏的內存,因此不能無限增大,change buffer的大小,可以通過參數innodb_change_buffer_max_size來動態設置,這個參數設置爲50的時候,表示change buffer的大小最多隻能佔用buffer pool的50%。

將數據從磁盤讀入內存涉及隨機IO的訪問,是數據庫裏面成本最高的操作之一,change buffer因爲減少了隨機磁盤訪問,所以對更新性能的提升是會很明顯的。

change buffer的使用場景

因爲merge的時候是真正進行數據更新的時刻,而change buffer的主要目的就是將記錄的變更動作緩存下來,所以在一個數據頁做merge之前,change buffer記錄的變更越多(也就是這個頁面上要更新的次數越多),收益就越大。

因此,對於寫多讀少的業務來說,頁面在寫完以後馬上被訪問到的概率比較小,此時change buffer的使用效果最好,這種業務模型常見的就是賬單類、日誌類的系統。

反過來,假設一個業務的更新模式是寫入之後馬上會做查詢,那麼即使滿足了條件,將更新先記錄在change buffer,但之後由於馬上要訪問這個數據頁,會立即觸發merge過程。這樣隨機訪問IO的次數不會減少,反而增加了change buffer的維護代價,所以,對於這種業務模式來說,change buffer反而起到了副作用。

前綴索引

我們存在郵箱作爲用戶名的情況,每個人的郵箱都是不一樣的,那我們是不是可以在郵箱上建立索引,但是郵箱這麼長,我們怎麼去建立索引呢?

MySQL是支持前綴索引的,也就是說,你可以定義字符串的一部分作爲索引。默認地,如果你創建索引的語句不指定前綴長度,那麼索引就會包含整個字符串。

我們是否可以建立一個區分度很高的前綴索引,達到優化和節約空間的目的呢?

使用前綴索引,定義好長度,就可以做到既節省空間,又不用額外增加太多的查詢成本。

上面說過覆蓋索引了,覆蓋索引是不需要回表的,但是前綴索引,即使你的聯合索引已經包涵了相關信息,他還是會回表,因爲他不確定你到底是不是一個完整的信息,就算你是[email protected]一個完整的郵箱去查詢,他還是不知道你是否是完整的,所以他需要回表去判斷一下。

下面這個也是我在阿里面試面試官問過我的,很長的字段,想做索引我們怎麼去優化他呢?

因爲存在一個磁盤佔用的問題,索引選取的越長,佔用的磁盤空間就越大,相同的數據頁能放下的索引值就越少,搜索的效率也就會越低。

我當時就回答了一個hash,把字段hash爲另外一個字段存起來,每次校驗hash就好了,hash的索引也不大。

我們都知道只要區分度過高,都可以,那我們可以採用倒序,或者刪減字符串這樣的情況去建立我們自己的區分度,不過大家需要注意的是,調用函數也是一次開銷喲,這點當時沒注意。

就比如本來是www.aobing@qq,com 其實前面的www.基本上是沒任何區分度的,所有人的郵箱都是這麼開頭的,你一搜一大堆出來,放在索引還浪費內存,你可以substring()函數截取掉前面的,然後建立索引。

我們所有人的身份證都是區域開頭的,同區域的人很多,那怎麼做良好的區分呢?REVERSE()函數翻轉一下,區分度可能就高了。

這些操作都用到了函數,我就說一下函數的坑。

條件字段函數操作

日常開發過程中,大家經常對很多字段進行函數操作,如果對日期字段操作,浮點字符操作等等,大家需要注意的是,如果對字段做了函數計算,就用不上索引了,這是MySQL的規定。

對索引字段做函數操作,可能會破壞索引值的有序性,因此優化器就決定放棄走樹搜索功能。

需要注意的是,優化器並不是要放棄使用這個索引。

這個時候大家可以用一些取巧的方法,比如 select * from tradelog where id + 1 = 10000 就走不上索引,select * from tradelog where id = 9999就可以。

隱式類型轉換

select * from t where id = 1

如果id是字符類型的,1是數字類型的,你用explain會發現走了全表掃描,根本用不上索引,爲啥呢?

因爲MySQL底層會對你的比較進行轉換,相當於加了 CAST( id AS signed int) 這樣的一個函數,上面說過函數會導致走不上索引。

隱式字符編碼轉換

還是一樣的問題,如果兩個表的字符集不一樣,一個是utf8mb4,一個是utf8,因爲utf8mb4是utf8的超集,所以一旦兩個字符比較,就會轉換爲utf8mb4再比較。

轉換的過程相當於加了CONVERT(id USING utf8mb4)函數,那又回到上面的問題了,用到函數就用不上索引了。

還有大家一會可能會遇到mysql突然卡頓的情況,那可能是MySQLflush了。

flush

redo log大家都知道,也就是我們對數據庫操作的日誌,他是在內存中的,每次操作一旦寫了redo log就會立馬返回結果,但是這個redo log總會找個時間去更新到磁盤,這個操作就是flush。

在更新之前,當內存數據頁跟磁盤數據頁內容不一致的時候,我們稱這個內存頁爲“髒頁”。

內存數據寫入到磁盤後,內存和磁盤上的數據頁的內容就一致了,稱爲“乾淨頁“。

那什麼時候會flush呢?

  1. InnoDB的redo log寫滿了,這時候系統會停止所有更新操作,把checkpoint往前推進,redo log留出空間可以繼續寫。

  2. 系統內存不足,當需要新的內存頁,而內存不夠用的時候,就要淘汰一些數據頁,空出內存給別的數據頁使用。如果淘汰的是“髒頁”,就要先將髒頁寫到磁盤。

你一定會說,這時候難道不能直接把內存淘汰掉,下次需要請求的時候,從磁盤讀入數據頁,然後拿redo log出來應用不就行了?

這裏其實是從性能考慮的,如果刷髒頁一定會寫盤,就保證了每個數據頁有兩種狀態:

  • 一種是內存裏存在,內存裏就肯定是正確的結果,直接返回;

  • 另一種是內存裏沒有數據,就可以肯定數據文件上是正確的結果,讀入內存後返回。這樣的效率最高。

  1. MySQL認爲系統“空閒”的時候,只要有機會就刷一點“髒頁”。

  2. MySQL正常關閉,這時候,MySQL會把內存的髒頁都flush到磁盤上,這樣下次MySQL啓動的時候,就可以直接從磁盤上讀數據,啓動速度會很快。

那我們怎麼做才能把握flush的時機呢?

Innodb刷髒頁控制策略,我們每個電腦主機的io能力是不一樣的,你要正確地告訴InnoDB所在主機的IO能力,這樣InnoDB才能知道需要全力刷髒頁的時候,可以刷多快。

這就要用到innodb_io_capacity這個參數了,它會告訴InnoDB你的磁盤能力,這個值建議設置成磁盤的IOPS,磁盤的IOPS可以通過fio這個工具來測試。

正確地設置innodb_io_capacity參數,可以有效的解決這個問題。

這中間有個有意思的點,刷髒頁的時候,旁邊如果也是髒頁,會一起刷掉的,並且如果周圍還有髒頁,這個連帶責任制會一直蔓延,這種情況其實在機械硬盤時代比較好,一次IO就解決了所有問題,

但是現在都是固態硬盤了,innodb_flush_neighbors=0這個參數可以不產生連帶制,在MySQL 8.0中,innodb_flush_neighbors參數的默認值已經是0了。

資料參考:《MySQL實戰》、《高性能MySQL》、《丁奇MySQL47講》

總結

在本文中我提到了以下知識點:

應該還不算全,行鎖、表鎖、間隙鎖、同步場景等等都沒怎麼提到,因爲他們的場景比較複雜,每種都可以單獨開一篇了,丁奇的MySQL裏面算是很全了,還有就是高性能MySQL大家可以展開看看,要是懶也可以等我總結。

每個點我也沒多仔細的講解,主要是篇幅原因,其實每個點在MySQL相關書籍都是很多篇幅才介紹完的,我就做個總結,對具體的概念不瞭解可以用搜索引擎查詢相關概念,不過我想我說得還算通俗易懂,歡迎大家點個在看哦~~

最後告訴大家一個好消息,

小灰的《漫畫算法》在京東平臺有優惠,原價79,現在只要不到39元歡迎掃碼看一看:

給個[在看],是對小灰最大的支持!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章