從新學習Mysql數據庫5:根據MySQL索引原理進行分析與優化

本文轉自互聯網html

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到個人倉庫裏查看mysql

https://github.com/h2pl/Java-Tutorialgit

喜歡的話麻煩點下Star哈github

文章首發於個人我的博客:面試

www.how2playlife.comsql

本文是微信公衆號【Java技術江湖】的《從新學習MySQL數據庫》其中一篇,本文部份內容來源於網絡,爲了把本文主題講得清晰透徹,也整合了不少我認爲不錯的技術博客內容,引用其中了一些比較好的博客文章,若有侵權,請聯繫做者。數據庫

該系列博文會告訴你如何從入門到進階,從sql基本的使用方法,從MySQL執行引擎再到索引、事務等知識,一步步地學習MySQL相關技術的實現原理,更好地瞭解如何基於這些知識來優化sql,減小SQL執行時間,經過執行計劃對SQL性能進行分析,再到MySQL的主從複製、主備部署等內容,以便讓你更完整地瞭解整個MySQL方面的技術體系,造成本身的知識框架。緩存

若是對本系列文章有什麼建議,或者是有什麼疑問的話,也能夠關注公衆號【Java技術江湖】聯繫做者,歡迎你參與本系列博文的創做和修訂。微信

一:Mysql原理與慢查詢

MySQL憑藉着出色的性能、低廉的成本、豐富的資源,已經成爲絕大多數互聯網公司的首選關係型數據庫。雖然性能出色,但所謂「好馬配好鞍」,如何可以更好的使用它,已經成爲開發工程師的必修課,咱們常常會從職位描述上看到諸如「精通MySQL」、「SQL語句優化」、「瞭解數據庫原理」等要求。咱們知道通常的應用系統,讀寫比例在10:1左右,並且插入操做和通常的更新操做不多出現性能問題,遇到最多的,也是最容易出問題的,仍是一些複雜的查詢操做,因此查詢語句的優化顯然是重中之重。網絡

本人從13年7月份起,一直在美團核心業務系統部作慢查詢的優化工做,共計十餘個系統,累計解決和積累了上百個慢查詢案例。隨着業務的複雜性提高,遇到的問題千奇百怪,五花八門,匪夷所思。本文旨在以開發工程師的角度來解釋數據庫索引的原理和如何優化慢查詢。

一個慢查詢引起的思考

select   count(*) from   task where   status=2    and operator_id=20839    and operate_time>1371169729    and operate_time<1371174603    and type=2;複製代碼

系統使用者反應有一個功能愈來愈慢,因而工程師找到了上面的SQL。而且興致沖沖的找到了我,「這個SQL須要優化,給我把每一個字段都加上索引」我很驚訝,問道「爲何須要每一個字段都加上索引?」「把查詢的字段都加上索引會更快」工程師信心滿滿「這種狀況徹底能夠建一個聯合索引,由於是最左前綴匹配,因此operate_time須要放到最後,並且還須要把其餘相關的查詢都拿來,須要作一個綜合評估。」「聯合索引?最左前綴匹配?綜合評估?」工程師不由陷入了沉思。多數狀況下,咱們知道索引可以提升查詢效率,但應該如何創建索引?索引的順序如何?許多人卻只知道大概。其實理解這些概念並不難,並且索引的原理遠沒有想象的那麼複雜。

二:索引創建

1. 主鍵索引

primary key() 要求關鍵字不能重複,也不能爲null,同時增長主鍵約束 主鍵索引定義時,不能命名

2. 惟一索引

unique index() 要求關鍵字不能重複,同時增長惟一約束

3. 普通索引

index() 對關鍵字沒有要求

4. 全文索引

fulltext key() 關鍵字的來源不是全部字段的數據,而是字段中提取的特別關鍵字

關鍵字:能夠是某個字段或多個字段,多個字段稱爲複合索引

建表:creat table student(    stu_id int unsigned not null auto_increment,    name varchar(32) not null default '',    phone char(11) not null default '',    stu_code varchar(32) not null default '',    stu_desc text,    primary key ('stu_id'),     //主鍵索引    unique index 'stu_code' ('stu_code'), //惟一索引    index 'name_phone' ('name','phone'),  //普通索引,複合索引    fulltext index 'stu_desc' ('stu_desc'), //全文索引) engine=myisam charset=utf8; 更新:alert table student    add primary key ('stu_id'),     //主鍵索引    add unique index 'stu_code' ('stu_code'), //惟一索引    add index 'name_phone' ('name','phone'),  //普通索引,複合索引    add fulltext index 'stu_desc' ('stu_desc'); //全文索引 刪除:alert table sutdent    drop primary key,    drop index 'stu_code',    drop index 'name_phone',    drop index 'stu_desc';複製代碼

三:淺析explain用法

有什麼用?

在MySQL中,當數據量增加的特別大的時候就須要用到索引來優化SQL語句,而如何才能判斷咱們辛辛苦苦寫出的SQL語句是否優良?這時候explain就派上了用場。

怎麼使用?

explain + SQL語句便可 如:explain select * from table;
複製代碼

以下

explain參數

相信第一次使用explain參數的朋友必定會疑惑這一大堆參數究竟有什麼用呢?筆者蒐集了一些資料,在這兒作一個總結但願可以幫助你們理解。

參數介紹

id

若是是子查詢,id的序號會遞增,id的值越大優先級越高,越先被執行
複製代碼

select_type

查詢的類型,主要用於區別普通查詢、聯合查詢、子查詢等的複雜查詢 SIMPLE:簡單的select查詢,查詢中不包含子查詢或者UNION PRIMARY:查詢中若包含任何複雜的子部分,最外層查詢則被標記爲PRIMARY(最後加載的那一個 ) SUBQUERY:在SELECT或WHERE列表中包含了子查詢 DERIVED:在FROM列表中包含的子查詢被標記爲DERIVED(衍生)Mysql會遞歸執行這些子查詢,把結果放在臨時表裏。 UNION:若第二個SELECT出如今UNION以後,則被標記爲UNION;若UNION包含在FROM字句的查詢中,外層SELECT將被標記爲:DERIVED UNION RESULT:從UNION表獲取結果的SELECT type

顯示查詢使用了何種類型

從最好到最差依次是System>const>eq_ref>range>index>All(**全表掃描**) 通常來講**至少達到range級別,最好達到ref**

System:表只有一行記錄,這是const類型的特例,平時不會出現(忽略不計)const:表示經過索引一次就找到了,const用於比較primary key或者unique索引,由於只匹配一行數據,因此很快。如將主鍵置於where列表中,MySQL就能將該查詢轉換爲一個常量。

eq_ref:惟一性索引掃描,對於每一個索引鍵,表中只有一條記錄與之匹配。常見於主鍵或惟一索引掃描。

ref:非惟一索引掃描,返回匹配某個單獨值的行,本質上也是一種索引訪問,它返回全部匹配某個單獨值的行,然而它可能會找到多個符合條件的行,因此它應該屬於查找和掃描的混合體range:只檢索給定範圍的行,使用一個索引來選擇行。

key列顯示使用了哪一個索引,通常就是在你的where語句中出現了between、<、>、in等的查詢。這種範圍掃描索引比全表掃描要好,由於它只須要開始於索引的某一點,而結束於另外一點,不用掃描所有索引。index:FULL INDEX SCAN,index與all區別爲index類型只遍歷索引樹。這一般比all快,由於索引文件一般比數據文件小。

extra

包含不適合在其餘列中顯示但十分重要的額外信息 包含的信息: (危險!)Using

filesort:說明mysql會對數據使用一個外部的索引排序,而不是按照表內的索引順序進行讀取,MYSQL中沒法利用索引完成的排序操做稱爲「文件排序」 (特別危險!)Using

temporary:使用了臨時表保存中間結果,MYSQL在對查詢結果排序時使用臨時表。常見於排序order by 和分組查詢 group by Using

index:表示相應的select操做中使用了覆蓋索引,避免訪問了表的數據行,效率不錯。若是同時出現using

where,代表索引被用來執行索引鍵值的查找;若是沒有同時出現using where,代表索引用來讀取數據而非執行查找操做。

possible_keys

顯示可能應用在這張表中的索引,一個或多個。查詢涉及到的字段上若存在索引,則該索引將被列出, 但不必定被查詢實際使用

key

實際使用的索引,若是爲NULL,則沒有使用索引。查詢中若使用了覆蓋索引,則該索引僅出如今key列表中,key參數能夠做爲使用了索引的判斷標準

key_len

:表示索引中使用的字節數,可經過該列計算查詢中索引的長度,在不損失精確性的狀況下,長度越短越好,keylen顯示的值爲索引字段的最大可能長度,並不是實際使用長度,即keylen是根據表定義計算而得,不是經過表內檢索出的。

ref

顯示索引的哪一列被使用了,若是可能的話,是一個常數。哪些列或常量被用於查找索引上的值。

rows

根據表統計信息及索引選用狀況,大體估算出找到所需記錄所須要讀取的行數

四:慢查詢優化

關於MySQL索引原理是比較枯燥的東西,你們只須要有一個感性的認識,並不須要理解得很是透徹和深刻。咱們回頭來看看一開始咱們說的慢查詢,瞭解完索引原理以後,你們是否是有什麼想法呢?先總結一下索引的幾大基本原則

建索引的幾大原則

1.最左前綴匹配原則,很是重要的原則,mysql會一直向右匹配直到遇到範圍查詢(>、<、between、like)就中止匹配,好比a 1="" =="" and="" b="2" c=""> 3 and d = 4 若是創建(a,b,c,d)順序的索引,d是用不到索引的,若是創建(a,b,d,c)的索引則均可以用到,a,b,d的順序能夠任意調整。

2.=和in能夠亂序,好比a = 1 and b = 2 and c = 3 創建(a,b,c)索引能夠任意順序,mysql的查詢優化器會幫你優化成索引能夠識別的形式

3.儘可能選擇區分度高的列做爲索引,區分度的公式是count(distinct col)/count(*),表示字段不重複的比例,比例越大咱們掃描的記錄數越少,惟一鍵的區分度是1,而一些狀態、性別字段可能在大數據面前區分度就是0,那可能有人會問,這個比例有什麼經驗值嗎?使用場景不一樣,這個值也很難肯定,通常須要join的字段咱們都要求是0.1以上,即平均1條掃描10條記錄

4.索引列不能參與計算,保持列「乾淨」,好比fromunixtime(createtime) = ’2014-05-29’就不能使用到索引,緣由很簡單,b+樹中存的都是數據表中的字段值,但進行檢索時,須要把全部元素都應用函數才能比較,顯然成本太大。因此語句應該寫成createtime = unixtimestamp(’2014-05-29’);5.儘可能的擴展索引,不要新建索引。好比表中已經有a的索引,如今要加(a,b)的索引,那麼只須要修改原來的索引便可

回到開始的慢查詢

根據最左匹配原則,最開始的sql語句的索引應該是status、operatorid、type、operatetime的聯合索引;其中status、operator_id、type的順序能夠顛倒,因此我纔會說,把這個表的全部相關查詢都找到,會綜合分析;

好比還有以下查詢

select * from task where status = 0 and type = 12 limit 10;
複製代碼

select count(*) from task where status = 0 ;
複製代碼

那麼索引創建成(status,type,operatorid,operatetime)就是很是正確的,由於能夠覆蓋到全部狀況。這個就是利用了索引的最左匹配的原則

查詢優化神器 - explain命令

關於explain命令相信你們並不陌生,具體用法和字段含義能夠參考官網explain-output,這裏須要強調rows是核心指標,絕大部分rows小的語句執行必定很快(有例外,下面會講到)。因此優化語句基本上都是在優化rows。

慢查詢優化基本步驟

0.先運行看看是否真的很慢,注意設置SQLNOCACHE1.where條件單表查,鎖定最小返回記錄表。這句話的意思是把查詢語句的where都應用到表中返回的記錄數最小的表開始查起,單表每一個字段分別查詢,看哪一個字段的區分度最高2.explain查看執行計劃,是否與1預期一致(從鎖定記錄較少的表開始查詢)3.order by limit 形式的sql語句讓排序的表優先查4.瞭解業務方使用場景5.加索引時參照建索引的幾大原則

6.觀察結果,不符合預期繼續從0分析

五:最左前綴原理與相關優化

高效使用索引的首要條件是知道什麼樣的查詢會使用到索引,這個問題和B+Tree中的「最左前綴原理」有關,下面經過例子說明最左前綴原理。

這裏先說一下聯合索引的概念。在上文中,咱們都是假設索引只引用了單個的列,實際上,MySQL中的索引能夠以必定順序引用多個列,這種索引叫作聯合索引,通常的,一個聯合索引是一個有序元組,其中各個元素均爲數據表的一列,實際上要嚴格定義索引須要用到關係代數,可是這裏我不想討論太多關係代數的話題,由於那樣會顯得很枯燥,因此這裏就再也不作嚴格定義。另外,單列索引能夠當作聯合索引元素數爲1的特例。

以employees.titles表爲例,下面先查看其上都有哪些索引:

  1. SHOW INDEX FROM employees.titles;
  2. +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+
  3. | Table | Nonunique | Keyname | Seqinindex | Columnname | Collation | Cardinality | Null | Indextype |
  4. +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+
  5. | titles | 0 | PRIMARY | 1 | emp_no | A | NULL | | BTREE |
  6. | titles | 0 | PRIMARY | 2 | title | A | NULL | | BTREE |
  7. | titles | 0 | PRIMARY | 3 | from_date | A | 443308 | | BTREE |
  8. | titles | 1 | empno | 1 | empno | A | 443308 | | BTREE |
  9. +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+

從結果中能夠到titles表的主索引爲 no, title, fromdate>,還有一個輔助索引 。爲了不多個索引使事情變複雜(MySQL的SQL優化器在多索引時行爲比較複雜),這裏咱們將輔助索引drop掉:

  1. ALTER TABLE employees.titles DROP INDEX emp_no;

這樣就能夠專心分析索引PRIMARY的行爲了。

狀況一:全列匹配。

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title='Senior Engineer' AND from_date='1986-06-26';
  2. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
  3. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
  5. | 1 | SIMPLE | titles | const | PRIMARY | PRIMARY | 59 | const,const,const | 1 | |
  6. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+

很明顯,當按照索引中全部列進行精確匹配(這裏精確匹配指「=」或「IN」匹配)時,索引能夠被用到。這裏有一點須要注意,理論上索引對順序是敏感的,可是因爲MySQL的查詢優化器會自動調整where子句的條件順序以使用適合的索引,例如咱們將where中的條件順序顛倒:

  1. EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26' AND emp_no='10001' AND title='Senior Engineer';
  2. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
  3. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
  5. | 1 | SIMPLE | titles | const | PRIMARY | PRIMARY | 59 | const,const,const | 1 | |
  6. +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+

效果是同樣的。

狀況二:最左前綴匹配。

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001';
  2. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
  3. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
  5. | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | |
  6. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+

當查詢條件精確匹配索引的左邊連續一個或幾個列時,如 no>或 < em="">no, title>,因此能夠被用到,可是隻能用到一部分,即條件所組成的最左前綴。上面的查詢從分析結果看用到了PRIMARY索引,可是key_len爲4,說明只用到了索引的第一列前綴。 <>

狀況三:查詢條件用到了索引中列的精確匹配,可是中間某個條件未提供。

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26';
  2. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
  3. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
  5. | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | Using where |
  6. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+

此時索引使用狀況和狀況二相同,由於title未提供,因此查詢只用到了索引的第一列,然後面的fromdate雖然也在索引中,可是因爲title不存在而沒法和左前綴鏈接,所以須要對結果進行掃描過濾fromdate(這裏因爲emp_no惟一,因此不存在掃描)。

若是想讓fromdate也使用索引而不是where過濾,能夠增長一個輔助索引 < em="">no, from date>,此時上面的查詢會使用這個索引。除此以外,還可使用一種稱之爲「隔離列」的優化方法,將empno與from_date之間的「坑」填上。 <>

首先咱們看下title一共有幾種不一樣的值:

  1. SELECT DISTINCT(title) FROM employees.titles;
  2. +--------------------+
  3. | title |
  4. +--------------------+
  5. | Senior Engineer |
  6. | Staff |
  7. | Engineer |
  8. | Senior Staff |
  9. | Assistant Engineer |
  10. | Technique Leader |
  11. | Manager |
  12. +--------------------+

只有7種。在這種成爲「坑」的列值比較少的狀況下,能夠考慮用「IN」來填補這個「坑」從而造成最左前綴:

  1. EXPLAIN SELECT * FROM employees.titles
  2. WHERE emp_no='10001'
  3. AND title IN ('Senior Engineer', 'Staff', 'Engineer', 'Senior Staff', 'Assistant Engineer', 'Technique Leader', 'Manager')
  4. AND from_date='1986-06-26';
  5. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  6. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  7. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  8. | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 59 | NULL | 7 | Using where |
  9. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+

此次key_len爲59,說明索引被用全了,可是從type和rows看出IN實際上執行了一個range查詢,這裏檢查了7個key。看下兩種查詢的性能比較:

  1. SHOW PROFILES;
  2. +----------+------------+-------------------------------------------------------------------------------+
  3. | Query_ID | Duration | Query |
  4. +----------+------------+-------------------------------------------------------------------------------+
  5. | 10 | 0.00058000 | SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26'|
  6. | 11 | 0.00052500 | SELECT * FROM employees.titles WHERE emp_no='10001' AND title IN ... |
  7. +----------+------------+-------------------------------------------------------------------------------+

「填坑」後性能提高了一點。若是通過emp_no篩選後餘下不少數據,則後者性能優點會更加明顯。固然,若是title的值不少,用填坑就不合適了,必須創建輔助索引。

狀況四:查詢條件沒有指定索引第一列。

  1. EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26';
  2. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
  3. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
  5. | 1 | SIMPLE | titles | ALL | NULL | NULL | NULL | NULL | 443308 | Using where |
  6. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+

因爲不是最左前綴,索引這樣的查詢顯然用不到索引。

狀況五:匹配某列的前綴字符串。

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title LIKE 'Senior%';
  2. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  3. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  5. | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 56 | NULL | 1 | Using where |
  6. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+

此時能夠用到索引,可是若是通配符不是隻出如今末尾,則沒法使用索引。(原文表述有誤,若是通配符%不出如今開頭,則能夠用到索引,但根據具體狀況不一樣可能只會用其中一個前綴)

狀況六:範圍查詢。

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no < '10010' and title='senior engineer';="" <="" li="">
  2. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  3. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  5. | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 4 | NULL | 16 | Using where |
  6. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+

範圍列能夠用到索引(必須是最左前綴),可是範圍列後面的列沒法用到索引。同時,索引最多用於一個範圍列,所以若是查詢條件中有兩個範圍列則沒法全用到索引。

  1. EXPLAIN SELECT * FROM employees.titles
  2. WHERE emp_no < '10010' <="" li="">
  3. AND title='Senior Engineer'
  4. AND from_date BETWEEN '1986-01-01' AND '1986-12-31';
  5. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  6. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  7. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  8. | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 4 | NULL | 16 | Using where |
  9. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+

能夠看到索引對第二個範圍索引無能爲力。這裏特別要說明MySQL一個有意思的地方,那就是僅用explain可能沒法區分範圍索引和多值匹配,由於在type中這二者都顯示爲range。同時,用了「between」並不意味着就是範圍查詢,例以下面的查詢:

  1. EXPLAIN SELECT * FROM employees.titles
  2. WHERE emp_no BETWEEN '10001' AND '10010'
  3. AND title='Senior Engineer'
  4. AND from_date BETWEEN '1986-01-01' AND '1986-12-31';
  5. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  6. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  7. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
  8. | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 59 | NULL | 16 | Using where |
  9. +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+

看起來是用了兩個範圍查詢,但做用於empno上的「BETWEEN」實際上至關於「IN」,也就是說empno實際是多值精確匹配。能夠看到這個查詢用到了索引所有三個列。所以在MySQL中要謹慎地區分多值匹配和範圍匹配,不然會對MySQL的行爲產生困惑。

狀況七:查詢條件中含有函數或表達式。

很不幸,若是查詢條件中含有函數或表達式,則MySQL不會爲這列使用索引(雖然某些在數學意義上可使用)。例如:

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND left(title, 6)='Senior';
  2. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
  3. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
  5. | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | Using where |
  6. +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+

雖然這個查詢和狀況五中功能相同,可是因爲使用了函數left,則沒法爲title列應用索引,而狀況五中用LIKE則能夠。再如:

  1. EXPLAIN SELECT * FROM employees.titles WHERE emp_no - 1='10000';
  2. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
  3. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  4. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
  5. | 1 | SIMPLE | titles | ALL | NULL | NULL | NULL | NULL | 443308 | Using where |
  6. +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+

顯然這個查詢等價於查詢emp_no爲10001的函數,可是因爲查詢條件是一個表達式,MySQL沒法爲其使用索引。看來MySQL尚未智能到自動優化常量表達式的程度,所以在寫查詢語句時儘可能避免表達式出如今查詢中,而是先手工私下代數運算,轉換爲無表達式的查詢語句。

索引選擇性與前綴索引

既然索引能夠加快查詢速度,那麼是否是隻要是查詢語句須要,就建上索引?答案是否認的。由於索引雖然加快了查詢速度,但索引也是有代價的:索引文件自己要消耗存儲空間,同時索引會加劇插入、刪除和修改記錄時的負擔,另外,MySQL在運行時也要消耗資源維護索引,所以索引並非越多越好。通常兩種狀況下不建議建索引。

第一種狀況是表記錄比較少,例如一兩千條甚至只有幾百條記錄的表,不必建索引,讓查詢作全表掃描就行了。至於多少條記錄纔算多,這個我的有我的的見解,我我的的經驗是以2000做爲分界線,記錄數不超過 2000能夠考慮不建索引,超過2000條能夠酌情考慮索引。

另外一種不建議建索引的狀況是索引的選擇性較低。所謂索引的選擇性(Selectivity),是指不重複的索引值(也叫基數,Cardinality)與表記錄數(#T)的比值:

Index Selectivity = Cardinality / #T

顯然選擇性的取值範圍爲(0, 1],選擇性越高的索引價值越大,這是由B+Tree的性質決定的。例如,上文用到的employees.titles表,若是title字段常常被單獨查詢,是否須要建索引,咱們看一下它的選擇性:

  1. SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles;
  2. +-------------+
  3. | Selectivity |
  4. +-------------+
  5. | 0.0000 |
  6. +-------------+

title的選擇性不足0.0001(精確值爲0.00001579),因此實在沒有什麼必要爲其單獨建索引。

有一種與索引選擇性有關的索引優化策略叫作前綴索引,就是用列的前綴代替整個列做爲索引key,當前綴長度合適時,能夠作到既使得前綴索引的選擇性接近全列索引,同時由於索引key變短而減小了索引文件的大小和維護開銷。下面以employees.employees表爲例介紹前綴索引的選擇和使用。

從圖12能夠看到employees表只有一個索引 ,那麼若是咱們想按名字搜索一我的,就只能全表掃描了:

  1. EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
  2. +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
  3. | id | selecttype | table | type | possiblekeys | key | key_len | ref | rows | Extra |
  4. +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
  5. | 1 | SIMPLE | employees | ALL | NULL | NULL | NULL | NULL | 300024 | Using where |
  6. +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+

若是頻繁按名字搜索員工,這樣顯然效率很低,所以咱們能夠考慮建索引。有兩種選擇,建 name>或 < em="">name, last_name>,看下兩個索引的選擇性: <>

  1. SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees;
  2. +-------------+
  3. | Selectivity |
  4. +-------------+
  5. | 0.0042 |
  6. +-------------+
  7. SELECT count(DISTINCT(concat(firstname, lastname)))/count(*) AS Selectivity FROM employees.employees;
  8. +-------------+
  9. | Selectivity |
  10. +-------------+
  11. | 0.9313 |
  12. +-------------+

name>顯然選擇性過低, < em="">name, last name>選擇性很好,可是firstname和last name加起來長度爲30,有沒有兼顧長度和選擇性的辦法?能夠考慮用firstname和last name的前幾個字符創建索引,例如 < em="">name, left(last_name, 3)>,看看其選擇性: <> <>

  1. SELECT count(DISTINCT(concat(firstname, left(lastname, 3))))/count(*) AS Selectivity FROM employees.employees;
  2. +-------------+
  3. | Selectivity |
  4. +-------------+
  5. | 0.7879 |
  6. +-------------+

選擇性還不錯,但離0.9313仍是有點距離,那麼把last_name前綴加到4:

  1. SELECT count(DISTINCT(concat(firstname, left(lastname, 4))))/count(*) AS Selectivity FROM employees.employees;
  2. +-------------+
  3. | Selectivity |
  4. +-------------+
  5. | 0.9007 |
  6. +-------------+

這時選擇性已經很理想了,而這個索引的長度只有18,比 name, lastname>短了接近一半,咱們把這個前綴索引 建上:

  1. ALTER TABLE employees.employees
  2. ADD INDEX first_name_last_name4 (firstname, lastname(4));

此時再執行一遍按名字查詢,比較分析一下與建索引前的結果:

  1. SHOW PROFILES;
  2. +----------+------------+---------------------------------------------------------------------------------+
  3. | Query_ID | Duration | Query |
  4. +----------+------------+---------------------------------------------------------------------------------+
  5. | 87 | 0.11941700 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |
  6. | 90 | 0.00092400 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |
  7. +----------+------------+---------------------------------------------------------------------------------+

性能的提高是顯著的,查詢速度提升了120多倍。

前綴索引兼顧索引大小和查詢速度,可是其缺點是不能用於ORDER BY和GROUP BY操做,也不能用於Covering index(即當索引自己包含查詢所需所有數據時,再也不訪問數據文件自己

六:InnoDB的主鍵選擇與插入優化

在使用InnoDB存儲引擎時,若是沒有特別的須要,請永遠使用一個與業務無關的自增字段做爲主鍵。

常常看到有帖子或博客討論主鍵選擇問題,有人建議使用業務無關的自增主鍵,有人以爲沒有必要,徹底可使用如學號或身份證號這種惟一字段做爲主鍵。不論支持哪一種論點,大多數論據都是業務層面的。若是從數據庫索引優化角度看,使用InnoDB引擎而不使用自增主鍵絕對是一個糟糕的主意。

上文討論過InnoDB的索引實現,InnoDB使用匯集索引,數據記錄自己被存於主索引(一顆B+Tree)的葉子節點上。這就要求同一個葉子節點內(大小爲一個內存頁或磁盤頁)的各條數據記錄按主鍵順序存放,所以每當有一條新的記錄插入時,MySQL會根據其主鍵將其插入適當的節點和位置,若是頁面達到裝載因子(InnoDB默認爲15/16),則開闢一個新的頁(節點)。

若是表使用自增主鍵,那麼每次插入新的記錄,記錄就會順序添加到當前索引節點的後續位置,當一頁寫滿,就會自動開闢一個新的頁。以下圖所示:

圖13

這樣就會造成一個緊湊的索引結構,近似順序填滿。因爲每次插入時也不須要移動已有數據,所以效率很高,也不會增長不少開銷在維護索引上。

若是使用非自增主鍵(若是身份證號或學號等),因爲每次插入主鍵的值近似於隨機,所以每次新紀錄都要被插到現有索引頁得中間某個位置:

圖14

此時MySQL不得不爲了將新記錄插到合適位置而移動數據,甚至目標頁面可能已經被回寫到磁盤上而從緩存中清掉,此時又要從磁盤上讀回來,這增長了不少開銷,同時頻繁的移動、分頁操做形成了大量的碎片,獲得了不夠緊湊的索引結構,後續不得不經過OPTIMIZE TABLE來重建表並優化填充頁面。

所以,只要能夠,請儘可能在InnoDB上採用自增字段作主鍵。

相關文章
相關標籤/搜索