MySQL 索引機制

MySQL 原理篇

MySQL 索引機制

MySQL 體系結構及存儲引擎

MySQL 語句執行過程詳解

MySQL 執行計劃詳解

MySQL InnoDB 緩衝池

MySQL InnoDB 事務

MySQL InnoDB 鎖

MySQL InnoDB MVCC

MySQL InnoDB 實現高併發原理

MySQL InnoDB 快照讀在RR和RC下有何差異

索引是什麼?

索引是爲了加速對錶中數據行的檢索而創建的一種分散存儲的數據結構。

MySQL 的索引是硬盤級,索引數據是保存在硬盤上的,有部分數據可以放入緩存,後面的文章會描述到, InnerDB 有一個緩存池,緩存池的大小是可以通過配置文件配置。

我們通過下圖來看看 MySQL 的索引是怎麼工作的?

比如我們建了一張老師表,有 N 條數據,每條數據對應有一個磁盤地址,在沒有引入索引機制的情況下,我們要查一條姓名等於王五的數據,我們需要一條一條比對老師表的所有數據,當老師表的數據量比較多時,全表比對檢索的速度會非常慢。

這樣我們就必須引入索引機制,比如我們對 id 建了一個索引,要查一條 id 等於101的數據,我們可以通過索引的數據結構快速檢索出 id 等於101的磁盤地址,這樣就可以通過磁盤地址快速定位到表中的數據。

爲什麼要用索引? 

  1. 索引能極大的減少存儲引擎需要掃描的數據量
  1. 索引可以把隨機 IO 變成順序 IO
  1. 索引可以幫助我們在進行分組、排序等操作時,避免使用臨時表(後面一篇文章會講到)

爲什麼是 B+Tree 

MySQL 爲什麼要選用 B+Tree 的數據結構來作爲它的索引機制?接下里我們一步一步來推敲。

國外的數據結構模擬網站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html,可以通過該網站模擬各種數據結構的創建過程。

二叉查找樹(Binary Search Tree)

首先,要提高數據檢索的效率,我們第一個考慮的數據結構就是二叉查找樹。

 

二叉查找樹(英語:Binary Search Tree),也稱爲二叉搜索樹有序二叉樹(ordered binary tree)或排序二叉樹(sorted binary tree),是指一棵空樹或者具有下列性質的二叉樹:

  1. 若任意節點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值;
  2. 若任意節點的右子樹不空,則右子樹上所有節點的值均大於它的根節點的值;
  3. 任意節點的左、右子樹也分別爲二叉查找樹;
  4. 沒有鍵值相等的節點。

假設該二叉查找樹保存的是 id 索引,我們要檢索一條 id=8 的數據,它會一個節點一個節點比對,首先把根節點(第一條插入的數據就是根節點)加載到內存中比對,8比10要小,這個時候會找10左邊的節點,再把5加載到內存中比對,5比8要小,繼續找5右邊的節點,最後找到8這個節點。

二叉查找樹的問題

比如我們生成一個如下圖所示的二叉樹:

我們每次在插入新的節點的時候,數據都比上一個插入的節點大,這樣就形成了如上圖所示的鏈表的關係,當我們要去檢索一條 id 等於33的時候,它檢索的效率和全表掃描一樣了,沒有任何優化。

所以二叉查找樹檢索的效率取決於二叉查找樹數據的分佈,當二叉查找樹數據的分佈相當於一個線性鏈表的時候,它的數據檢索效率將會非常低。

平衡二叉查找樹(Balanced Binary Tree)

平衡二叉搜索樹(英語:Balanced Binary Tree)是一種結構平衡的二叉搜索樹,即葉節點高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。它能在 O(log n) 內完成插入、查找和刪除操作,最早被髮明的平衡二叉搜索樹爲AVL樹。

 

 

當平衡二叉查找樹發生節點變更時,基於每個節點的平衡因子,通過一次或多次左旋和右旋來達到樹新的平衡。

磁盤塊是一個節點在硬盤中的保存位置,每個節點有三塊數據內容:關鍵字、數據區、子節點引用。

  • 關鍵字:比如我們用 ID 作爲索引,那這裏保存的就是 ID 內容
  • 數據區:可以是數據的磁盤位置,可以是真正的數據內容
  • 子節點引用:比如跟節點10的 P1 引用指向的是5節點,P2 引用指向的是20節點,基於 P1 和 P2 就可以通過順序 IO 的方式加載子節點磁盤塊的內容到內容中

平衡二叉查找樹的問題

太深了

數據處的深度決定着它的 IO 操作次數,IO 操作耗時大。比如要加載 ID 等於8的數據,需要3次 IO 操作,6條數據就就需要3次 IO 操作,當我們的數據量達到千萬級別的時候,這顆平衡二叉查找樹會很高,數據檢索時的 IO 操作次數會更多。

太小了

每一個磁盤塊(節點/頁)保存的數據量太小了,沒有很好的利用操作磁盤 IO 的數據交換特性,也沒有利用好磁盤 IO 的預讀能力(空間局部性原理),從而帶來平凡的 IO 操作

  • 磁盤 IO 的數據交換特性:一次 IO 操作,交換的數據是4K(一頁)
  • 磁盤IO 的預讀能力:操作系統在做 IO 操作的時候,一次不止加載4K的數據量,它會利用空間局部性原理,加載更多的數據,MySQL 的一頁數據是16K。

多路平衡查找樹(B-Tree)

m 階 B-Tree 滿足以下條件:

  • 每個節點最多擁有 m 個子樹
  • 根節點至少有2個子樹
  • 分支節點至少擁有 m/2 顆子樹(除根節點和葉子節點外都是分支節點)
  • 所有葉子節點都在同一層、每個節點最多可以有 m-1 個key,並且以升序排列

下圖是一個2-3 B樹的結構

對比平衡二叉查找樹,同樣最多三次 IO 操作,2-3 B樹最多可以檢索到的數據量是22條數據,而平衡二叉查找樹最多可以檢索到的數據量是8條,當多路平衡二叉查找樹的路數越多,在 IO 操作次數相同的情況下,它能檢索的數據量越多。

爲什麼要合理的設置 MySQL 的字段長度? 

比如我們用 MySQL 頁的定義,一次 IO 操作會加載16k的數據,用 int 類型的 id 做索引,int 類型的數據暫用的空間大小是4bit,假設再冗餘4bit作爲引用、數據區的大小,這樣 MySQL 的一頁數據能存放 16*1024/8=2048 個關鍵字。

所以我們在定義 MySQL 字段類型的時候,儘量要精簡一些,字段的長度要設定的比較合理,能短則短,否則如果我們把這個字段作爲索引,它會影響 B-Tree 的路數,進而影響索引的檢索效率。

爲什麼 MySQL 的索引不宜建多?

多路平衡二叉查找樹爲了保證樹的絕對平衡,會在樹中節點變更的時候,採用分裂、合併的操作來維持樹的絕對平衡。

所以我們不能建過多的索引,會拖慢 MySQL 的新增、更新、刪除操作,因爲每次數據變更,都會對所有的索引數據進行節點的變更。

加強版多路平衡查找樹(B+Tree)

B+Tree 是在 B-Tree 基礎上的一種優化,使其更適合實現外存儲索引結構,InnoDB 存儲引擎就是用 B+Tree 實現其索引結構。

從 B-Tree 結構圖中可以看到每個節點中不僅包含數據的 key 值,還有 data 值。而每一個頁的存儲空間是有限的,如果 data 數據較大時將會導致每個節點(即一個頁)能存儲的 key 的數量很小,當存儲的數據量很大時同樣會導致B-Tree 的深度較大,增大查詢時的磁盤 I/O 次數,進而影響查詢效率。在 B+Tree 中,所有數據記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只存儲 key 值信息,這樣可以大大加大每個節點存儲的 key 值數量,降低 B+Tree 的高度。

B+Tree 相對於 B-Tree 有幾點不同:

  • B+Tree 節點關鍵字搜索採用閉合區間
  • B+Tree 非葉節點不保存數據相關信息,只保存關鍵字和子節點的引用
  • B+Tree 關鍵字對應的數據保存在葉子節點中
  • B+Tree 葉子節點是順序排列的,並且相鄰節點具有順序引用的關係

下面是一個 B+Tree 的數據結構圖:

爲什麼選用 B+Tree?

  1. B+Tree 是B-樹的變種(PLUS版)多路絕對平衡查找樹,它擁有B-樹的優勢。
  1. B+Tree 掃庫、表能力更強。

如果要從 B-Tree 中掃描表數據的話,基本要把整棵樹都要掃描一遍,因爲每個節點都存在數據區。B+Tree 就不需要掃描整棵樹,只需要掃描葉子節點就可以了。

  1. B+Tree 的磁盤讀寫能力更強。

B+Tree 的節點上是不保存數據的,那麼它保存的關鍵字就更多,這樣一次 IO 操作,加載的關鍵字就更多,所以它的磁盤讀寫能力更強。

  1. B+Tree 的排序能力更強。

B+Tree 的葉子節點天然就是順序存放的

  1. B+Tree 的查詢效率更加穩定。

比如我們從上圖的 B-Tree 中查詢一條 id 等於8的數據需要經過兩次 IO 操作,查詢一條 id 等於3的數據需要經過三次 IO 操作,而從上圖的 B+Tree 中只有葉子節點才保存數據,所以查詢任何數據都需要經過三次 IO 操作。 所以 B+Tree 的查詢效率更加穩定。

MySQL B+Tree 索引的體現形式

索引的實現是由搜索引擎來實現的,那麼在  MySQL 中比較主流的兩大引擎是:Myisam 和 InnoDB,存儲引擎是建立在表上面的,在創建表的時候可以指定所需要的搜索引擎。下面的建表語句中就指定了搜索引擎爲 InnoDB,不指定就使用默認的 InnoDB。

獲取硬盤中數據存儲的地址: 

SHOW VARIABLES LIKE 'datadir';

進入該地址,找到剛纔創建的庫 engine,該庫創建了兩張表,分別使用了兩種存儲引擎,Myisam 存儲引擎:user_myisam,InnoDB 存儲引擎:user_innodb,可以看到如下所示的文件內容:

MySQL 中 B+Tree 索引體現形式 - MyISAM 

MyISAM 的數據和索引是分別存儲的,在創建好表結構並且指定搜索引擎爲 MyISAM 之後,會在數據目錄生成3個文件,分別是 table_name.frm(表結構文件),table_name.MYD(數據保存文件),table_name.MYI(索引保存文件)。 

ID 列索引

例如上圖的 teacher 表,兩個文件分別保存了數據及索引,由於 B+Tree 中只有葉子節點保存數據區,在 MyISAM 中,數據區中保存的是數據的引用地址,比如說 id 爲101的數據信息所保存到物理磁盤地址爲 0x123456,當掃描到這個指針位置,就可以通過這個磁盤指針將數據加載出來。

ID 列索引、name 列索引

在 MyISAM 中,name 索引和 ID 索引是一樣的,葉子節點也是保存它指向的磁盤位置指針,他們是平級的。

MySQL 中 B+Tree 索引體現形式 - InnoDB

InnoDB 的數據和索引是存儲在一起的,在創建好表結構並且指定搜索引擎爲 InnoDB 之後,會在數據目錄生成2個文件,分別是 table_name.frm(表結構文件),table_name.idb(數據與索引保存文件)。

InnoDB B+Tree 的體現是以主鍵爲索引來組織數據的存儲,當我們沒有顯示的建立主鍵索引的時候,搜索引擎會隱式的生成一個6位的 int 型的索引來作爲它的主鍵索引以組織數據的存儲。

 

數據庫錶行中數據的物理順序與鍵值的邏輯(索引)順序相同,InnoDB 就是以聚集索引來組織數據的存儲的,在葉子節點上,保存了數據的所有信息。如果這個時候建立了 name 字段的索引,它是如何組織數據的,如下圖所示:

會產生一個輔助索引,即 name 字段的索引,而此刻葉子節點上所保存的數據爲 聚集索引(ID索引)的關鍵字的值,基於輔助索引找到 ID 索引的值,再通過 ID 索引區獲取最終的數據。

這個做法的好處是在於產生數據遷移的時候只要 ID 沒發生變法,那麼輔助索引不需要重新生成,不這麼做的話,如果存儲的是磁盤地址的話,在數據遷移後所有輔助索引都需要重新生成。

索引知識

列的離散性 count(distinct col) : count(col)

比如下面的數據,找出離散性最好的列?可以發現 name 的離散性是最好的。

列的離散性的計算公式是:count(distinct col) : count(col),比例越大,離散性就越好

結論:列的離散性越好,列的選擇性就越好。

如何理解列的選擇性呢?比如對性別字段做索引,假設男爲1,女爲0,就會生成如下索引樹:

這個時候要搜索女的數據,從根節點出發,發現可以選擇的線路太多了,優化器覺得既然要搜索這麼多數據,還不如全表掃描,不利於 MySQL 數據檢索的性能。

最左匹配原則

對索引中關鍵字進行計算(比對),一定是從左往右依次進行,且不可跳過。

這裏需要說明一點,字符串也可以進行大小比對,在我們創建庫、表的時候需要選擇字符集及排序規則,都是有用的,它會影響字符串的排序。

比如一棵 B+tree 中的根節點爲一個字符串 abc ,那麼我現在要搜索一個爲 adc 的索引關鍵字的數據,根節點 abc 的 ASCII 碼爲 97 98 99,而 adc 的爲 97 100 99,那麼和3個數字會逐一比對,且100>98,接下去一定會走右子樹。

聯合索引 

  • 單列索引:節點中關鍵字[name]
  • 聯合索引:節點中關鍵字[name,phoneNum]

單列索引是特殊的聯合索引

聯合索引列選擇原則:

  1. 經常用的列優先【最左匹配原則】
  2. 選擇性(離散度)高的列優先【離散度高原則】
  3. 寬度小的列優先【最少空間原則】
案例一

比如公司經排查發現最常用的 SQL 語句,如何在 users 表上建立索引?

select * from users where name = ? ;
select * from users where name = ? and phoneNum = ?;

機靈的李二狗的解決方案:

create index idx_name on users(name);
-- 上面一個是冗餘索引,不需要建立,根據最左原則,下面這個聯合索引適用於以上2個sql語句
create index idx_name_phoneNum on users(name, phoneNum);
案例二

登錄業務需要執行如下 SQL,如何在 users 表上建立索引?

select uid, login_time from t_user where login_name=? and passwd=?

可以建立 (login_name, passwd) 的聯合索引。爲什麼呢?

聯合索引能夠滿足最左側查詢需求,例如 (a, b, c) 三列的聯合索引,能夠加速 a | (a, b) | (a, b, c) 三組查詢需求。這也就是爲何不建立 (passwd, login_name) 這樣聯合索引的原因,業務上幾乎沒有 passwd 的單條件查詢需求,而有很多 login_name 的單條件查詢需求。 

如下查詢能否命中 (login_name, passwd) 這個聯合索引?

select uid, login_time from t_user where passwd=? and login_name=?

答案是可以,最左側查詢需求,並不是指 SQL 語句的寫法必須滿足索引的順序(這是很多朋友的誤解),應該是 MySQL 對傳入的 SQL 執行了優化。

覆蓋索引

如果查詢列可通過索引節點中的關鍵字直接返回,則該索引稱之爲覆蓋索引。

覆蓋索引可減少數據庫 IO,將隨機 IO 變爲順序 IO,可提高查詢性能。

總結及驗證 

  1. 索引列不允許爲空
  2. 索引列的數據長度能少則少
  3. 索引一定不是越多越好,越全越好,一定是建合適的
  4. 匹配列前綴可用到索引 like 9999%,like %9999%、like %9999 用不到索引
  5. where 條件中 not in 和 <> 操作無法使用索引
  6. 匹配範圍值,order by 也可用到索引
  7. 多用指定列查詢,只返回自己想到的數據列,少用 select *
  8. 聯合索引中如果不是按照索引最左列開始查找,無法使用索引
  9. 聯合索引中精確匹配最左前列並範圍匹配另外一列可以用到索引
  10. 聯合索引中如果查詢中有某個列的範圍查詢,則其右邊的所有列都無法使用索引
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章