1. 程式人生 > >MySQL慢查詢優化(線上案例調優)

MySQL慢查詢優化(線上案例調優)

## 文章說明 這篇文章主要是記錄自己最近在真實工作中遇到的慢查詢的案例,然後進行調優分析的過程,歡迎大家一起討論調優經驗。(以下出現的表名,列名都是化名,實際資料也進行過一點微調。) PS:最近做了一個面試題精選精答的開源專案,如果想要了解更多MySQL相關的技術總結,可以看一看,如果對大家有幫助,希望大家幫忙給一個star,謝謝大家了! 《面試指北》專案地址:https://github.com/NotFound9/interviewGuide ![](https://user-gold-cdn.xitu.io/2020/5/11/172033a79b3d52d5) 之前建了一個技術交流群,大家感興趣也可以進一下,希望可以和大家一起學習進步! ## 一.複雜的深分頁問題優化 #### 背景 有一個article表,用於儲存文章的基本資訊的,有文章id,作者id等一些屬性,有一個content表,主要用於儲存文章的內容,主鍵是article_id,需求需要將一些滿足條件的作者釋出的文章匯入到另外一個庫,所以我同事就在專案中先查詢出了符合條件的作者id,然後開啟了多個執行緒,每個執行緒每次取一個作者id,執行查詢和匯入工作。 查詢出作者id是1111,名下的所有文章資訊,文章內容相關的資訊的SQL如下: ```SQL SELECT a.*, c.* FROM article a LEFT JOIN content c ON a.id = c.article_id WHERE a.author_id = 1111 AND a.create_time < '2020-04-29 00:00:00' LIMIT 210000,100 ``` 因為查詢的這個資料庫是機械硬碟的,在offset查詢到20萬時,查詢時間已經特別長了,運維同事那邊直接收到報警,說這個庫已經IO阻塞了,已經多次進行主從切換了,我們就去navicat裡面試著執行了一下這個語句,也是一直在等待, 然後對資料庫執行show proceesslist 命令查看了一下,發現每個查詢都是處於Writing to net的狀態,沒辦法只能先把匯入的專案暫時下線,然後執行kill命令將當前的查詢都殺死程序(因為只是客戶端Stop的話,MySQL服務端會繼續查詢)。 然後我們開始分析這條命令執行慢的原因: #### 是否是聯合索引的問題 當前是索引情況如下: ``` article表的主鍵是id,author_id是一個普通索引 content表的主鍵是article_id ``` 所以認為當前是執行流程是先去article表的普通索引author_id裡面找到1111的所有文章id,然後根據這些文章id去article表的聚集索引中找到所有的文章,然後拿每個文章id去content表中找文章內容等資訊,然後判斷create_time是否滿足要求,進行過濾,最終找到offset為20000後的100條資料。 所以我們就將article的author_id索引改成了聯合索引(author_id,create_time),這樣聯合索引(author_id,create_time)中的B+樹就是先安裝author_id排序,再按照create_time排序,這樣一開始在聯合(author_id,create_time)查詢出來的文章id就是滿足create_time < '2020-04-29 00:00:00'條件的,後面就不用進行過濾了,就不會就是符合就不用對create_time過濾。 流程確實是這個流程,但是去查詢時,如果limit還是210000, 100時,還是查不出資料,幾分鐘都沒有資料,一直到navica提示超時,使用Explain看的話,確實命中索引了,如果將offset調小,調成6000, 100,勉強可以查出資料,但是需要46s,所以瓶頸不在這裡。 真實原因如下: 先看關於深分頁的兩個查詢,id是主鍵,val是普通索引 #### 直接查詢法 ```SQL select * from test where val=4 limit 300000,5; ``` #### 先查主鍵再join ```SQL select * from test a inner join (select id from test where val=4 limit 300000,5) as b on a.id=b.id; ``` 這兩個查詢的結果都是查詢出offset是30000後的5條資料,區別在於第一個查詢需要先去普通索引val中查詢出300005個id,然後去聚集索引下讀取300005個數據頁,然後拋棄前面的300000個結果,只返回最後5個結果,過程中會產生了大量的隨機I/O。第二個查詢一開始在普通索引val下就只會讀取後5個id,然後去聚集索引下讀取5個數據頁。 同理我們業務中那條查詢其實是更加**複雜**的情況,因為我們業務的那條SQL不僅會讀取article表中的210100條結果,而且會每條結果去content表中查詢文章相關內容,而這張表有幾個TEXT型別的欄位,我們使用show table status命令查看錶相關的資訊發現 | Name | Engine | Row_format | Rows | Avg_Row_length | | ------- | ------ | ---------- | ------- | -------------- | | article | InnoDB | Compact | 2682682 | 266 | | content | InnoDB | Compact | 2824768 | 16847 | 發現兩個表的資料量都是200多萬的量級,article表的行平均長度是266,content表的平均長度是16847,簡單來說是當 InnoDB 使用 Compact 或者 Redundant 格式儲存極長的 VARCHAR 或者 BLOB 這類大物件時,我們並不會直接將所有的內容都存放在資料頁節點中,而是將行資料中的前 768 個位元組儲存在資料頁中,後面會通過偏移量指向溢位頁。 (詳細瞭解可以看看這篇文章[深度好文帶你讀懂MySQL和InnoDB](https://mp.weixin.qq.com/s?src=11×tamp=1588316993&ver=2311&signature=wlqIQrV2ZK4JJhqP4E1hqr8j3SBaQSEaiPoPM2KlAF9z-*jpWnwYiORweW3LDIWfY2J6LY8coaqXDMFezKZvEIEGRIaMEs5G*0N4naBh9DBCmUjRQnvuluU8Q5LOPttc&new=1)) ![img](https://user-gold-cdn.xitu.io/2020/5/1/171cf4968afd910e?w=640&h=144&f=jpeg&s=5598) 這樣再從content表裡面查詢連續的100行資料時,讀取每行資料時,還需要去讀溢位頁的資料,這樣就需要大量隨機IO,因為機械硬碟的硬體特性,隨機IO會比順序IO慢很多。所以我們後來又進行了測試, 只是從article表裡面查詢limit 200000,100的資料,發現即便存在深分頁的問題,查詢時間只是0.5s,因為article表的平均列長度是266,所有資料都存在資料頁節點中,不存在頁溢位,所以都是順序IO,所以比較快。 ```SQL //查詢時間0.51s SELECT a.* FROM article a WHERE a.author_id = 1111 AND a.create_time < '2020-04-29 00:00:00' LIMIT 200100, 100 ``` 相反的,我們直接先找出100個article_id去content表裡面查詢資料,發現比較慢,第一次查詢時需要3s左右(也就是這些id的文章內容相關的資訊都沒有過,沒有快取的情況),第二次查詢時因為這些溢位頁資料已經載入到buffer pool,所以大概0.04s。 ```SQL SELECT SQL_NO_CACHE c.* FROM article_content c WHERE c.article_id in(100個article_id) ``` ### 解決方案 所以針對這個問題的解決方案主要有兩種: #### 先查出主鍵id再inner join 非連續查詢的情況下,也就是我們在查第100頁的資料時,不一定查了第99頁,也就是允許跳頁查詢的情況,那麼就是使用**先查主鍵再join**這種方法對我們的業務SQL進行改寫成下面這樣,下查詢出210000, 100時主鍵id,作為臨時表temp_table,將article表與temp_table表進行inner join,查詢出中文章相關的資訊,並且去left Join content表查詢文章內容相關的資訊。 第一次查詢大概1.11s,後面每次查詢大概0.15s ```SQL SELECT a.*, c.* FROM article a INNER JOIN( SELECT id FROM article a WHERE a.author_id = 1111 AND a.create_time < '2020-04-29 00:00:00' LIMIT 210000 , 100 ) as temp_table ON a.id = temp_table.id LEFT JOIN content c ON a.id = c.article_id ``` #### 優化結果 優化前,offset達到20萬的量級時,查詢時間過長,一直到超時。 優化後,offset達到20萬的量級時,查詢時間為1.11s。 #### 利用範圍查詢條件來限制取出的資料 這種方法的大致思路如下,假設要查詢test_table中offset為10000的後100條資料,假設我們事先已知第10000條資料的id,值為min_id_value `select * from test_table where id > min_id_value order by id limit 0`, 100,就是即利用條件id > min_id_value在掃描索引是跳過10000條記錄,然後取100條資料即可,這種處理方式的offset值便成為0了,但此種方式有限制,必須知道offset對應id,然後作為min_id_value,增加id > min_id_value的條件來進行過濾,如果是用於分頁查詢的話,也就是必須知道上一頁的最大的id,所以只能一頁一頁得查,不能跳頁,但是因為我們的業務需求就是每次100條資料,進行分批導資料,所以我們這種場景是可以使用。針對這種方法,我們的業務SQL改寫如下: ```SQL //先查出最大和最小的id SELECT min(a.id) as min_id , max(a.id) as max_id FROM article a WHERE a.author_id = 1111 AND a.create_time < '2020-04-29 00:00:00' //然後每次迴圈查詢 whil