有一個article表,用於存儲文章的基本信息的,有文章id,做者id等一些屬性,有一個content表,主要用於存儲文章的內容,主鍵是article_id,需求須要將一些知足條件的做者發佈的文章導入到另一個庫。java
因此我同事就在項目中先查詢出了符合條件的做者id,而後開啓了多個線程,每一個線程每次取一個做者id,執行查詢和導入工做。面試
查詢出做者id是1111,名下的全部文章信息,文章內容相關的信息的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萬時,查詢時間已經特別長了。spring
運維同事那邊直接收到報警,說這個庫已經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是普通索引
二、直接查詢法
select * from test where val=4 limit 300000,5;
三、先查主鍵再join
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命令查看錶相關的信息發現
發現兩個表的數據量都是200多萬的量級,article表的行平均長度是266,content表的平均長度是16847。
簡單來講是當 InnoDB 使用 Compact 或者 Redundant 格式存儲極長的 VARCHAR 或者 BLOB 這類大對象時,咱們並不會直接將全部的內容都存放在數據頁節點中,而是將行數據中的前 768 個字節存儲在數據頁中,後面會經過偏移量指向溢出頁。
這樣再從content表裏面查詢連續的100行數據時,讀取每行數據時,還須要去讀溢出頁的數據,這樣就須要大量隨機IO,由於機械硬盤的硬件特性,隨機IO會比順序IO慢不少。因此咱們後來又進行了測試,
只是從article表裏面查詢limit 200000,100的數據,發現即使存在深分頁的問題,查詢時間只是0.5s,由於article表的平均列長度是266,全部數據都存在數據頁節點中,不存在頁溢出,因此都是順序IO,因此比較快。
//查詢時間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。
SELECT SQL_NO_CACHE c.* FROM article_content c WHERE c.article_id in(100個article_id)
四、解決方案
因此針對這個問題的解決方案主要有兩種:
(1)先查出主鍵id再inner join
非連續查詢的狀況下,也就是咱們在查第100頁的數據時,不必定查了第99頁,也就是容許跳頁查詢的狀況。
那麼就是使用先查主鍵再join這種方法對咱們的業務SQL進行改寫成下面這樣,下查詢出210000, 100時主鍵id,做爲臨時表temp_table,將article表與temp_table表進行inner join,查詢出中文章相關的信息,而且去left Join content表查詢文章內容相關的信息。
第一次查詢大概1.11s,後面每次查詢大概0.15s
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。
(2)利用範圍查詢條件來限制取出的數據
這種方法的大體思路以下,假設要查詢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改寫以下:
//先查出最大和最小的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' //而後每次循環查找 while(min_id<max_id) { SELECT a.*, c.* FROM article a LEFT JOIN content c ON a.id = c.article_id WHERE a.author_id = 1111 AND a.id > min_id LIMIT 100 //這100條數據導入完畢後,將100條數據數據中最大的id賦值給min_id,以便導入下100條數據 }
優化結果
優化前,offset達到20萬的量級時,查詢時間過長,一直到超時。
優化後,offset達到20萬的量級時,因爲知道第20萬條數據的id,查詢時間爲0.34s。
聯合索引其實有兩個做用:
一、充分利用where條件,縮小範圍
例如咱們須要查詢如下語句:
SELECT * FROM test WHERE a = 1 AND b = 2
若是對字段a創建單列索引,對b創建單列索引,那麼在查詢時,只能選擇走索引a,查詢全部a=1的主鍵id,而後進行回表,在回表的過程當中,在彙集索引中讀取每一行數據,而後過濾出b = 2結果集,或者走索引b,也是這樣的過程。
若是對a,b創建了聯合索引(a,b),那麼在查詢時,直接在聯合索引中先查到a=1的節點,而後根據b=2繼續往下查,查出符合條件的結果集,進行回表。
二、避免回表(此時也叫覆蓋索引)
這種狀況就是假如咱們只查詢某幾個經常使用字段,例如查詢a和b以下:
SELECT a,b FROM test WHERE a = 1 AND b = 2
對字段a創建單列索引,對b創建單列索引就須要像上面所說的,查到符合條件的主鍵id集合後須要去彙集索引下回表查詢,可是若是咱們要查詢的字段自己在聯合索引中就都包含了,那麼就不用回表了。
三、減小須要回表的數據的行數
這種狀況就是假如咱們須要查詢a>1而且b=2的數據
SELECT * FROM test WHERE a > 1 AND b = 2
若是創建的是單列索引a,那麼在查詢時會在單列索引a中把a>1的主鍵id所有查找出來而後進行回表。
若是創建的是聯合索引(a,b),基於最左前綴匹配原則,由於a的查詢條件是一個範圍查找(=或者in以外的查詢條件都是範圍查找),這樣雖然在聯合索引中查詢時只能命中索引a的部分,b的部分命中不了,只能根據a>1進行查詢,可是因爲聯合索引中每一個葉子節點包含b的信息,在查詢出全部a>1的主鍵id時,也會對b=2進行篩選,這樣須要回表的主鍵id就只有a>1而且b=2這部分了,因此回表的數據量會變小。
咱們業務中碰到的就是第3種狀況,咱們的業務SQL原本更加複雜,還會join其餘表,可是因爲優化的瓶頸在於創建聯合索引,因此進行了一些簡化,下面是簡化後的SQL:
SELECT a.id as article_id , a.title as title , a.author_id as author_id from article a where a.create_time between '2020-03-29 03:00:00.003' and '2020-04-29 03:00:00.003' and a.status = 1
咱們的需求其實就是從article表中查詢出最近一個月,status爲1的文章,咱們原本就是針對create_time建了單列索引,結果在慢查詢日誌中發現了這條語句,查詢時間須要0.91s左右,因此開始嘗試着進行優化。
爲了便於測試,咱們在表中分別對create_time創建了單列索引create_time,對(create_time,status)創建聯合索引idx_createTime_status。
強制使用idx_createTime進行查詢
SELECT a.id as article_id , a.title as title , a.author_id as author_id from article a FORCE INDEX(idx_createTime) where a.create_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' and a.status = 1
強制使用idx_createTime_status進行查詢(即便不強制也是會選擇這個索引)
SELECT a.id as article_id , a.title as title , a.author_id as author_id from article a FORCE INDEX(idx_createTime_status) where a.create_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' and a.status = 1
優化結果:
優化前使用idx_createTime單列索引,查詢時間爲0.91s
優化前使用idx_createTime_status聯合索引,查詢時間爲0.21s
EXPLAIN的結果以下:
idtypekeykey_lenrowsfilteredExtra1rangeidx_createTime431160825.00Using index condition; Using where2rangeidx_createTime_status6310812100.00Using index condition
四、原理分析
先介紹一下EXPLAIN中Extra列的各類取值的含義
Using filesort
當Query 中包含 ORDER BY 操做,並且沒法利用索引完成排序操做的時候,MySQL Query Optimizer 不得不選擇相應的排序算法來實現。數據較少時從內存排序,不然從磁盤排序。Explain不會顯示的告訴客戶端用哪一種排序。
Using index
僅使用索引樹中的信息從表中檢索列信息,而不須要進行附加搜索來讀取實際行(使用二級覆蓋索引便可獲取數據)。當查詢僅使用做爲單個索引的一部分的列時,可使用此策略。
Using temporary
要解決查詢,MySQL須要建立一個臨時表來保存結果。若是查詢包含不一樣列的GROUP BY和ORDER BY子句,則一般會發生這種狀況。
官方解釋:」爲了解決查詢,MySQL須要建立一個臨時表來容納結果。典型狀況如查詢包含能夠按不一樣狀況列出列的GROUP BY和ORDER BY子句時。很明顯就是經過where條件一次性檢索出來的結果集太大了,內存放不下了,只能經過加臨時表來輔助處理。
Using where
表示當where過濾條件中的字段無索引時,MySQL Sever層接收到存儲引擎(例如innodb)的結果集後,根據where條件中的條件進行過濾。
Using index condition
Using index condition 會先條件過濾索引,過濾完索引後找到全部符合索引條件的數據行,隨後用 WHERE 子句中的其餘條件去過濾這些數據行;
咱們的實際案例中,其實就是走單個索引idx_createTime時,只能從索引中查出 知足a.create_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003'條件的主鍵id,而後進行回表,由於idx_createTime索引中沒有status的信息,只能回表後查出全部的主鍵id對應的行。
而後innodb將結果集返回給MySQL Sever,MySQL Sever根據status字段進行過濾,篩選出status爲1的字段,因此第一個查詢的Explain結果中的Extra纔會顯示Using where。
filtered字段表示存儲引擎返回的數據在server層過濾後,剩下多少知足查詢的記錄數量的比例,這個是預估值,由於status取值是null,1,2,3,4,因此這裏給的25%。
因此第二個查詢與第一個查詢的區別主要在於一開始去idx_createTime_status查到的結果集就是知足status是1的id,因此去彙集索引下進行回表查詢時,掃描的行數會少不少(大概是2.7萬行與15萬行的區別)。
以後innodb返回給MySQL Server的數據就是知足條件status是1的結果集(2.7萬行),不用再進行篩選了,因此第二個查詢纔會快這麼多,時間是優化前的23%。
兩種查詢方式的EXPLAIN預估掃描行數都是30萬行左右是由於idx_createTime_status只命中了createTime,由於createTime不是查單個值,查的是範圍。
//查詢結果行數是15萬行左右 SELECT count(*) from article a where a.post_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' //查詢結果行數是2萬6行左右 SELECT count(*) from article a where a.post_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' and a.audit_status = 1
發散思考:若是將聯合索引(createTime,status)改爲(status,createTime)會怎麼樣?
where a.create_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' and a.status = 1
根據最左匹配的原則,由於咱們的where查詢條件是這樣,若是是(createTime,status)那麼索引就只能用到createTime;
若是是(status,createTime),由於status是查詢單個值,因此status,createTime均可以命中,在(status,createTime)索引中掃描行數會減小;
可是因爲(createTime,status)這個索引自己值包含createTime,status,id三個字段的信息,數據量比較小,而一個數據頁是16k,能夠存儲1000個以上的索引數據節點,並且是查詢到createTime後,進行的順序IO,因此讀取比較快,總得的查詢時間二者基本是一致。
下面是測試結果:
首先建立了(status,createTime)名叫idx_status_createTime,
SELECT a.id as article_id , a.title as title , a.author_id as author_id from article a FORCE INDEX(idx_status_createTime) where a.create_time between '2020-03-22 03:00:00.003' and '2020-04-22 03:00:00.003' and a.status = 1
查詢時間是0.21,跟第二種方式(createTime,status)索引的查詢時間基本一致。
Explain結果對比:
掃描行數確實會少一些,由於在idx_status_createTime的索引中,一開始根據status = 1排除掉了status取值爲其餘值的狀況。
文源網絡,僅供學習之用,若有侵權,聯繫刪除。我將面試題和答案都整理成了PDF文檔,還有一套學習資料,涵蓋Java虛擬機、spring框架、Java線程、數據結構、設計模式等等,但不只限於此。
關注公衆號【java圈子】獲取資料,還有優質文章每日送達。