Mysql 慢查詢優化實踐

LIONIDO發表於2019-01-29

Mysql 慢查詢優化實踐

目標:

提高mysql執行效率,增加併發,提高響應速度

方案:

通過阿里雲給的慢查詢日誌excel,對耗時長,開銷大的sql語句進行優化,提升訪問速度伺服器執行效率

實踐:

分析
阿里雲給的資料庫單日報表有以下欄位

  • Create Time DBName
  • MySQL Total Execution Counts
  • MySQL Total Execution Times
  • Max Execution Time
  • Max Lock Time
  • Parse Total Row Counts
  • Parse Max Row Count
  • Return Total Row Counts
  • Return Max Row Count
  • SQL Text
  • hard ware- latencies

分別是

  • 建立時間
  • 資料庫名
  • mysql總執行數目
  • muysql總執行耗時
  • 最大執行耗時
  • 最大鎖耗時
  • 解析總行數統計
  • 解析最大行數
  • 返回總計行數
  • 返回最大行數
  • sql語句
  • 硬體延遲

根據阿里雲提供的慢查詢記錄,本次採用的優化策略如下:

查詢次數超過100次/日的高頻需求,按照最大查詢/總查詢用時最大,依次優化取得的優化收益最高.

第一條語句:

執行次數: 1114 最大耗時: 7 解析最大行數: 348325 返回最大行數 4

執行次數: 1114 最大耗時: 7 解析最大行數: 348325 返回最大行數 4

select id from appname_m_members where yiku_id = :1

可以看出,這個簡單的sql不應該有這麼大的解析行數,甚至最高要七秒鐘.

初步判斷沒有在yiku_id這個欄位加索引的可能性最大.現在我們需要尋求各種辦法來驗證下我們的猜測

分析
explain select id from appname_m_members where yiku_id = 1;

可以看到的確是沒有給yiku_id增加索引.

索引的特點

對於查詢操作能迅速縮小查詢範圍,減少row的數量,指數級提高查詢速度點

對於寫操作,因為需要維護索引的變更,有一定開銷.如果遇到大量併發寫入,會有負面影響.

在這個表用來記錄我們微信使用者和應用id的關係,所以讀的操作較之寫操作更多,所以能夠增加索引.

#增加索引

ALTER TABLE `appname_m_members` 
ADD INDEX `yiku_id` (`yiku_id`) ;

嘗試增加索引之後,再次分析語句的執行

結果:

匹配範圍 rows 從32w 降低到1

可以看到type從all的全表掃描變成ref的單個行的索引訪問,rows從全表32w降為1,說明新增索引對這條語句產生了巨大效果.

第二條語句:

執行次數: 482 最大耗時: 15 解析最大行數: 764383 返回最大行數: 482

#執行次數: 482 最大耗時: 15 解析最大行數: 764383 返回最大行數: 482
select fullname as username , linkphone as userphone , 
`userimage` , `nickname` , `hospitalname` , `partmentname` , 
`doctortitle` , `iscertification` , `fullname` 
from `users` 
where `useruuid` = `597_f66e1cb79341cedf6f24aaf01fde8611` limit 1;
分析:

對其增加索引:
#增加索引
ALTER TABLE `users` 
ADD INDEX `useruuid` (`useruuid`);

直接將掃描範圍(rows)從72w降到了1,提升明顯

結果:

匹配範圍 rows 從72w 降低到1

第三條語句:

執行次數: 820 最大耗時: 10 解析最大行數: 167214 返回最大行數 1

#執行次數: 820 最大耗時: 10 解析最大行數: 167214  返回最大行數 1
select count ( postingid ) as postnum from mediposting 
where isaudit != :1
and isgoodcase = :2
and postsection = :3
分析:

改變sql語句的順序,按照最左原則修改如下

select count(postingid) as postnum from mediposting 
where postsection = 1
and isgoodcase = 1
and isaudit != 1
結果:

主要使用的是 postsection 作為索引來統計總數,這部分無需優化.

##### 第四條語句:
執行次數: 482 最大耗時: 15 解析最大行數: 764383 返回最大行數: 482
##執行次數: 410 最大耗時: 10 解析最大行數:348325   返回最大行數 1
........
結果: 語句過長(2017個字元),巢狀了邏輯,暫不優化
第五條語句:

執行次數: 659 最大耗時: 6 解析最大行數:215115 返回最大行數 659

## 執行次數: 659 最大耗時: 6 解析最大行數:215115   返回最大行數 659
select `medigooddoc`.`docid` , `medigooddoc`.`docname` ,
 `medigooddoc`.`doctitle` , `medigooddoc`.`docimgurl` ,
  `medigooddoc`.`docdep` , `medigooddoc`.`dochospital` ,
   ( initalscore+effectevaladd ) as `effectval` 
   from `medigooddoc` 
   where ( ( initalscore+effectevaladd ) > 80 ) 
   order by rand ( ) limit 1 ;
分析:

rand()函式放在order by後面會被執行多次,優化方式:
求出隨機id後,取得對應記錄

select `medigooddoc`.`docid` , `medigooddoc`.`docname` ,
 `medigooddoc`.`doctitle` , `medigooddoc`.`docimgurl` ,
 `medigooddoc`.`docdep` , `medigooddoc`.`dochospital` ,
 ( initalscore+effectevaladd ) as `effectval` 
 from `medigooddoc` 
 where (initalscore+effectevaladd)  > 80
 and docid > ( 
     RAND() * (
         (SELECT MAX(docid) FROM `medigooddoc`) 
         -
         (SELECT MIN(docid) FROM `medigooddoc`)
    )
    +
    (SELECT MIN(docid) FROM `medigooddoc`) 
 )
 order by `docid` limit 1;

優化前語句:

可以看到掃描範圍很大(rows) 120 770行.

可以看到

  • 查詢範圍最小(rows) 1
  • 獲取最大值/最小值是直接從mysql查詢優化器返回資料(extra).mysql文件中有以下解釋:

The query contained only aggregate functions (MIN(), MAX()) that were all resolved using an index, or COUNT(*) for MyISAM, and no GROUP BY clause. The optimizer determined that only one row should be returned.

測試執行效率:

  • 執行10次 a: 2 941 ms b: 168 ms
  • 執行50次 a: 14 441 ms b: 828 ms
  • 執行100次a: 29 126 ms b: 1 645 ms

可以看到每百次執行時間已經從30s縮短到不到2秒,大大提高查詢mysql響應速度.
但是還有個問題,總共100 000的id,原來的語句查詢出的結果比較平衡,有過萬也有幾千,但是用這個語句後,總是出現小於一萬的id,結果在我們預期之外.

修正概率偏差

方案1:

增加一次對資料庫消耗不大的表查詢

# php
$round = select max(docid) as max,min(docid) as min from medigooddoc;
$rand = rand($round[`min`],$round[`max`]);
# sql
select `medigooddoc`.`docid` , `medigooddoc`.`docname` ,
 `medigooddoc`.`doctitle` , `medigooddoc`.`docimgurl` ,
 `medigooddoc`.`docdep` , `medigooddoc`.`dochospital` ,
 ( initalscore+effectevaladd ) as `effectval` 
 from `medigooddoc` 
 where (initalscore+effectevaladd)  > 80
 and docid > $rand
 order by `docid` limit 1;

這樣的問題是:會多產生一個sql互動,資料庫

方案2:

使用內連線 join 優化

#可用一
select `docid` ,`docname`,
 `doctitle` , `docimgurl` ,
 `docdep` , `dochospital` ,
 ( initalscore+effectevaladd ) as `effectval` 
 from `medigooddoc` as t1 
 join (
     select rand() * (select max(docid) from `medigooddoc`) 
     as rand
 ) as t2 
 where (t1.initalscore+t1.effectevaladd)  > 80
 and `t1`.`docid` >= t2.rand
 order by `docid` limit 1;

但是這樣有一個問題:並不是完全平均落到每條記錄上,因為記錄並不是連續的

修正概率 rand * 數量範圍,這樣概率平均到整張表存在的記錄中.

select `docid` ,`docname`,
 `doctitle` , `docimgurl` ,
 `docdep` , `dochospital` ,
 ( initalscore+effectevaladd ) as `effectval` 
 from `medigooddoc` as t1 
 join (
     select rand() * 
     (
         (select max(docid) from `medigooddoc`)
         -
         (select min(docid) from `medigooddoc`)
     )
     +
     (select min(docid) from `medigooddoc`)  
     as rand
 ) as t2 
 where (t1.initalscore+t1.effectevaladd)  > 80
 and `t1`.`docid` >= t2.rand
 order by `docid` limit 1;

綜合來說,因為方案1 產生了更多的資料庫互動,因為我們的資料庫是另一臺伺服器,網路連線開銷是比較大的,額外的查詢也會在高併發的時刻對資料庫產生更大壓力.

而方案2採用內連線的方式,僅需要一次資料庫互動就能完成,最大最小值也是直接由mysql查詢器返回,減少了種種資料庫效能開銷.故採用為最佳方案..

結果:

使用mysql儲存的表結構資訊替代了order rand()的低效率查詢.

深入理解:

第六條語句:

執行次數: 729 最大耗時: 4秒 解析最大行數:130898 返回最大行數 2

select `medigooddoc`.`docid` , `medigooddoc`.`yikuid` 
from `medigooddoc` 
where ( yikuid = 597725 or yikuid = -597725 );
分析:

優化方案:

欄位yikuid加索引

ALTER TABLE `medigooddoc`
ADD INDEX `YiKuID` (`YiKuID`);

再次執行explain分析

結果:

匹配範圍 rows 從8.3w 降低到1

第七條語句

執行次數: 474 最大耗時: 5秒 解析最大行數:261797 返回最大行數 1

select `medigooddoc`.`docid` , `medigooddoc`.`docname` ,
 `medigooddoc`.`doctitle` , `medigooddoc`.`docimgurl` 
 from `medigooddoc` order by rand ( ) limit 1;
分析

方案

將獲取一條隨機記錄 由order by rand() limit 1 改為 內連線方式

select `docid`, `docname`,
  `doctitle` , `docimgurl` 
 from `medigooddoc` as  t1 
 inner join 
 (
     select rand()  *
     (
         (select MAX(docid) from `medigooddoc`)
         -
         (select MIN(docid) from `medigooddoc`)
     )
     +
     (select MIN(docid) from `medigooddoc`)
     as rand
 ) as t2 
 on t1.docid >= t2.rand
 order by docid limit 1;

再次執行explain分析

結果

用mysql儲存的表資訊替代了效率低下的order by rand()

第八條語句

執行次數: 136 最大耗時: 7秒 解析最大行數:301880 返回最大行數 1

select `searchrecords`.`searchid` , `searchrecords`.`searchnum` 
from `searchrecords` 
where ( searchtype = 0 ) and ( userid = 14 ) 
and ( searchmsg = `碳酸鈣D3` );
分析

方案

索引的目的是為了縮小查詢範圍,通過文字內容的前三個字區分,通過userid進行區分,可以得到範圍更精確的語句執行

ALTER TABLE searchrecords ADD INDEX searchmsg (searchmsg(5));
ALTER TABLE searchrecords ADD INDEX userid (userid);

通過文字前5個字建立索引來區分範圍後,範圍縮小到28個記錄

再通過使用者ID建立索引,進一步縮小範圍,僅需要查詢1條記錄

分析索引對寫入的影響
表主要用來記錄使用者搜尋的高頻詞,主要的寫操作時更新統計欄位,這兩個新增索引的欄位並不會頻繁更新,故索引開銷不大.

結果

匹配範圍從 29w 縮小到 1

第九條語句
select `projects`.`id` , `projects`.`guid` , 
`projects`.`getittime` , `projects`.`keywords` ,
 `projects`.`barcode` as `num` , `projects`.`goodcasedep` ,
  `projects`.`bingshi` , `pictures`.* 
  from `projects` 
  inner join `pictures` on projects.guid = pictures.projectid 
  and pictures.filetype = :1 
  where ( islock != :2 ) and ( isgoodcase = :3 ) 
  and ( ( goodcasedep like :4 or goodcasedep like :5 
  or goodcasedep like :6 or goodcasedep like :7 
  or goodcasedep like :8 or goodcasedep like :9 
  or goodcasedep like :10 or goodcasedep like :11 
  or goodcasedep like :12 or goodcasedep like :13 
  or goodcasedep like :14 or goodcasedep like :15 
  or goodcasedep like :16 or goodcasedep like :17 
  or goodcasedep like :18 or goodcasedep like :19 
  or goodcasedep like :20 or goodcasedep like :21 
  or goodcasedep like :22 or goodcasedep like :23 
  or goodcasedep like :24 or goodcasedep like :25 
  or goodcasedep like :26 or goodcasedep like :27 
  or goodcasedep like :28 or goodcasedep like :29 
  or goodcasedep like :30 or goodcasedep like :31 
  or goodcasedep like :32 or goodcasedep like :33 
  or goodcasedep like :34 or goodcasedep like :35 
  or goodcasedep like :36 or goodcasedep like :37 
  or goodcasedep like :38 or goodcasedep like :39 
  or goodcasedep like :40 or goodcasedep like :41 ) ) 
  order by rand ( ) limit :42
結果:

暫不修改:超過位元組限制

第十條語句

執行次數: 145 最大耗時: 2秒 解析最大行數:130898 返回最大行數 1

select `medigooddoc`.`isfollow` , `medigooddoc`.`isconsult` ,
 `medigooddoc`.`isphone` , `medigooddoc`.`isprivate` 
 from `medigooddoc` where ( yikuid =  694 );
分析:

方案

增加索引

ALTER TABLE `medigooddoc` ADD INDEX YiKuID(`YiKuID`);

再次執行explain分析

結果

匹配範圍從12w縮小到1

第十一條

執行次數: 148 最大耗時: 3秒 解析最大行數:74616 返回最大行數 30

select `magazinearticle`.`articleid` , 
`magazinearticle`.`articletitle` , 
`magazinearticle`.`article_publishtime` ,
 `magazinearticle`.`articlepicpath` ,
  `magazinearticle`.`articleurl` ,
   `magazinearticle`.`articlenum` ,
    `magazinearticle`.`perid` ,
     `magazinearticle`.`article_originallink` ,
      `magazinearticle`.`islink` from `magazinearticle` 
      where ( logicdel = 0 ) and ( perid != 60 ) 
      order by `article_publishtime` desc limit 1,30;
分析:

方案:

由於是讀多寫少的文章表,增加索引適用這類場景,提高查詢響應速度.

ALTER TABLE `magazinearticle` ADD INDEX 
article_publishtime(`article_publishtime`);

再次執行explain分析

結果:

匹配範圍 rows 從2w縮小到59

深入理解:

explain type的不同種類

型別 含義
型別 含義
system 表只有一行
const 表最多隻有一行匹配,通用用於主鍵或者唯一索引比較時
eq_ref 每次與之前的表合併行都只在該表讀取一行,這是除了system,const之外最好的一種,特點是使用=,而且索引的所有部分都參與join且索引是主鍵或非空唯一鍵的索引
ref 如果每次只匹配少數行,那就是比較好的一種,使用=或<=>,可以是左覆蓋索引或非主鍵或非唯一鍵
fulltext 全文搜尋
ref_or_null 與ref類似,但包括NULL
index_merge 表示出現了索引合併優化(包括交集,並集以及交集之間的並集),但不包括跨表和全文索引。
這個比較複雜,目前的理解是合併單表的範圍索引掃描(如果成本估算比普通的range要更優的話)
unique_subquery 在in子查詢中,就是value in (select…)把形如“select unique_key_column”的子查詢替換。
PS:所以不一定in子句中使用子查詢就是低效的!
index_subquery 同上,但把形如”select non_unique_key_column“的子查詢替換
range 常數值的範圍
index a.當查詢是索引覆蓋的,即所有資料均可從索引樹獲取的時候(Extra中有Using Index)
b.以索引順序從索引中查詢資料行的全表掃描(無 Using Index);
c.如果Extra中Using Index與Using Where同時出現的話,則是利用索引查詢鍵值的意思;
d.如單獨出現,則是用讀索引來代替讀行,但不用於查詢
all 全表掃描
第十二條

執行次數: 135 最大耗時: 3秒 解析最大行數:78395 返回最大行數 0

select distinct userid from weekhosnominate 
where userid = 351211 and datatype = 4
分析

方案
ALTER TABLE `weekhosnominate` ADD INDEX UserID(`UserID`);

再次執行explain分析

結果

匹配範圍 rows 從1w縮小到288

第十三條

執行次數: 110 最大耗時: 2秒 解析最大行數:87693 返回最大行數 1

select `inspectioninfo`.`itemmsg` from `inspectioninfo` 
where ( itemid in ( 30 ,31 ) and itemtype = 0
and inspectionid = 109 ) limit 1 ;
分析

方案:

增加索引

ALTER TABLE `inspectioninfo` ADD INDEX 
InspectionID(`InspectionID`);

再次執行explain分析

結果

匹配範圍 rows 從 5w 縮小到 13

第十四條語句

執行次數: 103 最大耗時: 2秒 解析最大行數:78395 返回最大行數 0

select `weekhosnominate`.`id` from `weekhosnominate` 
where ( userid = 351211 );
分析:

方案:

通過給欄位 userid 建立索引來區分,縮小範圍

ALTER TABLE `weekhosnominate` ADD INDEX UserID(UserID) ;

再次執行explain分析可以發現,
通過索引 userid 將範圍由全表掃描的近萬到索引指向的數十條記錄.

結果:

匹配範圍 rows 從 9k 縮小到 288

深入理解:


mysql結構

mysql索引原理

索引目的

索引的目的在於提高查詢效率,可以類比字典,如果要查“mysql”這個單詞,我們肯定需要定位到m字母,然後從下往下找到y字母,再找到剩下的sql。如果沒有索引,那麼你可能需要把所有單詞看一遍才能找到你想要的,如果我想找到m開頭的單詞呢?或者ze開頭的單詞呢?是不是覺得如果沒有索引,這個事情根本無法完成?

索引原理

除了詞典,生活中隨處可見索引的例子,如火車站的車次表、圖書的目錄等。它們的原理都是一樣的,通過不斷的縮小想要獲得資料的範圍來篩選出最終想要的結果,同時把隨機的事件變成順序的事件,也就是我們總是通過同一種查詢方式來鎖定資料。

資料庫也是一樣,但顯然要複雜許多,因為不僅面臨著等值查詢,還有範圍查詢(>、<、between、in)、模糊查詢(like)、並集查詢(or)等等。資料庫應該選擇怎麼樣的方式來應對所有的問題呢?我們回想字典的例子,能不能把資料分成段,然後分段查詢呢?最簡單的如果1000條資料,1到100分成第一段,101到200分成第二段,201到300分成第三段……這樣查第250條資料,只要找第三段就可以了,一下子去除了90%的無效資料。但如果是1千萬的記錄呢,分成幾段比較好?稍有演算法基礎的同學會想到搜尋樹,其平均複雜度是lgN,具有不錯的查詢效能。但這裡我們忽略了一個關鍵的問題,複雜度模型是基於每次相同的操作成本來考慮的,資料庫實現比較複雜,資料儲存在磁碟上,而為了提高效能,每次又可以把部分資料讀入記憶體來計算,因為我們知道訪問磁碟的成本大概是訪問記憶體的十萬倍左右,所以簡單的搜尋樹難以滿足複雜的應用場景。

磁碟IO與預讀

前面提到了訪問磁碟,那麼這裡先簡單介紹一下磁碟IO和預讀,磁碟讀取資料靠的是機械運動,每次讀取資料花費的時間可以分為尋道時間、旋轉延遲、傳輸時間三個部分,尋道時間指的是磁臂移動到指定磁軌所需要的時間,主流磁碟一般在5ms以下;旋轉延遲就是我們經常聽說的磁碟轉速,比如一個磁碟7200轉,表示每分鐘能轉7200次,也就是說1秒鐘能轉120次,旋轉延遲就是1/120/2 = 4.17ms;傳輸時間指的是從磁碟讀出或將資料寫入磁碟的時間,一般在零點幾毫秒,相對於前兩個時間可以忽略不計。那麼訪問一次磁碟的時間,即一次磁碟IO的時間約等於5+4.17 = 9ms左右,聽起來還挺不錯的,但要知道一臺500 -MIPS的機器每秒可以執行5億條指令,因為指令依靠的是電的性質,換句話說執行一次IO的時間可以執行40萬條指令,資料庫動輒十萬百萬乃至千萬級資料,每次9毫秒的時間,顯然是個災難。下圖是計算機硬體延遲的對比圖,供大家參考:

硬體處理延遲

REFER:

MySQL索引原理及慢查詢優化

相關文章