1. 程式人生 > >MySQL索引原理

MySQL索引原理

一、介紹

1.什麼是索引?

一般的應用系統,讀寫比例在10:1左右,插入操作和一般的更新操作很少出現效能問題,在生產環境中,我們遇到最多的,也是最容易出問題的,還是一些複雜的查詢操作,因此對查詢語句的優化顯然是重中之重。說起加速查詢,就不得不提到索引了。

2.為什麼要有索引呢?

索引在MySQL中也叫做“鍵”,是儲存引擎用於快速找到記錄的一種資料結構。

索引對於良好的效能非常關鍵,尤其是當表中的資料量越來越大時,索引對於效能的影響愈發重要。 索引優化應該是對查詢效能優化最有效的手段了。索引能夠輕易將查詢效能提高好幾個數量級。 索引相當於字典的音序表,如果要查某個字,如果不使用音序表,則需要從幾百頁中逐頁去查

二、索引的原理

一 索引原理

與我們查閱圖書所用的目錄是一個道理:先定位到章,然後定位到該章下的一個小節,然後找到頁數。相似的例子還有:查字典,查火車車次,飛機航班等

本質都是:通過不斷地縮小想要獲取資料的範圍來篩選出最終想要的結果,同時把隨機的事件變成順序的事件,也就是說,有了這種索引機制,我們可以總是用同一種查詢方式來鎖定資料

資料庫也是一樣,但顯然要複雜的多,因為不僅面臨著等值查詢,還有範圍查詢(>、<、between、in)、模糊查詢(like)、並集查詢(or)等等。資料庫應該選擇怎麼樣的方式來應對所有的問題呢?我們回想字典的例子,能不能把資料分成段,然後分段查詢呢?最簡單的如果1000條資料,1到100分成第一段,101到200分成第二段,201到300分成第三段……這樣查第250條資料,只要找第三段就可以了,一下子去除了90%的無效資料。但如果是1千萬的記錄呢,分成幾段比較好?稍有演算法基礎的同學會想到搜尋樹,其平均複雜度是lgN,具有不錯的查詢效能。但這裡我們忽略了一個關鍵的問題,複雜度模型是基於每次相同的操作成本來考慮的。而資料庫實現比較複雜,一方面資料是儲存在磁碟上的,另外一方面為了提高效能,每次又可以把部分資料讀入記憶體來計算,因為我們知道訪問磁碟的成本大概是訪問記憶體的十萬倍左右,所以簡單的搜尋樹難以滿足複雜的應用場景。

 二、 磁碟IO與預讀

分頁:

考慮到磁碟IO是非常高昂的操作,計算機作業系統做了一些優化,當一次IO時,不光把當前磁碟地址的資料,而是把相鄰的資料也都讀取到記憶體緩衝區內,因為區域性預讀性原理告訴我們,當計算機訪問一個地址的資料的時候,與其相鄰的資料也會很快被訪問到。每一次IO讀取的資料我們稱之為一頁(page)。具體一頁有多大資料跟作業系統有關,一般為4k或8k,也就是我們讀取一頁內的資料時候,實際上才發生了一次IO,這個理論對於索引的資料結構設計非常有幫助。

三、索引的資料結構

任何一種資料結構都不是憑空產生的,一定會有它的背景和使用場景,我們現在總結一下,我們需要這種資料結構能夠做些什麼,其實很簡單,那就是:每次查詢資料時把磁碟IO次數控制在一個很小的數量級,最好是常數數量級。

那麼我們就想到如果一個高度可控的多路搜尋樹是否能滿足需求呢?就這樣,b+樹應運而生。

如上圖,是一顆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並不真實存在於資料表中。

b+樹的查詢過程 如圖所示,如果要查詢資料項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,顯然成本非常非常高。

b+樹性質

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的資料了, 這個是非常重要的性質,即索引的最左匹配特性。

四、Mysql索引管理

一、功能

  1. 1. 索引的功能就是加速查詢
  2. 2. mysql中的primary key,unique,聯合唯一也都是索引,這些索引除了加速查詢以外,還有約束的功能

二、MySQL的索引分類

  1. 索引分類
  2. 1.普通索引index :加速查詢
  3. 2.唯一索引
  4. 主鍵索引:primary key :加速查詢+約束(不為空且唯一)
  5. 唯一索引:unique:加速查詢+約束 (唯一)
  6. 3.聯合索引
  7. -primary key(id,name):聯合主鍵索引
  8. -unique(id,name):聯合唯一索引
  9. -index(id,name):聯合普通索引
  10. 4.全文索引fulltext :用於搜尋很長一篇文章的時候,效果最好。
  11. 5.空間索引spatial :瞭解就好,幾乎不用

各個索引的應用場景

  1. 舉個例子來說,比如你在為某商場做一個會員卡的系統。
  2. 這個系統有一個會員表
  3. 有下列欄位:
  4. 會員編號 INT
  5. 會員姓名 VARCHAR(10)
  6. 會員身份證號碼 VARCHAR(18)
  7. 會員電話 VARCHAR(10)
  8. 會員住址 VARCHAR(50)
  9. 會員備註資訊 TEXT
  10. 那麼這個 會員編號,作為主鍵,使用 PRIMARY
  11. 會員姓名 如果要建索引的話,那麼就是普通的 INDEX
  12. 會員身份證號碼 如果要建索引的話,那麼可以選擇 UNIQUE (唯一的,不允許重複)
  13. #除此之外還有全文索引,即FULLTEXT
  14. 會員備註資訊 , 如果需要建索引的話,可以選擇全文搜尋。
  15. 用於搜尋很長一篇文章的時候,效果最好。
  16. 用在比較短的文字,如果就一兩行字的,普通的 INDEX 也可以。
  17. 但其實對於全文搜尋,我們並不會使用MySQL自帶的該索引,而是會選擇第三方軟體如Sphinx,專門來做全文搜尋。
  18. #其他的如空間索引SPATIAL,瞭解即可,幾乎不用

三、 索引的兩大型別hash與btree

  1. #我們可以在建立上述索引的時候,為其指定索引型別,分兩類
  2. hash型別的索引:查詢單條快,範圍查詢慢
  3. btree型別的索引:b+樹,層數越多,資料量指數級增長(我們就用它,因為innodb預設支援它)
  4. #不同的儲存引擎支援的索引型別也不一樣
  5. InnoDB 支援事務,支援行級別鎖定,支援 B-tree、Full-text 等索引,不支援 Hash 索引;
  6. MyISAM 不支援事務,支援表級別鎖定,支援 B-tree、Full-text 等索引,不支援 Hash 索引;
  7. Memory 不支援事務,支援表級別鎖定,支援 B-tree、Hash 等索引,不支援 Full-text 索引;
  8. NDB 支援事務,支援行級別鎖定,支援 Hash 索引,不支援 B-tree、Full-text 等索引;
  9. Archive 不支援事務,支援表級別鎖定,不支援 B-tree、Hash、Full-text 等索引;

四、建立/刪除索引的語法

建立/刪除索引的語法

  1. 1 #方法一:建立表時
  2. 2   CREATE TABLE 表名 (
  3. 3 欄位名1 資料型別 [完整性約束條件…],
  4. 4 欄位名2 資料型別 [完整性約束條件…],
  5. 5 [UNIQUE | FULLTEXT | SPATIAL ] INDEX | KEY
  6. 6 [索引名] (欄位名[(長度)] [ASC |DESC])
  7. 7 );
  8. 8
  9. 9
  10. 10 #方法二:CREATE在已存在的表上建立索引
  11. 11 CREATE [UNIQUE | FULLTEXT | SPATIAL ] INDEX 索引名
  12. 12 ON 表名 (欄位名[(長度)] [ASC |DESC]) ;
  13. 13
  14. 14
  15. 15 #方法三:ALTER TABLE在已存在的表上建立索引
  16. 16 ALTER TABLE 表名 ADD [UNIQUE | FULLTEXT | SPATIAL ] INDEX
  17. 17 索引名 (欄位名[(長度)] [ASC |DESC]) ;
  18. 18
  19. 19 #刪除索引:DROP INDEX 索引名 ON 表名字;
  1. 善用幫助文件
  2. help create
  3. help create index
  4. ==================
  5. 1.建立索引
  6. -在建立表時就建立(需要注意的幾點)
  7. create table s1(
  8. id int ,#可以在這加primary key
  9. #id int index #不可以這樣加索引,因為index只是索引,沒有約束一說,
  10. #不能像主鍵,還有唯一約束一樣,在定義欄位的時候加索引
  11. name char(20),
  12. age int,
  13. email varchar(30)
  14. #primary key(id) #也可以在這加
  15. index(id) #可以這樣加
  16. );
  17. -在建立表後在建立
  18. create index name on s1(name); #新增普通索引
  19. create unique age on s1(age);新增唯一索引
  20. alter table s1 add primary key(id); #新增住建索引,也就是給id欄位增加一個主鍵約束
  21. create index name on s1(id,name); #新增普通聯合索引
  22. 2.刪除索引
  23. drop index id on s1;
  24. drop index name on s1; #刪除普通索引
  25. drop index age on s1; #刪除唯一索引,就和普通索引一樣,不用在index前加unique來刪,直接就可以刪了
  26. alter table s1 drop primary key; #刪除主鍵(因為它新增的時候是按照alter來增加的,那麼我們也用alter來刪)

幫助檢視

五、測試索引

1、準備

  1. #1. 準備表
  2. create table s1(
  3. id int,
  4. name varchar(20),
  5. gender char(6),
  6. email varchar(50)
  7. );
  8. #2. 建立儲存過程,實現批量插入記錄
  9. delimiter $$ #宣告儲存過程的結束符號為$$
  10. create procedure auto_insert1()
  11. BEGIN
  12. declare i int default 1;
  13. while(i<3000000)do
  14. insert into s1 values(i,concat('egon',i),'male',concat('egon',i,'@oldboy'));
  15. set i=i+1;
  16. end while;
  17. END$$ #$$結束
  18. delimiter ; #重新宣告分號為結束符號
  19. #3. 檢視儲存過程
  20. show create procedure auto_insert1\G
  21. #4. 呼叫儲存過程
  22. call auto_insert1();

2 、在沒有索引的前提下測試查詢速度

  1. #無索引:從頭到尾掃描一遍,所以查詢速度很慢
  2. mysql> select * from s1 where id=333;
  3. +------+---------+--------+----------------+
  4. | id | name | gender | email |
  5. +------+---------+--------+----------------+
  6. | 333 | egon333 | male | [email protected] |
  7. | 333 | egon333 | f | [email protected] |
  8. | 333 | egon333 | f | [email protected] |
  9. +------+---------+--------+----------------+
  10. rows in set (0.32 sec)
  11. mysql> select * from s1 where email='[email protected]';
  12. ....
  13. ... rows in set (0.36 sec)

3、 加上索引

  1. #1. 一定是為搜尋條件的欄位建立索引,比如select * from t1 where age > 5;就需要為age加上索引
  2. #2. 在表中已經有大量資料的情況下,建索引會很慢,且佔用硬碟空間,插入刪除更新都很慢,只有查詢快
  3. 比如create index idx on s1(id);會掃描表中所有的資料,然後以id為資料項,建立索引結構,存放於硬碟的表中。
  4. 建完以後,再查詢就會很快了
  5. #3. 需要注意的是:innodb表的索引會存放於s1.ibd檔案中,而myisam表的索引則會有單獨的索引檔案table1.MYI

六、正確使用索引

一、覆蓋索引
  1. #分析
  2. select * from s1 where id=123;
  3. 該sql命中了索引,但未覆蓋索引。
  4. 利用id=123到索引的資料結構中定位到該id在硬碟中的位置,或者說再資料表中的位置。
  5. 但是我們select的欄位為*,除了id以外還需要其他欄位,這就意味著,我們通過索引結構取到id還不夠,
  6. 還需要利用該id再去找到該id所在行的其他欄位值,這是需要時間的,很明顯,如果我們只select id,
  7. 就減去了這份苦惱,如下
  8. select id from s1 where id=123;
  9. 這條就是覆蓋索引了,命中索引,且從索引的資料結構直接就取到了id在硬碟的地址,速度很快

二、聯合索引

三、索引合併

  1. #索引合併:把多個單列索引合併使用
  2. #分析:
  3. 組合索引能做到的事情,我們都可以用索引合併去解決,比如
  4. create index ne on s1(name,email);#組合索引
  5. 我們完全可以單獨為name和email建立索引
  6. 組合索引可以命中:
  7. select * from s1 where name='egon' ;
  8. select * from s1 where name='egon' and email='adf';
  9. 索引合併可以命中:
  10. select * from s1 where name='egon' ;
  11. select * from s1 where email='adf';
  12. select * from s1 where name='egon' and email='adf';
  13. 乍一看好像索引合併更好了:可以命中更多的情況,但其實要分情況去看,如果是name='egon' and email='adf',
  14. 那麼組合索引的效率要高於索引合併,如果是單條件查,那麼還是用索引合併比較合理

三 若想利用索引達到預想的提高查詢速度的效果,我們在新增索引時,必須遵循以下原則

  1. #1.最左字首匹配原則,非常重要的原則,
  2. create index ix_name_email on s1(name,email,)
  3. - 最左字首匹配:必須按照從左到右的順序匹配
  4. select * from s1 where name='egon'; #可以
  5. select * from s1 where name='egon' and email='asdf'; #可以
  6. select * from s1 where email='[email protected]'; #不可以
  7. mysql會一直向右匹配直到遇到範圍查詢(>、<、between、like)就停止匹配,
  8. 比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)順序的索引,
  9. d是用不到索引的,如果建立(a,b,d,c)的索引則都可以用到,a,b,d的順序可以任意調整。
  10. #2.=和in可以亂序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意順序,mysql的查詢優化器
  11. 會幫你優化成索引可以識別的形式
  12. #3.儘量選擇區分度高的列作為索引,區分度的公式是count(distinct col)/count(*),
  13. 表示欄位不重複的比例,比例越大我們掃描的記錄數越少,唯一鍵的區分度是1,而一些狀態、
  14. 性別欄位可能在大資料面前區分度就是0,那可能有人會問,這個比例有什麼經驗值嗎?使用場景不同,
  15. 這個值也很難確定,一般需要join的欄位我們都要求是0.1以上,即平均1條掃描10條記錄
  16. #4.索引列不能參與計算,保持列“乾淨”,比如from_unixtime(create_time) = ’2014-05-29’
  17. 就不能使用到索引,原因很簡單,b+樹中存的都是資料表中的欄位值,
  18. 但進行檢索時,需要把所有元素都應用函式才能比較,顯然成本太大。
  19. 所以語句應該寫成create_time = unix_timestamp(’2014-05-29’);

最左字首示範

  1. mysql> select * from s1 where id>3 and name='egon' and email='[email protected]' and gender='male';
  2. Empty set (0.39 sec)
  3. mysql> create index idx on s1(id,name,email,gender); #未遵循最左字首
  4. Query OK, 0 rows affected (15.27 sec)
  5. Records: 0 Duplicates: 0 Warnings: 0
  6. mysql> select * from s1 where id>3 and name='egon' and email='[email protected]' and gender='male';
  7. Empty set (0.43 sec)
  8. mysql> drop index idx on s1;
  9. Query OK, 0 rows affected (0.16 sec)
  10. Records: 0 Duplicates: 0 Warnings: 0
  11. mysql> create index idx on s1(name,email,gender,id); #遵循最左字首
  12. Query OK, 0 rows affected (15.97 sec)
  13. Records: 0 Duplicates: 0 Warnings: 0
  14. mysql> select * from s1 where id>3 and name='egon' and email='[email protected]' and gender='male';
  15. Empty set (0.03 sec)

建聯合索引,最左匹配

  1. 1 6. 最左字首匹配
  2. 2 index(id,age,email,name)
  3. 3 #條件中一定要出現id(只要出現id就會提升速度)
  4. 4 id
  5. 5 id age
  6. 6 id email
  7. 7 id name
  8. 8
  9. 9 email #不行 如果單獨這個開頭就不能提升速度了
  10. 10 mysql> select count(*) from s1 where id=3000;
  11. 11 +----------+
  12. 12 | count(*) |
  13. 13 +----------+
  14. 14 | 1 |
  15. 15 +----------+
  16. 16 1 row in set (0.11 sec)
  17. 17
  18. 18 mysql> create index xxx on s1(id,name,age,email);
  19. 19 Query OK, 0 rows affected (6.44 sec)
  20. 20 Records: 0 Duplicates: 0 Warnings: 0
  21. 21
  22. 22 mysql> select count(*) from s1 where id=3000;
  23. 23 +----------+
  24. 24 | count(*) |
  25. 25 +----------+
  26. 26 | 1 |
  27. 27 +----------+
  28. 28 1 row in set (0.00 sec)
  29. 29
  30. 30 mysql> select count(*) from s1 where name='egon';
  31. 31 +----------+
  32. 32 | count(*) |
  33. 33 +----------+
  34. 34 | 299999 |
  35. 35 +----------+
  36. 36 1 row in set (0.16 sec)
  37. 37
  38. 38 mysql> select count(*) from s1 where email='[email protected]';
  39. 39 +----------+
  40. 40 | count(*) |
  41. 41 +----------+
  42. 42 | 1 |
  43. 43 +----------+
  44. 44 1 row in set (0.15 sec)
  45. 45
  46. 46 mysql> select count(*) from s1 where id=1000 and email='[email protected]';
  47. 47 +----------+
  48. 48 | count(*) |
  49. 49 +----------+
  50. 50 | 0 |
  51. 51 +----------+
  52. 52 1 row in set (0.00 sec)
  53. 53
  54. 54 mysql> select count(*) from s1 where email='[email protected]' and id=3000;
  55. 55 +----------+
  56. 56 | count(*) |
  57. 57 +----------+
  58. 58 | 0 |
  59. 59 +----------+
  60. 60 1 row in set (0.00 sec)

索引無法命中的情況需要注意:

  1. - like '%xx'
  2. select * from tb1 where email like '%cn';
  3. - 使用函式
  4. select * from tb1 where reverse(email) = 'wupeiqi';
  5. - or
  6. select * from tb1 where nid = 1 or name = '[email protected]';
  7. 特別的:當or條件中有未建立索引的列才失效,以下會走索引
  8. select * from tb1 where nid = 1 or name = 'seven';
  9. select * from tb1 where nid = 1 or name = '[email protected]' and email = 'alex'
  10. - 型別不一致
  11. 如果列是字串型別,傳入條件是必須用引號引起來,不然...
  12. select * from tb1 where email = 999;
  13. 普通索引的不等於不會走索引
  14. - !=
  15. select * from tb1 where email != 'alex'
  16. 特別的:如果是主鍵,則還是會走索引
  17. select * from tb1 where nid != 123
  18. - >
  19. select * from tb1 where email > 'alex'
  20. 特別的:如果是主鍵或索引是整數型別,則還是會走索引
  21. select * from tb1 where nid > 123
  22. select * from tb1 where num > 123
  23. #排序條件為索引,則select欄位必須也是索引欄位,否則無法命中
  24. - order by
  25. select name from s1 order by email desc;
  26. 當根據索引排序時候,select查詢的欄位如果不是索引,則不走索引
  27. select email from s1 order by email desc;
  28. 特別的:如果對主鍵排序,則還是走索引:
  29. select * from tb1 order by nid desc;
  30. - 組合索引最左字首
  31. 如果組合索引為:(name,email)
  32. name and email -- 使用索引
  33. name -- 使用索引
  34. email -- 不使用索引
  35. - count(1)或count(列)代替count(*)在mysql中沒有差別了
  36. - create index xxxx on tb(title(19)) #text型別,必須制定長度
  1. - 避免使用select *
  2. - count(1)或count(列) 代替 count(*)
  3. - 建立表時儘量時 char 代替 varchar
  4. - 表的欄位順序固定長度的欄位優先
  5. - 組合索引代替多個單列索引(經常使用多個條件查詢時)
  6. - 儘量使用短索引
  7. - 使用連線(JOIN)來代替子查詢(Sub-Queries)
  8. - 連表時注意條件型別需一致
  9. - 索引雜湊值(重複少)不適合建索引,例:性別不適合

七、慢查詢優化的基本步驟

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