當咱們談 SQL 優化時在談些什麼?

做者 |孫銀行
編輯 | 顧鄉mysql

背景

Mysql數據庫做爲數據持久化的存儲系統,在實際業務中應用普遍。在應用也常常會由於SQL遇到各類各樣的瓶頸。最經常使用的Mysql引擎是innodb,索引類型是B-Tree索引,增刪改查等操做最常常遇到的問題是「查」,查詢又以索引爲重點(沒索引不是病,慢起來太要命)。踩過O2O優惠券、搖一搖周邊兩個業務的一些坑,當談到SQL優化時,想分享下innodb下B-Tree索引的一些理解與實踐。sql

接下來的內容,安排以下:數據庫

  1. 介紹索引的工做原理;
  2. 引用實例具體介紹索引;
  3. 如何使用explain排查線上問題;
  4. 實際碰到的問題彙總;

索引如何工做

當查詢時,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查詢?優化

一、索引的左前綴規則;索引中的列由左向右逐一匹配,若是中間某一列不能使用索引則後序列不在查詢中再也不被使用。ui

例如,若是有一個3列索引(str_col1,col2,col3),其中str_col1爲字符串,則對(str_col1)、(str_col1,col2)和(str_col1,col2,col3)上的查詢進行了索引。
若是列不構成索引最左面的前綴,MySQL不能使用索引。假定有下面顯示的SELECT語句。spa

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)的最左邊的前綴。code

二、where語句中列的表達式爲=、>、>=、<、<=、BETWEEN、ISNULL或者LIKE ’pattern’(其中’pattern’不以通配符開始)

三、每一個AND組做爲表達式匹配索引。

SELECT * FROM tbl_name WHERE (str_col1=val1 OR col4 =val4) AND col2=val2;

由於str_col1=val1 OR col4 =val4做爲一組,col4不匹配索引中的列,因此查詢不匹配索引。

四、若是表達式中存在類型轉換或者列上有複雜函數則與該列不匹配索引中的列。

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是表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

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)
字符集是否一致:涉及表關聯時,兩表字符集是否一致。

相關推薦

一條SQL搞定卡方檢驗計算
一條SQL搞定信息增益的計算
MySQL內核深度優化

相關文章
相關標籤/搜索