這篇文章主要是站在老司機的肩膀上從原理總結、mysql慢查詢優化方法、case案例分析等幾個方面結合自己這段時間在工作上遇到的慢查詢談談數據庫索引的原理和如何優化慢查詢。一方面給自己總結,另一方面希望看到的老司機能夠指出其中的錯誤和不足,哈哈哈。
1.上車前(原理回顧)
這部分我主要想總結一下數據庫索引的原理,可能是老生常談的東西了,【慢查詢】這個詞主要的重點就是慢,就像我們開車一樣,我們發車前最重要的就是了解這部車,而我們要知道我們的SQL語句爲什麼會慢,我們當然必須對數據的查找過程有所瞭解。
1.1 磁盤IO
舉個例子。其實對於數據索引這樣的例子,在我們日常生活其實也是很多,通常大家都舉查字典的例子吧?爲了新鮮感,我換一個,比如你找對象,如果你是男的,你最先的目標是女孩子的吧(除個別外),這樣我們就排除了一部無效數據,然後咱們再選咱們同一個城市的吧?又剔除了一部分無效數據,最後我們再選年齡等,最後留下了我們目標人羣。這種查找過程其實也是一種索引過程。
磁盤IO。我們計算機是怎麼查詢數據的呢?當計算機把數據保存在磁盤上,而爲了提高性能,每次又可以把部分數據讀入內存來計算,因爲我們知道訪問磁盤的成本大概是訪問內存的十萬倍左右,所以簡單的搜索樹難以滿足複雜的應用場景。考慮到磁盤IO是非常高昂的操作,計算機操作系統做了一些優化,當一次IO時,不光把當前磁盤地址的數據,而是把相鄰的數據也都讀取到內存緩衝區內,因爲局部預讀性原理告訴我們,當計算機訪問一個地址的數據的時候,與其相鄰的數據也會很快被訪問到。每一次IO讀取的數據我們稱之爲一頁(page)。具體一頁有多大數據跟操作系統有關,一般爲4k或8k,也就是我們讀取一頁內的數據時候,實際上才發生了一次IO,所以結合我們的例子以及計算機查詢數據的原理,爲了提高查詢數據的查詢速度,需要保證最小的IO次數,B+樹的數據結構應運而生。
1.2 索引結構
如上圖,是一顆b+樹,關於b+樹的定義可以參見B+樹,這裏只說一些重點,淺藍色的塊我們稱之爲一個磁盤塊,可以看到每個磁盤塊包含幾個數據項(深藍色所示)和指針(黃色所示),如磁盤塊1包含數據項17和35,包含指針P1、P2、P3,P1表示小於17的磁盤塊,P2表示在17和35之間的磁盤塊,P3表示大於35的磁盤塊。真實的數據存在於葉子節點即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非葉子節點只不存儲真實的數據,只存儲指引搜索方向的數據項,如17、35並不真實存在於數據表中。 如果要查找數據項29,那麼首先會把磁盤塊1由磁盤加載到內存,此時發生一次IO,在內存中用二分查找確定29在17和35之間,鎖定磁盤塊1的P2指針,內存時間因爲非常短(相比磁盤的IO)可以忽略不計,通過磁盤塊1的P2指針的磁盤地址把磁盤塊3由磁盤加載到內存,發生第二次IO,29在26和30之間,鎖定磁盤塊3的P2指針,通過指針加載磁盤塊8到內存,發生第三次IO,同時內存中做二分查找找到29,結束查詢,總計三次IO。真實的情況是,3層的b+樹可以表示上百萬的數據,如果上百萬的數據查找只需要三次IO,性能提高將是巨大的,如果沒有索引,每個數據項都要發生一次IO,那麼總共需要百萬次的IO,顯然成本非常非常高。 1.通過上面的分析,我們知道IO次數取決於b+數的高度h,假設當前數據表的數據爲N,每個磁盤塊的數據項的數量是m,則有h=㏒(m+1)N,當數據量N一定的情況下,m越大,h越小;而m = 磁盤塊的大小 / 數據項的大小,磁盤塊的大小也就是一個數據頁的大小,是固定的,如果數據項佔的空間越小,數據項的數量越多,樹的高度越低。這就是爲什麼每個數據項,即索引字段要儘量的小,比如int佔4字節,要比bigint8字節少一半。這也是爲什麼b+樹要求把真實的數據放到葉子節點而不是內層節點,一旦放到內層節點,磁盤塊的數據項會大幅度下降,導致樹增高。當數據項等於1時將會退化成線性表。 2.當b+樹的數據項是複合的數據結構,比如(name,age,sex)的時候,b+數是按照從左到右的順序來建立搜索樹的,比如當(張三,20,F)這樣的數據來檢索的時候,b+樹會優先比較name來確定下一步的所搜方向,如果name相同再依次比較age和sex,最後得到檢索的數據;但當(20,F)這樣的沒有name的數據來的時候,b+樹就不知道下一步該查哪個節點,因爲建立搜索樹的時候name就是第一個比較因子,必須要先根據name來搜索才能知道下一步去哪裏查詢。比如當(張三,F)這樣的數據來檢索時,b+樹可以用name來指定搜索方向,但下一個字段age的缺失,所以只能把名字等於張三的數據都找到,然後再匹配性別是F的數據了, 這個是非常重要的性質,即索引的最左匹配特性。
2. 上車(慢查詢優化)
從上面的原理我們可以知道,我們其實要做的就是讓數據庫在查找數據時,儘可能地選擇最短的路徑查找到想要的數據,儘可能地減少磁盤IO次數。
2.1索引的一些概念
索引概念(重要)
排好序的快速查找的數據結構(我們平時說的索引,如果沒有特別指明,都是指B樹,其中聚集索引、次要索引、覆蓋索引、複合索引、前綴索引、唯一索引默認使用的都是B+樹索引,除B+樹這種類型的索引外還有哈希索引等)
優缺點---->何種情況建索引
1、優點
查找 :提高數據檢索效率,降低IO成本。
排序:通過索引對數據進行排序,降低排序成本,降低cpu消耗
2、缺點
實際上索引也是一張表,該表保存了主鍵與索引字段,並指向索引的記錄,所以索引列也需要佔空間。
更新表時(insert、update、delete)不僅要保存數據還要更新保存索引文件新添加的索引列。
索引分類
1、單值索引(單列索引):一個索引只包含單個列,一個表中可以有多個單列索引。
2、唯一索引:索引列必須唯一,但可以允許有空值
3、複合索引:一個索引包含多個列
mysql索引結構
1、BTree索引
2、Hash索引
3、full-text全文檢索
4、R-Tree索引
2.2 適合創建索引和不適合創建索引的情況
哪些情況要建索引
1、主鍵自動建主鍵索引
2、頻繁作爲查詢條件的字段應該創建索引
3、查詢中與其他表關聯的字段,外鍵關係建立索引
4、在高併發下傾向建立組合索引
5、查詢中的排序字段,排序字段若通過索引去訪問將大大提高排序速度
6、查詢中統計或者分組的數據
哪些情況不適合建索引
1、頻繁更新的字段
2、where條件用不到的字段不創建索引
3、表記錄太少
4、經常增刪改的表
5、數據重複太多的字段,爲它建索引意義不大(假如一個表有10萬,有一個字段只有T和F兩種值,每個值的分佈概率大約只有50%,那麼對這個字段的建索引一般不會提高查詢效率,索引的選擇性是指索引列的不同值數據與表中索引記錄的比,,如果,一個表中有2000條記錄,表中索引列的不同值記錄有1980個,這個索引的選擇性爲1980/2000=0.99,如果索引項越接近1,這個索引效率越高)
3. 翻車(explain字段分析)
!
- id:表示select子句或者操作的順序
1、id相同:執行順序自上而下
2、id不同:id值越大優先級越高,越先被執行
3、id相同不同:id越大越先執行,相同的自上而下執行
- select_type:主要是區分普通查詢、聯合查詢、子查詢等。
- SIMPLE:簡單的select查詢,不包含子查詢與union
- PRIMARY:查詢中包含複雜的子部分,最外層會被標記爲primary
- SUBQUERY:在select或者where列表中包含了子查詢
- DERIVED:在from列表中包含的子查詢衍生表
- UNION:若第二個select出現在union之後,則被標記爲union
- UNION RESESULT:從union表獲取結果的select
-
table:這一行數據是哪個表的數據
-
partitions: 所在的分區
-
type:查詢中使用了何種類型(優化程度參考)
結果值從最好到最壞:system>const>eq_ref>ref>fulltext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>all
一般來說,得保證查詢至少達到range級別,最好能到達ref
system:表只有一行記錄(等於系統表),這是const類型的特例,平時不會出現
const:表示通過索引一次就能夠找到
eq_ref:唯一性索引掃描,對於每個索引鍵,表示只有一條記錄與之匹配,常見於主鍵或唯一索引掃描
ref:非唯一性索引掃描,返回匹配某個單獨值的所有行
range:只檢索給定範圍的行,使用一個索引來選擇行,一般就是在where語句中出現了between、<、>、in等的查詢
index:index比all快,因爲index是從索引中讀取,all是從硬盤中讀取
all:遍歷全表才能找到
-
possible_key:顯示可能應用在這張表中的索引,但實際上不一定用到
-
key:實際上使用的索引,如果沒有則爲null
-
key_len:表示索引中使用的字節數(可能使用的,不是實際的),可通過該列查詢中使用的索引的長度,在不損失精確性的情況下,長度越短越好
-
ref:顯示索引的哪一列被用到,如果可能的話是一個常數,哪些常量被用於查找索引列上的值
-
rows:大致估算找出所需的記錄要讀取的行數
-
filtered: 表示此查詢條件所過濾的數據的百分比
-
Extra:包含不適合在其他列中顯示,但十分重要的的額外信息
1、Using filesort 說明mysql會對數據使用一個外部的索引排序,而不是按照表內的索引順序進行讀取,mysql中無法利用索引完成的排序成爲“文件排序”
2、Using temporary 使了用臨時表保存中間結果,mysql在對查詢結果排序時使用了臨時表,常見於排序order by 和分組查詢group by
3、Using index 表示相應的select操作中使用了覆蓋索引,避免訪問了表的數據行,效率高,如果同時出現了using where 表明索引被用來執行索引鍵值查找,如果沒有出現 using where 表明索引用來讀取而非執行查詢動作。
4、Using where 表明使用了where進行過濾
5、Using join buffer 使用了連接緩存
6、impossible where where子句的值總是false,不能用來獲取任何元組
7、select table optimized away 在沒有group by子句的情況下,基於索引優化min/max操作或者對於myisam存儲引擎優化count(*)操作,不必等到執行階段再進行計算
8、distinct:在優化distinct操作,在找到第一匹配的元組後即停止找到同樣值的動作
3.1 索引失效_複合索引(避免)
1、應該儘量全值匹配
2、複合最佳左前綴法則(第一個索引不能掉,中間不能斷開)
3、不在索引列上做任何操作(計算、函數、類型轉換)會導致索引失效而轉向全表掃描
4、儲存引擎不能使用索引中範圍條件右邊的列
5、儘量使用覆蓋索引(只訪問索引的查詢(索引列和查詢列一致)),減少select*
6、mysql在使用不等於(!=或者<>)的時候無法使用索引會導致全表掃描
7、is null,is not null也會無法使用索引
8、like以統配符開頭
9、字符串不加單引號
10、少用or
3.2 order by優化
1、避免filesort
2、儘量在索引上進行排序,遵照最佳左前綴原則
3、filesort有兩種排序:
雙路排序:兩次磁盤掃描
單路排序:一次性讀取保存在內存中,沒拉完的數據再次拉
單路排序是後出的,總體好於雙路排序
優化策略: - 1、增大sort_buffer_size參數的設置 - 2、增大max_length_for_sort_data參數的設置
原因:儘可能一次拿到內存
4.車禍現場(case分析)
4.1 join慢查詢
4.1.1 CaseStudy-20140929-慢查詢壓垮DB502
2014.09.29 17:01 爲了確認剛剛上線的效果,李某通過命令行連上db502執行了兩條慢查詢SQL,發現sql寫錯後執行中斷操作,但是客戶端與數據庫突然斷開連接,李某並未注意到。由於用戶JAVA服務暫時沒有數據庫重連機制,影響到約三分之一的需要獲取用戶信息的JAVA服務接口。
李某執行的sql語句如下:
代碼塊
select * from useracct join userinfo order by useracct.id desc limit 11;
select * from useracct join userinfo where useracct.id > 116277616 order by useracct.id desc limit 11;
對第一條sql進行explain可以發現,因爲忘寫了join的on條件,這是掃全表sql,如下圖:
我們首先看type級別兩個表的級別都是ALL,說明該條語句沒有用到索引,做了全表掃描是最差的情況,
4.2 order by慢查詢
4.2.1 CaseStudy-20150605
該事故中的慢查詢語句:
代碼塊 SELECT * FROM coupon_userid_61
FORCE INDEX (orderid
) WHERE userid
= 91241561 AND status
IN (0,144) ORDER BY id
ASC ; 該SQL由於強制指定了使用orderid索引,但條件中並沒有orderid,導致產生全表掃描(type: ALL);如下爲問題SQL和正確SQL的執行計劃:
mysql> explain SELECT * FROM
coupon_userid_61 FORCE INDEX (
orderid) WHERE
userid= 91241561 AND
statusIN (0,144) ORDER BY
id ASC ;
mysql> explain SELECT * FROM
coupon_userid_61 FORCE INDEX (
orderid) WHERE
userid= 91241561 AND
statusIN (0,144) and orderid=1 ORDER BY
idASC ;
直接原因是最終傳給SQL查詢函數的參數,orderid沒有加入where子句,但forceindex一直生效
4.3 出現using filesort
通過執行計劃可以看出,用到了主鍵索引以及SpuType索引,該語句先使用了sputype索引,而該字段數據大部分數據都一樣,導致出現了using filesort,解決這個問題有兩種解決方案:
1、可以強制使用主鍵索引
2、強制不使用sputype索引