天天看點

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

作者:架構師成長曆程

在面試中被問到MySQL慢查詢的機率還是非常高的。

說你沒有經曆過就是理由嗎?顯然不是。

一般來說一句SQL語句執行超過5s就能夠算是慢SQL,需要進行優化了。

為何要對慢SQL進行治理

每一個SQL都需要消耗一定的I/O資源,SQL執行的快慢直接決定了資源被占用時間的長短。假設業務要求每秒需要完成100條SQL的執行,而其中10條SQL執行時間過長,進而導緻每秒隻能完成90條SQL,所有新的SQL将進入排隊等待,直接影響業務,然後使用者就各種投訴來了。

治理的優先級

  1. master資料庫->slave資料庫 采用讀寫分離架構,讀在從庫slave上執行,寫在主庫master上執行。但由于從庫的資料都是在主庫複制過去的,主庫如果等待較多的情況,會加大從庫的複制延時
  2. 執行SQL次數多的優先治理
  3. 某張表被高并發集中通路的優先治理

MySQL執行原理

為了更好的優化慢SQL,我們來簡單了解下MySQL的執行原理
MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

綠色部分為SQL實際執行部分,主要分為兩步:

  1. 解析:詞法解析->文法解析->邏輯計劃->查詢優化->實體執行計劃,過程中會檢查緩存是否可用,如果沒有可用緩存則進入下一步mysql_execute_command執行
  2. 執行:檢查使用者、表權限->表加上共享讀鎖->取資料到query_cache->取消共享讀鎖

如何發現慢查詢SQL

-- 修改慢查詢時間,隻能目前會話有效;
set long_query_time=1; ­
-- 啟用慢查詢 ,加上global,不然會報錯的;
set global slow_query_log='ON';­­ 
-- 是否開啟慢查詢;
show variables like "%slow%";­­ 
-- 查詢慢查詢SQL狀況;
show status like "%slow%"; ­­ 
-- 慢查詢時間(預設情況下MySQL認位10秒以上才是慢查詢)
show variables like "long_query_time"; ­­ 
           

除了sql的方式,我們也可以在配置檔案(my.ini)中修改,加入配置時必須要在[mysqld]後面加入

-- 開啟日志;
slow_query_log = on
-- 記錄日志的log檔案(注意:window上必須寫絕對路徑)
slow_query_log_file = D:/mysql5.5.16/data/showslow.log
-- 最長查詢的秒數;
long_query_time = 2
-- 表示記錄沒有使用索引的查詢
log­queries­not­using­indexes ­­ 
           
特别注意:開啟慢查詢會帶來CPU損耗與日志記錄的IO開銷,是以建議間斷性的打開慢查詢日志來觀察MySQL運作狀态

慢查詢分析示例

假設我們有一條SQL

SELECT * FROM `emp` where ename like '%mQspyv%'; 
           

執行時間為1.163s,而我們設定的慢查詢時間為1s,這時我們可以打開慢查詢日志進行日志分析:

## Time: 150530 15:30:58 ­­ -- 該查詢發生在2015­5­30 15:30:58
## User@Host: root[root] @ localhost [127.0.0.1] ­­ --是誰,在什麼主機上發生的查詢
## Query_time: 1.134065 Lock_time: 0.000000 Rows_sent: 8 Rows_examined: 4000000 Query_time: --查詢總共用了多少時間,Lock_time: 在查詢時鎖定表的時間,Rows_sent: 傳回多少rows資料,Rows_examined: 表掃描了400W行資料才得到的結果;
           

如果我們的慢SQL很多,人工分析肯定分析不過來,這時候我們就需要借助一些分析工具,MySQL自帶了一個慢查詢分析工具mysqldumpslow,以下是常見使用示例

mysqldumpslow ­s c ­t 10 /var/run/mysqld/mysqld­slow.log # 取出使用最多的10條慢查詢
mysqldumpslow ­s t ­t 3 /var/run/mysqld/mysqld­slow.log # 取出查詢時間最慢的3條慢查詢
mysqldumpslow ­s t ­t 10 ­g “left join” /database/mysql/slow­log #得到按照時間排序的前10條裡面含有左連接配接的查詢語句
mysqldumpslow ­s r ­t 10 ­g 'left join' /var/run/mysqld/mysqldslow.log # 按照掃描行數最多的
           

SQL語句常見優化

隻要簡單了解過MySQL内部優化機制,就很容易寫出高性能的SQL

不使用子查詢

SELECT * FROM t1 WHERE id (SELECT id FROM t2 WHERE name='hechunyang');
           

在MySQL5.5版本中,内部執行計劃器是先查外表再比對内表,如果外表資料量很大,查詢速度會非常慢

再MySQL5.6中,有對内查詢做了優化,優化後SQL如下

SELECT t1.* FROM t1 JOIN t2 ON t1.id = t2.id;
           

但也僅針對select語句有效,update、delete子查詢無效,是以生成環境不建議使用子查詢

避免函數索引

SELECT * FROM t WHERE YEAR(d) >= 2016;
           

即使d字段有索引,也會全盤掃描,應該優化為:

SELECT * FROM t WHERE d >= '2016-01-01';
           

使用IN替換OR

SELECT * FROM t WHERE LOC_ID = 10 OR LOC_ID = 20 OR LOC_ID = 30;
           

非聚簇索引走了3次,使用IN之後隻走一次:

SELECT * FROM t WHERE LOC_IN IN (10,20,30);
           

LIKE雙百分号無法使用到索引

SELECT * FROM t WHERE name LIKE '%de%';
           

應優化為右模糊

SELECT * FROM t WHERE name LIKE 'de%';
           

增加LIMIT M,N 限制讀取的條數

避免資料類型不一緻

SELECT * FROM t WHERE id = '19';
           

應優化為

SELECT * FROM t WHERE id = 19;
           

分組統計時可以禁止排序

SELECT goods_id,count(*) FROM t GROUP BY goods_id;
           

預設情況下MySQL會對所有GROUP BY co1,col2 …的字段進行排序,我們可以對其使用

ORDER BY NULL
           

禁止排序,避免排序消耗資源

SELECT goods_id,count(*) FROM t GROUP BY goods_id ORDER BY NULL;
           

去除不必要的ORDER BY語句

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

csharp複制代碼select id from appname_m_members where yiku_id = :1
           

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

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

分析

explain select id from appname_m_members where yiku_id = 1;
           
MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

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

索引的特點

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

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

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

markdown複制代碼#增加索引

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

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

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

結果:

比對範圍 rows 從32w 降低到1

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

第二條語句:

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

javascript複制代碼#執行次數: 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;
           

分析:

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

對其增加索引:

markdown複制代碼#增加索引
ALTER TABLE `users` 
ADD INDEX `useruuid` (`useruuid`);
           
MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

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

結果:

比對範圍 rows 從72w 降低到1

第三條語句:

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

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

分析:

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

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

csharp複制代碼select count(postingid) as postnum from mediposting 
where postsection = 1
and isgoodcase = 1
and isaudit != 1
           

結果:

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

shell複制代碼##### 第四條語句:
執行次數: 482 最大耗時: 15 解析最大行數: 764383 傳回最大行數: 482
##執行次數: 410 最大耗時: 10 解析最大行數:348325   傳回最大行數 1
........
           

結果: 語句過長(2017個字元),嵌套了邏輯,暫不優化

第五條語句:

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

perl複制代碼## 執行次數: 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後,取得對應記錄

javascript複制代碼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;
           

優化前語句:

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

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

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

可以看到

  • 查詢範圍最小(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:

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

python複制代碼# php
$round = select max(docid) as max,min(docid) as min from medigooddoc;
$rand = rand($round['min'],$round['max']);
           
perl複制代碼# 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 優化

perl複制代碼#可用一
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 * 數量範圍,這樣機率平均到整張表存在的記錄中.

javascript複制代碼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慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

結果:

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

深入了解:

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

第六條語句:

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

go複制代碼select `medigooddoc`.`docid` , `medigooddoc`.`yikuid` 
from `medigooddoc` 
where ( yikuid = 597725 or yikuid = -597725 );
           

分析:

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

優化方案:

字段yikuid加索引

go複制代碼ALTER TABLE `medigooddoc`
ADD INDEX `YiKuID` (`YiKuID`);
           

再次執行explain分析

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

結果:

比對範圍 rows 從8.3w 降低到1

第七條語句

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

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

分析

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

方案

将擷取一條随機記錄 由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慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

結果

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

第八條語句

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

go複制代碼select `searchrecords`.`searchid` , `searchrecords`.`searchnum` 
from `searchrecords` 
where ( searchtype = 0 ) and ( userid = 14 ) 
and ( searchmsg = '碳酸鈣D3' );
           

方案

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

sql複制代碼ALTER TABLE searchrecords ADD INDEX searchmsg (searchmsg(5));
ALTER TABLE searchrecords ADD INDEX userid (userid);
           

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

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

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

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

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

結果

比對範圍從 29w 縮小到 1

第九條語句

less複制代碼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

go複制代碼select `medigooddoc`.`isfollow` , `medigooddoc`.`isconsult` ,
 `medigooddoc`.`isphone` , `medigooddoc`.`isprivate` 
 from `medigooddoc` where ( yikuid =  694 );
           

分析:

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

方案

增加索引

sql複制代碼ALTER TABLE `medigooddoc` ADD INDEX YiKuID(`YiKuID`);
           

再次執行explain分析

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

結果

比對範圍從12w縮小到1

第十一條

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

go複制代碼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;
           

分析:

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

方案:

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

sql複制代碼ALTER TABLE `magazinearticle` ADD INDEX 
article_publishtime(`article_publishtime`);
           

再次執行explain分析

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

結果:

比對範圍 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

sql複制代碼select distinct userid from weekhosnominate 
where userid = 351211 and datatype = 4
           

分析

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

方案

sql複制代碼ALTER TABLE `weekhosnominate` ADD INDEX UserID(`UserID`);
           

再次執行explain分析

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

結果

比對範圍 rows 從1w縮小到288

第十三條

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

ini複制代碼select `inspectioninfo`.`itemmsg` from `inspectioninfo` 
where ( itemid in ( 30 ,31 ) and itemtype = 0
and inspectionid = 109 ) limit 1 ;
           

分析

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

方案:

增加索引

sql複制代碼ALTER TABLE `inspectioninfo` ADD INDEX 
InspectionID(`InspectionID`);
           

再次執行explain分析

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

結果

比對範圍 rows 從 5w 縮小到 13

第十四條語句

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

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

分析:

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

方案:

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

sql複制代碼ALTER TABLE `weekhosnominate` ADD INDEX UserID(UserID) ;
           

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

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

結果:

比對範圍 rows 從 9k 縮小到 288

深入了解:

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

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毫秒的時間,顯然是個災難。下圖是計算機硬體延遲的對比圖,供大家參考:

MySQL慢查詢之慢 SQL 定位、日志分析與優化方案以及實踐

硬體處理延

總結

總的來說,我們知道曼查詢的SQL後,優化方案可以做如下嘗試:

  1. SQL語句優化,盡量精簡,去除非必要語句
  2. 索引優化,讓所有SQL都能夠走索引
  3. 如果是表的瓶頸問題,則分表,單表資料量維持在2000W(理論上)以内
  4. 如果是單庫瓶頸問題,則分庫,讀寫分離
  5. 如果是實體機器性能問題,則分多個資料庫節點

繼續閱讀