歡迎大家關注騰訊雲技術社區-博客園官方主頁,我們將持續在博客園為大家推薦技術精品文章哦~
作者:孫銀行
背景
Mysql數據庫作為數據持久化的存儲系統,在實際業務中應用廣泛。在應用也經常會因為SQL遇到各種各樣的瓶頸。最常用的Mysql引擎是innodb,索引類型是B-Tree索引,增刪改查等操作最經常遇到的問題是“查”,查詢又以索引為重點(沒索引不是病,慢起來太要命)。踩過O2O優惠券、搖一搖周邊兩個業務的一些坑,當談到SQL優化時,想分享下innodb下B-Tree索引的一些理解與實踐。
接下來的內容,安排如下:
- 介紹索引的工作原理;
- 引用實例具體介紹索引;
- 如何使用explain排查線上問題;
- 實際碰到的問題匯總;
索引如何工作
當查詢時,Mysql的查詢優化器會使用統計數據預估使用各個索引的代價(COST),與不使用索引的代價(COST)比較。Mysql會選擇代價最低的方式執行查詢。Mysql如何使用索引,可以用下面的偽代碼來說明:
min_cost = INIT_VALUE
min_cost_index = NONE
for(index in all_indexs): if (index match WHERE_CLAUSE): cur_cost = COST(index) if(cur_cost < min_cost): min_cost = cur_cost min_cost_index = index
INIT_VALUE:不使用索引時的代價
all_indexs:查詢表上所有的索引COST:基本是由“估計需要掃描的行數”(rows)來確定
WHERE_CLAUSE:查詢SQL中的WHERE子句
大致的意思:Mysql會遍歷該查詢相關的表(table)的每一條索引,然后判斷該索引能否被本次查詢使用(possible_keys)。當索引可以使用時,Mysql預估使用該索引進行查詢的cost,然后選擇預估代價最低的代價的方式(key)執行查詢。
索引匹配(match)
怎樣判斷索引是否匹配(match)SQL查詢?
1、索引的左前綴規則;索引中的列由左向右逐一匹配,如果中間某一列不能使用索引則后序列不在查詢中不再被使用。
例如,如果有一個3列索引(str_col1,col2,col3),其中str_col1為字符串,則對(str_col1)、(str_col1,col2)和(str_col1,col2,col3)上的查詢進行了索引。
如果列不構成索引最左面的前綴,MySQL不能使用索引。假定有下面顯示的SELECT語句。
SELECT * FROM tbl_name WHERE str_col1=val1; SELECT * FROM tbl_name WHERE str_col1=val1 AND col2=val2; SELECT * FROM tbl_name WHERE col2=val2; SELECT * FROM tbl_name WHERE col2=val2 AND col3=val3;
如果 (str_col1,col2,col3)有一個索引,只有前2個查詢使用索引。第3個和第4個查詢確實包括索引的列,但(col2)和(col2,col3)不是 (col1,col2,col3)的最左邊的前綴。
2、where語句中列的表達式為=、>、>=、<、<=、BETWEEN、ISNULL或者LIKE ’pattern’(其中’pattern’不以通配符開始)
3、每個AND組作為表達式匹配索引。
SELECT * FROM tbl_name WHERE (str_col1=val1 OR col4 =val4) AND col2=val2;
因為str_col1=val1 OR col4 =val4作為一組,col4不匹配索引中的列,所以查詢不匹配索引。
4、如果表達式中存在類型轉換或者列上有復雜函數則與該列不匹配索引中的列。
SELECT * FROM tbl_name WHERE str_col1=1; SELECT * FROM tbl_name WHERE SUBSTRING(str_col1,1,8) = ‘title’;
第1個查詢,因為1是整數、str_col1是字符串,所以不匹配索引;第2個查詢str_col1有復雜函數,同樣不匹配索引。
索引的COST
Mysql如何計算索引的COST?
索引的cost基本是由“估計需要掃描的行數”(rows)來確定。數據來源於information_schema,在Mysql啟動的時候讀入內存,運行時只使用內存值,存儲引擎會動態更新這些值。
我們可以通過explain看下“估計需要掃描的函數”,可以通過optimizer_trace查詢適用每一條SQL的具體的cost值。explain也是線上排查問題的利器,后面會重點介紹。
索引實例分析
索引的字段究竟是怎么從where語句中提取,並被Mysql使用呢,下面將以一個實例分析這個過程。內容全文為摘取何登成的文章《SQL中的where條件,在數據庫中提取與應用淺析》,並做了部分刪改。
我們創建一張測試表,一個索引索引,然后插入幾條記錄。(注意:下面的實例,使用的表的結構不是InnoDB引擎所采用的聚簇索引表。圖例僅為說明,原理適用innodb)
create table t1 (a int primary key, b int, c int, d int, e varchar(20)); create index idx_t1_bcd on t1(b, c, d); insert into t1 values (4,3,1,1,’d’); insert into t1 values (1,1,1,1,’a’); insert into t1 values (8,8,8,8,’h’): insert into t1 values (2,2,2,2,’b’); insert into t1 values (5,2,3,5,’e’); insert into t1 values (3,3,2,2,’c’); insert into t1 values (7,4,5,5,’g’); insert into t1 values (6,6,4,4,’f’);
t1表的存儲結構如下圖所示(只畫出了idx_t1_bcd索引與t1表結構,沒有包括t1表的主鍵索引):
簡單說明上圖,idx_t1_bcd索引上有[b,c,d]三個字段,不包括[a,e]字段。idx_t1_bcd索引,首先按照b字段排序,b字段相同,則按照c字段排序,以此類推。
考慮以下SQL:
select * from t1 where b >= 2 and b < 8 and c > 1 and d != 4 and e != ‘a’;
可以發現where條件使用到了[b,c,d,e]四個字段,而t1表的idx_t1_bcd索引,恰好使用了[b,c,d]這三個字段,那么走idx_t1_bcd索引進行條件過濾,應該是一個不錯的選擇。
所有SQL的where條件,均可歸納為3大類:Index Key (First Key & Last Key),Index Filter,Table Filter。
接下來,讓我們來詳細分析者3大類分別是如何定義,以及如何提取的。
l Index Key
用於確定SQL查詢在索引中的連續范圍(起始范圍+結束范圍)的查詢條件,被稱之為Index Key。由於一個范圍,至少包含一個起始與一個終止,Index Key也被拆分為Index First Key和Index Last Key,分別用於定位索引查找的起始,以及索引查詢的終止條件。
Index First Key
提取規則:從索引的第一個鍵值開始,檢查其在where條件中是否存在,若存在並且條件是=、>=,則將對應的條件加入Index First Key之中,繼續讀取索引的下一個鍵值,使用同樣的提取規則;若存在並且條件是>,則將對應的條件加入Index First Key中,同時終止Index First Key的提取;若不存在,同樣終止Index First Key的提取。
針對上面的SQL,應用這個提取規則,提取出來的Index First Key為(b >= 2, c > 1)。由於c的條件為 >,提取結束,不包括d。
Index Last Key
提取規則:從索引的第一個鍵值開始,檢查其在where條件中是否存在,若存在並且條件是=、<=,則將對應條件加入到Index Last Key中,繼續提取索引的下一個鍵值,使用同樣的提取規則;若存在並且條件是 < ,則將條件加入到Index Last Key中,同時終止提取;若不存在,同樣終止Index Last Key的提取。
針對上面的SQL,應用這個提取規則,提取出來的Index Last Key為(b < 8),由於是 < 符號,因此提取b之后結束。
2 Index Filter
在完成Index Key的提取之后,我們根據where條件固定了索引的查詢范圍,但是此范圍中的項,並不都是滿足查詢條件的項。在上面的SQL用例中,(3,1,1),(6,4,4)均屬於范圍中,但是又均不滿足SQL的查詢條件。
Index Filter的提取規則:同樣從索引列的第一列開始,檢查其在where條件中是否存在:若存在並且where條件僅為 =,則跳過第一列繼續檢查索引下一列,下一索引列采取與索引第一列同樣的提取規則;若where條件為 >=、>、<、<= 其中的幾種,則跳過索引第一列,將其余where條件中索引相關列全部加入到Index Filter之中;若索引第一列的where條件包含 =、>=、>、<、<= 之外的條件,則將此條件以及其余where條件中索引相關列全部加入到Index Filter之中;若第一列不包含查詢條件,則將所有索引相關條件均加入到Index Filter之中。
針對上面的用例SQL,索引第一列只包含 >=、< 兩個條件,因此第一列可跳過,將余下的c、d兩列加入到Index Filter中。因此獲得的Index Filter為 c > 1 and d != 4 。
3 Table Filter
Table Filter是最簡單,最易懂,也是提取最為方便的。提取規則:所有不屬於索引列的查詢條件,均歸為Table Filter之中。
同樣,針對上面的用例SQL,Table Filter就為 e != ‘a’。
根據以上實例其實可以總結出一些規律,WHERE語句究竟怎樣(是否)匹配索引,不用迷信出自他人之口的規則。只需要簡單的按照索引自左向右的每一列,從WHERE語句提取條件,能否從索引樹的根節點出發,到達索引樹的葉節點,成功匹配出一個或幾個范圍區間,即能自己自行判斷是否能使用索引。反過來,最左前綴匹配、Like不能以通配符開始、AND分組,也都是由B-Tree本身特性決定的。
索引問題排查
前面我們談使用索引的cost的值提到過explain。下面介紹explain的值,並以一個實際遇到的問題說明如何排查問題。
Explain詳解
使用一個示例SQL來解釋explain:
select id from r_ibeacon_biz_device_d where ftime >= 20151126 and ftime <= 20151126 and biz_id = 11602 limit 50;
IDX_BID_FTIME<biz_id, ftime="">是表r_ibeacon_biz_device_d的其中一條索引。
Biz_id,ftime均為bigint類型。
我們着重關注幾個重點字段的重點值:
- type:索引的使用方式
eq_ref … 索引,關聯匹配若干行
ref … 索引(前綴)匹配 range … 索引范圍掃(BETWEEN、IN、>=、LIKE)得到數據 index … 索引全掃描 all … 表全掃描
示例中使用的索引是使用全索引范圍掃描,所以type為range
- possible_keys:適用查詢的索引列表。示例中有三條索引適用本次查詢。
- key: 查詢實際執行使用的索引。示例使用的為IDX_BID_FTIME
- key_len:查詢使用索引的長度。
null 1字節 tinyint 1字節 int 4字節 bigint 8字節 double 8字節 datetime 8字節 timestamp 4字節 varchr(10)變長字段且允許NULL: 10*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)+2(變長字段) char(10)固定字段且允許NULL: 10*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)
以上是常用類型的長度,示例中key_len為18,即:8字節(biz_id bigint)+1字節(biz_id允許為null)+8字節(ftimebigint)+1字節(ftime允許為null)。所以本次查詢是使用了索引的所有字段加速查詢
- rows:查詢預估掃描的行數
Explain跟進問題
搖一搖周邊后台的數據統計接口爾會有小尖峰,涉及了一條SQL:
一條SQL搞定卡方檢驗計算select d.id from r_ibeacon_biz_page_d d where d.ftime >= 20151126 and d.ftime <= 20151126 and d.biz_id = 11023 and d.page_id = 778495 limit 0,20;
表r_ibeacon_biz_page_d 的主要字段信息如下:
ftime bigint(20) biz_id bigint(20) page_id varchar(200)
索引為:IDX_BID_PID_FTIME<biz_id,page_id,ftime>
Explain結果如下
觀察以上explain結果可以看到一切正常,SQL“符合預期”的走了索引。但是rows稍微多了點,但是看起來也“好像”ok。但是問題就是出現尖峰。
問題排查:
首先,注意到的一點就是explain中的type異常,是ref。按照上面的解釋,如果走了索引那應該是range類型才對啊。
其次,觀察key_len,9,發現確實有些不對,怎么會這么小。按照類型所占字節,9剛好為biz_id的長度,確定這條SQL雖然走了索引,但是只使用了biz_id字段。原因呢?
然后執行“desc r_ibeacon_biz_page_d”,查看表結構的索引字段,突然發現page_id的類型怎么是varchar,再看SQL中page_id=11023。突然意識到了什么,此時剛好違反索引匹配的第四條規則。更改SQL“page_id=11023”為“page_id=‘11023’”驗證,如下
可以看到type=range、key_len=621,符合預期。接下來要做的就是更改表中page_id的類型為bigint。隔天再看接口的尖峰果然削平。
Explain是一個很好的工具,可以用來驗證SQL是否使用了索引,更重要的是驗證SQL是否如預期的使用索引上。排查線上問題還有profile和optimizer_trace,由於實際沒有太多用到暫且不表。
常見問題匯總
- Range怎么使用索引?
詳見上文
- Order by使用索引嗎?
該問題可以由以下資料解釋:
SQL queries with an order by clause don’t need to sort the result explicitly if the relevant index already delivers the rows in the required order. That means the same index that is used for the where clause must also cover the order by clause.
總之一句話:索引本身並不能避免排序,當根據索引取出的數據已經滿足order by子句的要求就可以避免排序操作。
- order by太慢?
避免數據排序,采用索引排序(分頁查詢文藝寫法)
- limit offset太慢?
避免大offset,使用where語句過濾更多的行。更多參考的實踐《Efficient Pagination Using MySQL》
- 為什么不走索引(索引也走了,還是慢)?
類型是否一致: int vs char(varchar)、varchar(32)vs varchar(64)
字符集是否一致:涉及表關聯時,兩表字符集是否一致。
相關推薦
MySQL索引及查詢優化總結
埋在MYSQL數據庫應用中的17個關鍵問題!
騰訊雲采購節服務器三折起,這才是人民的雲計算!
此文已由作者授權騰訊雲技術社區發布,轉載請注明文章出處
原文鏈接:https://www.qcloud.com/community/article/302356
獲取更多騰訊海量技術實踐干貨,歡迎大家前往騰訊雲技術社區