定義:索引是存儲引擎用於快速找到記錄的一種數據結構。舉例說明:若是查找一本書中的某個特定主題,通常會先看書的目錄(相似索引),找到對應頁面。在MySQL,存儲引擎採用相似的方法使用索引,高效獲取查找的數據。html
索引的分類node
1)從存儲結構上來劃分mysql
2)從應用層次上來劃分sql
3)從表記錄的排列順序和索引的排列順序是否一致來劃分數據庫
數據庫保存的數據是存儲在磁盤上,查找數據時須要將磁盤中的數據加載到內存中,在介紹索引的實現以前,先了解下磁盤IO與預讀。bash
磁盤讀取數據靠的是機械運動,每次讀取數據花費的時間能夠分爲尋道時間、旋轉延遲、傳輸時間三個部分,尋道時間指的是磁臂移動到指定磁道所須要的時間,主流磁盤通常在5ms如下;旋轉延遲就是咱們常常據說的磁盤轉速,好比一個磁盤7200轉,表示每分鐘能轉7200次,也就是說1秒鐘能轉120次,旋轉延遲就是1/120/2 = 4. 17ms;傳輸時間指的是從磁盤讀出或將數據寫入磁盤的時間,通常在零點幾毫秒,相對於前兩個時間能夠忽略不計。那麼訪問一次磁盤的時間,即一次磁盤IO的時間約等於5+4. 17 = 9ms左右,聽起來還挺不錯的,但要知道一臺500 -MIPS的機器每秒能夠執行5億條指令,由於指令依靠的是電的性質,換句話說執行一次IO的時間能夠執行40萬條指令,數據庫動輒十萬百萬乃至千萬級數據,每次9毫秒的時間,顯然是個災難。數據結構
下圖是計算機硬件延遲的對比圖,供你們參考:ide
考慮到磁盤IO是很是高昂的操做,計算機操做系統作了一些優化,當一次IO時,不光把當前磁盤地址的數據,而是把相鄰的數據也都讀取到內存緩衝區內,由於局部預讀性原理告訴咱們,當計算機訪問一個地址的數據的時候,與其相鄰的數據也會很快被訪問到。每一次IO讀取的數據咱們稱之爲一頁(page)。具體一頁有多大數據跟操做系統有關,通常爲4k或8k,也就是咱們讀取一頁內的數據時候,實際上才發生了一次IO,這個理論對於索引的數據結構設計很是有幫助。函數
B-Tree是爲磁盤等外存儲設備設計的一種平衡查找樹。oop
B-Tree結構的數據可讓系統高效的找到數據所在的磁盤塊。爲了描述B-Tree,首先定義一條記錄爲一個二元組[key, data] ,key爲記錄的鍵值,對應表中的主鍵值,data爲一行記錄中除主鍵外的數據。對於不一樣的記錄,key值互不相同。
一棵m階的B-Tree有以下特性:
B-Tree中的每一個節點根據實際狀況能夠包含大量的關鍵字信息和分支,以下圖所示爲一個3階的B-Tree:
每一個節點佔用一個盤塊的磁盤空間,一個節點上有兩個升序排序的關鍵字和三個指向子樹根節點的指針,指針存儲的是子節點所在磁盤塊的地址。兩個關鍵詞劃分紅的三個範圍域對應三個指針指向的子樹的數據的範圍域。以根節點爲例,關鍵字爲17和35,P1指針指向的子樹的數據範圍爲小於17,P2指針指向的子樹的數據範圍爲17~35,P3指針指向的子樹的數據範圍爲大於35。
模擬查找關鍵字29的過程:
根據根節點找到磁盤塊1,讀入內存。【磁盤I/O操做第1次】
比較關鍵字29在區間(17, 35),找到磁盤塊1的指針P2。
根據P2指針找到磁盤塊3,讀入內存。【磁盤I/O操做第2次】
比較關鍵字29在區間(26, 30),找到磁盤塊3的指針P2。
根據P2指針找到磁盤塊8,讀入內存。【磁盤I/O操做第3次】
在磁盤塊8中的關鍵字列表中找到關鍵字29。
分析上面過程,發現須要3次磁盤I/O操做,和3次內存查找操做。因爲內存中的關鍵字是一個有序表結構,能夠利用二分法查找提升效率。而3次磁盤I/O操做是影響整個B-Tree查找效率的決定因素。B-Tree相對於AVLTree縮減了節點個數,使每次磁盤I/O取到內存的數據都發揮了做用,從而提升了查詢效率。
B+Tree是在B-Tree基礎上的一種優化,InnoDB存儲引擎就是用B+Tree實現其索引結構。
在B+Tree中,全部數據記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只存儲key值信息,這樣能夠大大加大每一個節點存儲的key值數量,下降B+Tree的高度。
因爲B+Tree的非葉子節點只存儲鍵值信息,假設每一個磁盤塊能存儲4個鍵值及指針信息,則變成B+Tree後其結構以下圖所示:
最左前綴匹配原則,很是重要的原則,mysql會一直向右匹配直到遇到範圍查詢(>、<、between、like)就中止匹配,好比a = 1 and b = 2 and c > 3 and d = 4 若是創建(a,b,c,d)順序的索引,d是用不到索引的,若是創建(a,b,d,c)的索引則均可以用到,a,b,d的順序能夠任意調整。
=和in能夠亂序,好比a = 1 and b = 2 and c = 3 創建(a,b,c)索引能夠任意順序,mysql的查詢優化器會幫你優化成索引能夠識別的形式。
儘可能選擇區分度高的列做爲索引,區分度的公式是count(distinct col)/count(*),表示字段不重複的比例,比例越大咱們掃描的記錄數越少,惟一鍵的區分度是1,而一些狀態、性別字段可能在大數據面前區分度就是0,那可能有人會問,這個比例有什麼經驗值嗎?使用場景不一樣,這個值也很難肯定,通常須要join的字段咱們都要求是0.1以上,即平均1條掃描10條記錄。
索引列不能參與計算,保持列「乾淨」,好比from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,緣由很簡單,b+樹中存的都是數據表中的字段值,但進行檢索時,須要把全部元素都應用函數才能比較,顯然成本太大。因此語句應該寫成create_time = unix_timestamp(’2014-05-29’)。
儘可能的擴展索引,不要新建索引。好比表中已經有a的索引,如今要加(a,b)的索引,那麼只須要修改原來的索引便可。
explain爲mysql提供語句的執行計劃信息。能夠應用在select、delete、insert、update和place語句上。explain的執行計劃,只是做爲語句執行過程的一個參考,實際執行的過程不必定和計劃徹底一致,可是執行計劃中透露出的信息卻能夠幫助選擇更好的索引和寫出更優化的查詢語句。
explain輸出項
Column | JSON Name | Meaning |
---|---|---|
id | select_id | The SELECT identifier |
select_type | None | The SELECT type |
table | table_name | The table for the output row |
partitions | partitions | The matching partitions |
type | access_type | The join type |
possible_keys | possible_keys | The possible indexes to choose |
key | key | The index actually chosen |
key_len | key_length | The length of the chosen key |
ref | ref | The columns compared to the index |
rows | rows | Estimate of rows to be examined |
filtered | filtered | Percentage of rows filtered by table condition |
Extra | None | Additional information |
id列的編號是 select 的序列號,有幾個 select 就有幾個id,而且id的順序是按 select 出現的順序增加的。
MySQL將 select 查詢分爲簡單查詢(SIMPLE)和複雜查詢(PRIMARY)。複雜查詢分爲三類:簡單子查詢、派生表(from語句中的子查詢)、union 查詢。
id列越大執行優先級越高,id相同則從上往下執行,id爲NULL最後執行。
select_type 表示對應行是簡單仍是複雜的查詢。
這一列表示 explain 的一行正在訪問哪一個表。
當 from 子句中有子查詢時,table列是 格式,表示當前查詢依賴 id=N 的查詢,因而先執行 id=N 的查詢。
當有 union 時,UNION RESULT 的 table 列的值爲<union1, 2>,1和2表示參與 union 的 select 行id。
這一列表示關聯類型或訪問類型,即MySQL決定如何查找表中的行,查找數據行記錄的大概範圍。
依次從最優到最差分別爲:system > const > eq_ref > ref > range > index > ALL
這一列顯示查詢可能使用哪些索引來查找。
explain 時可能出現 possible_keys 有列,而 key 顯示 NULL 的狀況,這種狀況是由於表中數據很少,mysql認爲索引對此查詢幫助不大,選擇了全表查詢。
若是該列是NULL,則沒有相關的索引。在這種狀況下,能夠經過檢查 where 子句看是否能夠創造一個適當的索引來提升查詢性能,而後用 explain 查看效果。
這一列顯示mysql實際採用哪一個索引來優化對該表的訪問。
若是沒有使用索引,則該列是 NULL。若是想強制mysql使用或忽視possible_keys列中的索引,在查詢中使用 force index、ignore index。
這一列顯示了mysql在索引裏使用的字節數,經過這個值能夠算出具體使用了索引中的哪些列。
這一列顯示了在key列記錄的索引中,表查找值所用到的列或常量,常見的有:const(常量),字段名(例:film. id)
這一列是mysql估計要讀取並檢測的行數,注意這個不是結果集裏的行數。
Using index:查詢的列被索引覆蓋,而且where篩選條件是索引的前導列(最左側索引),是性能高的表現。通常是使用了覆蓋索引(索引包含了全部查詢的字段)。對於innodb來講,若是是輔助索引性能會有很多提升。
Using where:查詢的列未被索引覆蓋,where篩選條件非索引的前導列。
Using where Using index:查詢的列被索引覆蓋,而且where篩選條件是索引列之一但不是索引的前導列,意味着沒法直接經過索引查找來查詢到符合條件的數據, Using index表明select用到了覆蓋索引。
NULL:查詢的列未被索引覆蓋,而且where篩選條件是索引的前導列,意味着用到了索引,可是部分字段未被索引覆蓋,必須經過「回表」來實現,不是純粹地用到了索引,也不是徹底沒用到索引。
Using index condition:與Using where相似,查詢的列不徹底被索引覆蓋,where條件中是一個前導列的範圍;
Using temporary:mysql須要建立一張臨時表來處理查詢。出現這種狀況通常是要進行優化的,首先是想到用索引來優化。
Using filesort:mysql 會對結果使用一個外部索引排序,而不是按索引次序從表裏讀取行。此時mysql會根據聯接類型瀏覽全部符合條件的記錄,並保存排序關鍵字和行指針,而後排序關鍵字並按順序檢索行信息。這種狀況下通常也是要考慮使用索引來優化的。
不少狀況下,咱們寫SQL只是爲了實現功能,這只是第一步,不一樣的語句書寫方式對於效率每每有本質的差異,這要求咱們對mysql的執行計劃和索引原則有很是清楚的認識,請看下面的語句:
select
distinct cert.emp_id
from
cm_log cl
inner join (
select
emp.id as emp_id,
emp_cert.id as cert_id
from
employee emp
left join emp_certificate emp_cert on emp.id = emp_cert.emp_id
where
emp.is_deleted = 0
) cert on (
cl.ref_table = 'Employee'
and cl.ref_oid = cert.emp_id
)
or (
cl.ref_table = 'EmpCertificate'
and cl.ref_oid = cert.cert_id
)
where
cl.last_upd_date >= '2013-11-07 15:03:00'
and cl.last_upd_date <= '2013-11-08 16:00:00';
複製代碼
53 rows in set (1.87 sec)
複製代碼
簡述一下執行計劃,首先mysql根據idx_last_upd_date索引掃描cm_log表得到379條記錄;而後查表掃描了63727條記錄,分爲兩部分,derived表示構造表,也就是不存在的表,能夠簡單理解成是一個語句造成的結果集,後面的數字表示語句的ID。derived2表示的是ID = 2的查詢構造了虛擬表,而且返回了63727條記錄。咱們再來看看ID = 2的語句究竟作了寫什麼返回了這麼大量的數據,首先全表掃描employee表13317條記錄,而後根據索引emp_certificate_empid關聯emp_certificate表,rows = 1表示,每一個關聯都只鎖定了一條記錄,效率比較高。得到後,再和cm_log的379條記錄根據規則關聯。從執行過程上能夠看出返回了太多的數據,返回的數據絕大部分cm_log都用不到,由於cm_log只鎖定了379條記錄。
如何優化呢?能夠看到咱們在運行完後仍是要和cm_log作join, 那麼咱們能不能以前和cm_log作join呢?仔細分析語句不難發現,其基本思想是若是cm_log的ref_table是EmpCertificate就關聯emp_certificate表,若是ref_table是Employee就關聯employee表,咱們徹底能夠拆成兩部分,並用union鏈接起來,注意這裏用union,而不用union all是由於原語句有「distinct」來獲得惟一的記錄,而union剛好具有了這種功能。若是原語句中沒有distinct不須要去重,咱們就能夠直接使用union all了,由於使用union須要去重的動做,會影響SQL性能。
優化過的語句以下:
select
emp.id
from
cm_log cl
inner join employee emp on cl.ref_table = 'Employee'
and cl.ref_oid = emp.id
where
cl.last_upd_date >= '2013-11-07 15:03:00'
and cl.last_upd_date <= '2013-11-08 16:00:00'
and emp.is_deleted = 0
union
select
emp.id
from
cm_log cl
inner join emp_certificate ec on cl.ref_table = 'EmpCertificate'
and cl.ref_oid = ec.id
inner join employee emp on emp.id = ec.emp_id
where
cl.last_upd_date >= '2013-11-07 15:03:00'
and cl.last_upd_date <= '2013-11-08 16:00:00'
and emp.is_deleted = 0
複製代碼
不須要了解業務場景,只須要改造的語句和改造以前的語句保持結果一致
現有索引能夠知足,不須要建索引
用改造後的語句實驗一下,只須要10ms 下降了近200倍!
舉這個例子的目的在於顛覆咱們對列的區分度的認知,通常上咱們認爲區分度越高的列,越容易鎖定更少的記錄,但在一些特殊的狀況下,這種理論是有侷限性的。
select
*
from
stage_poi sp
where
sp.accurate_result = 1
and (
sp.sync_status = 0
or sp.sync_status = 2
or sp.sync_status = 4
);
複製代碼
951 rows in set (6.22 sec)
複製代碼
全部字段都應用查詢返回記錄數,由於是單表查詢 0已經作過了951條。
讓explain的rows 儘可能逼近951。
看一下accurate_result = 1的記錄數:
select count(*),accurate_result from stage_poi group by accurate_result;
+----------+-----------------+
| count(*) | accurate_result |
+----------+-----------------+
| 1023 | -1 |
| 2114655 | 0 |
| 972815 | 1 |
+----------+-----------------+
複製代碼
咱們看到accurate_result這個字段的區分度很是低,整個表只有-1, 0, 1三個值,加上索引也沒法鎖定特別少許的數據。
再看一下sync_status字段的狀況:
select count(*),sync_status from stage_poi group by sync_status;
+----------+-------------+
| count(*) | sync_status |
+----------+-------------+
| 3080 | 0 |
| 3085413 | 3 |
+----------+-------------+
複製代碼
一樣的區分度也很低,根據理論,也不適合創建索引。
問題分析到這,好像得出了這個表沒法優化的結論,兩個列的區分度都很低,即使加上索引也只能適應這種狀況,很難作廣泛性的優化,好比當sync_status 0、3分佈的很平均,那麼鎖定記錄也是百萬級別的。
找業務方去溝通,看看使用場景。業務方是這麼來使用這個SQL語句的,每隔五分鐘會掃描符合條件的數據,處理完成後把sync_status這個字段變成1, 五分鐘符合條件的記錄數並不會太多,1000個左右。瞭解了業務方的使用場景後,優化這個SQL就變得簡單了,由於業務方保證了數據的不平衡,若是加上索引能夠過濾掉絕大部分不須要的數據。
alter table stage_poi add index idx_acc_status(accurate_result,sync_status);
複製代碼
952 rows in set (0.20 sec)
複製代碼
咱們再來回顧一下分析問題的過程,單表查詢相對來講比較好優化,大部分時候只須要把where條件裏面的字段依照規則加上索引就好,若是隻是這種「無腦」優化的話,顯然一些區分度很是低的列,不該該加索引的列也會被加上索引,這樣會對插入、更新性能形成嚴重的影響,同時也有可能影響其它的查詢語句。因此咱們第4步調差SQL的使用場景很是關鍵,咱們只有知道這個業務場景,才能更好地輔助咱們更好的分析和優化查詢語句。
select
c.id,
c.name,
c.position,
c.sex,
c.phone,
c.office_phone,
c.feature_info,
c.birthday,
c.creator_id,
c.is_keyperson,
c.giveup_reason,
c.status,
c.data_source,
from_unixtime(c.created_time) as created_time,
from_unixtime(c.last_modified) as last_modified,
c.last_modified_user_id
from
contact c
inner join contact_branch cb on c.id = cb.contact_id
inner join branch_user bu on cb.branch_id = bu.branch_id
and bu.status in (1, 2)
inner join org_emp_info oei on oei.data_id = bu.user_id
and oei.node_left >= 2875
and oei.node_right <= 10802
and oei.org_category = - 1
order by
c.created_time desc
limit
0, 10;
複製代碼
仍是幾個步驟。
10 rows in set (13.06 sec)
複製代碼
explain
從執行計劃上看,mysql先查org_emp_info表掃描8849記錄,再用索引idx_userid_status關聯branch_user表,再用索引idx_branch_id關聯contact_branch表,最後主鍵關聯contact表。
rows返回的都很是少,看不到有什麼異常狀況。咱們在看一下語句,發現後面有order by + limit組合,會不會是排序量太大搞的?因而咱們簡化SQL,去掉後面的order by 和 limit,看看到底用了多少記錄來排序。
select
count(*)
from
contact c
inner join
contact_branch cb
on c.id = cb.contact_id
inner join
branch_user bu
on cb.branch_id = bu.branch_id
and bu.status in (
1,
2)
inner join
org_emp_info oei
on oei.data_id = bu.user_id
and oei.node_left >= 2875
and oei.node_right <= 10802
and oei.org_category = - 1
+----------+
| count(*) |
+----------+
| 778878 |
+----------+
1 row in set (5.19 sec)
複製代碼
發現排序以前竟然鎖定了778878條記錄,若是針對70萬的結果集排序,將是災難性的,怪不得這麼慢,那咱們能不能換個思路,先根據contact的created_time排序,再來join會不會比較快呢?
因而改形成下面的語句,也能夠用straight_join來優化:
select
c.id,
c.name,
c.position,
c.sex,
c.phone,
c.office_phone,
c.feature_info,
c.birthday,
c.creator_id,
c.is_keyperson,
c.giveup_reason,
c.status,
c.data_source,
from_unixtime(c.created_time) as created_time,
from_unixtime(c.last_modified) as last_modified,
c.last_modified_user_id
from
contact c
where
exists (
select
1
from
contact_branch cb
inner join branch_user bu on cb.branch_id = bu.branch_id
and bu.status in (1, 2)
inner join org_emp_info oei on oei.data_id = bu.user_id
and oei.node_left >= 2875
and oei.node_right <= 10802
and oei.org_category = - 1
where
c.id = cb.contact_id
)
order by
c.created_time desc
limit
0, 10;
複製代碼
驗證一下效果 預計在1ms內,提高了13000多倍!
10 rows in set (0.00 sec)
複製代碼
本覺得至此大工告成,但咱們在前面的分析中漏了一個細節,先排序再join和先join再排序理論上開銷是同樣的,爲什麼提高這麼可能是由於有一個limit!大體執行過程是:mysql先按索引排序獲得前10條記錄,而後再去join過濾,當發現不夠10條的時候,再次去10條,再次join,這顯然在內層join過濾的數據很是多的時候,將是災難的,極端狀況,內層一條數據都找不到,mysql還傻乎乎的每次取10條,幾乎遍歷了這個數據表!
用不一樣參數的SQL試驗下:
select
sql_no_cache c.id,
c.name,
c.position,
c.sex,
c.phone,
c.office_phone,
c.feature_info,
c.birthday,
c.creator_id,
c.is_keyperson,
c.giveup_reason,
c.status,
c.data_source,
from_unixtime(c.created_time) as created_time,
from_unixtime(c.last_modified) as last_modified,
c.last_modified_user_id
from
contact c
where
exists (
select
1
from
contact_branch cb
inner join branch_user bu on cb.branch_id = bu.branch_id
and bu.status in (1, 2)
inner join org_emp_info oei on oei.data_id = bu.user_id
and oei.node_left >= 2875
and oei.node_right <= 2875
and oei.org_category = - 1
where
c.id = cb.contact_id
)
order by
c.created_time desc
limit
0, 10;
Empty set (2 min 18.99 sec)
複製代碼
2 min 18. 99 sec!比以前的狀況還糟糕不少。因爲mysql的nested loop機制,遇到這種狀況,基本是沒法優化的。這條語句最終也只能交給應用系統去優化本身的邏輯了。
經過這個例子咱們能夠看到,並非全部語句都能優化,而每每咱們優化時,因爲SQL用例迴歸時落掉一些極端狀況,會形成比原來還嚴重的後果。因此,第一:不要期望全部語句都能經過SQL優化,第二:不要過於自信,只針對具體case來優化,而忽略了更復雜的狀況。
慢查詢的案例就分析到這兒,以上只是一些比較典型的案例。咱們在優化過程當中遇到過超過1000行,涉及到16個表join的「垃圾SQL」,也遇到過線上線下數據庫差別致使應用直接被慢查詢拖死,也遇到過varchar等值比較沒有寫單引號,還遇到過笛卡爾積查詢直接把從庫搞死。再多的案例其實也只是一些經驗的積累,若是咱們熟悉查詢優化器、索引的內部原理,那麼分析這些案例就變得特別簡單了。