索引的功能
索引可以大幅增加數據庫的查詢的性能,在實際業務場景中,或多或少都會使用到。
但是索引是有如下 2 個代價的:
- 需要額外的磁盤空間來保存索引
- 對於插入、更新、刪除等操作由於更新索引會增加額外的開銷
因此索引比較適合用在讀多寫少的場景。
MySQL 的索引類型及實現
索引類型
共分為5類:
-
唯一索引:索引列中的值必須是唯一的,但是允許出現空值。這種索引一般用來保證數據的唯一性,比如保存賬戶信息的表,每個賬戶的id必須保證唯一,如果重復插入相同的賬戶id時會MySQL返回異常。
-
主鍵索引:特殊的唯一索引,不允許出現空值。
-
普通索引:與唯一索引不同,允許索引列中存在相同的值。例如學生的成績表,各個學科的分數是允許重復的,就可以使用普通索引。
-
聯合索引:由多個列共同組成的索引。一個表中含有多個單列的索引並不是聯合索引,聯合索引是對多個列字段按順序共同組成一個索引。應用聯合索引時需要注意最左原則,就是 Where 查詢條件中的字段必須與索引字段從左到右進行匹配。比如,一個用戶信息表,用姓名和年齡組成了聯合索引,如果查詢條件是姓名等於張三,那么滿足最左原則;如果查詢條件是年齡大於20,由於索引中最左的字段是姓名不是年齡,所以不能使用這個索引。
-
全文索引:MyISAM 引擎中實現了這個索引,在 5.6版 本后 InnoDB 引擎也支持了全文索引,並且在 5.7.6 版本后支持了中文索引。全文索引只能在 CHAR,VARCHAR,TEXT 類型字段上使用,底層使用倒排索引實現。要注意對於大數據量的表,生成全文索引會非常消耗時間也非常消耗磁盤空間。
索引實現
索引實現共分4種形式:
-
B+ 樹實現:B+ 樹比較適合用作 '>' 或 '<' 這樣的范圍查詢,是 MySQL 中最常使用的一種索引實現。
-
R-tree:是一種用於處理多維數據的數據結構,可以對地理數據進行空間索引。不過實際業務場景中使用的比較少。
-
Hash:是使用散列表來對數據進行索引,Hash 方式不像 Btree 那樣需要多次查詢才能定位到記錄,因此Hash索引的效率高於 B-tree ,但是不支持范圍查找和排序等功能.實際使用的也比較少。
-
FullText:就是我們前面提到的全文索引,是一種記錄關鍵字與對應文檔關系的倒排索引。
常見面試題
引入索引相關的常見的面試題,更客觀的學習索引相關的內容
問:數據庫中最常見的慢查詢優化方式是什么?
- 同學A:加索引。
問:為什么加索引能優化慢查詢?
- 同學A:...不知道
- 同學B:因為索引其實就是一種優化查詢的數據結構,比如 MySQL 中的索引是用 B+ 樹實現的,而 B+ 樹就是一種數據結構,可以優化查詢速度,可以利用索引快速查找數據,所以能優化查詢。
問:哪些數據結構可以提高查詢速度?(聽到這個問題就感覺此處有坑...)
- 同學B:哈希表、完全平衡二叉樹、B樹、B+樹等等。
問:那這些數據結構既然都能優化查詢速度,那 MySQL 為何選擇使用 B+ 樹?
- 同學B:...不知道(下面會解答)
問:有一個 titles 表,主鍵由 empno,title,fromdate 三個字段組成。那么以下幾個語句會用到索引嗎?
select * from employees.titles where emp_no=1 select * from employees.titles where title='1' select * from employees.titles where emp_no='1'andtitle=1 select * from employees.titles where title='1'andemp_no=1
問:哈希表、完全平衡二叉樹、B樹、B+樹都可以優化查詢,為何 MySQL 獨獨喜歡 B+ 樹?哈希表有什么特點?
- 答:假如有這么一張表(表名:sanguo):
現在對name字段建立哈希索引,注意字段值所對應的數組下標是哈希算法隨機算出來的,所以可能出現哈希沖突。那么對於這樣一個索引結構,現在來執行下面的 SQL 語句:
select * from sanguo where name='周瑜'
可以直接對 ‘周瑜’ 按哈希算法算出來一個數組下標,然后可以直接從數據中取出數據並拿到鎖對應那一行數據的地址,進而查詢那一行數據。那么如果現在執行下面的 SQL 語句:
select * from sanguo where name>'周瑜'
則無能為力,因為哈希表的特點就是可以快速的精確查詢,但是不支持范圍查詢。
如果用完全平衡二叉樹呢?
還是上面的表數據用完全平衡二叉樹表示如下圖(為了簡單,數據對應的地址就不畫在圖中了。):
圖中的每一個節點實際上應該有四部分:
- 左指針,指向左子樹
- 鍵值
- 鍵值所對應的數據的存儲地址
- 右指針,指向右子樹
另外需要提醒的是,二叉樹是有順序的,簡單的說就是“左邊的小於右邊的”假如我們現在來查找‘周瑜’,需要找2次(第一次曹操,第二次周瑜),比哈希表要多一次。而且由於完全平衡二叉樹是有序的,所以也是支持范圍查找的。
如果用B樹呢?
還是上面的表數據用B樹表示如下圖(為了簡單,數據對應的地址就不畫在圖中了。):
可以發現同樣的元素,B樹的表示要比完全平衡二叉樹要“矮”,原因在於B樹中的一個節點可以存儲多個元素。
如果用B+樹呢?
還是上面的表數據用B+樹表示如下圖(為了簡單,數據對應的地址就不畫在圖中了。)
我們可以發現同樣的元素,B+樹的表示要比B樹要“胖”,原因在於B+樹中的非葉子節點會冗余一份在葉子節點中,並且葉子節點之間用指針相連。
那么B+樹到底有什么優勢呢?
這里我們用“反證法”,假如我們現在就用完全平衡二叉樹作為索引的數據結構,我們來看一下有什么不妥的地方。實際上,索引也是很“大”的,因為索引也是存儲元素的,我們的一個表的數據行數越多,那么對應的索引文件其實也是會很大的,實際上也是需要存儲在磁盤中的,而不能全部都放在內存中,所以我們在考慮選用哪種數據結構時,我們可以換一個角度思考,哪個數據結構更適合從磁盤中讀取數據,或者哪個數據結構能夠提高磁盤的IO效率。回頭看一下完全平衡二叉樹,當我們需要查詢“張飛”時,需要以下步驟:
- 從磁盤中取出“曹操”到內存,CPU從內存取出數據進行筆記,“張飛”<“曹操”,取左子樹(產生了一次磁盤IO)
- 從磁盤中取出“周瑜”到內存,CPU從內存取出數據進行筆記,“張飛”>“周瑜”,取右子樹(產生了一次磁盤IO)
- 從磁盤中取出“孫權”到內存,CPU從內存取出數據進行筆記,“張飛”>“孫權”,取右子樹(產生了一次磁盤IO)
- 從磁盤中取出“黃忠”到內存,CPU從內存取出數據進行筆記,“張飛”=“張飛”,找到結果(產生了一次磁盤IO)
同理,回頭看一下B樹,我們發現只發送三次磁盤IO就可以找到“張飛”了,這就是B樹的優點:一個節點可以存儲多個元素,相對於完全平衡二叉樹所以整棵樹的高度就降低了,磁盤IO效率提高了。
而B+樹是B樹的升級版,只是把非葉子節點冗余一下,這么做的好處是為了提高范圍查找的效率。
到這里可以總結出來,Mysql選用B+樹這種數據結構作為索引,可以提高查詢索引時的磁盤IO效率,並且可以提高范圍查詢的效率,並且B+樹里的元素也是有序的。
那么,一個B+樹的節點中到底存多少個元素合適呢?
其實也可以換個角度來思考B+樹中一個節點到底多大合適?
答案是:B+樹中一個節點為一頁或頁的倍數最為合適。因為如果一個節點的大小小於1頁,那么讀取這個節點的時候其實也會讀出1頁,造成資源的浪費;如果一個節點的大小大於1頁,比如1.2頁,那么讀取這個節點的時候會讀出2頁,也會造成資源的浪費;所以為了不造成浪費,所以最后把一個節點的大小控制在1頁、2頁、3頁、4頁等倍數頁大小最為合適。
那么,Mysql中B+樹的一個節點大小為多大呢?
這個問題的答案是“1頁”,這里說的“頁”是Mysql自定義的單位(其實和操作系統類似),Mysql的Innodb引擎中一頁的默認大小是16k(如果操作系統中一頁大小是4k,那么Mysql中1頁=操作系統中4頁),可以使用命令SHOW GLOBALSTATUS like 'Innodbpagesize'; 查看。並且還可以告訴你的是,一個節點為1頁就夠了。
為什么一個節點為1頁(16k)就夠了?
解決這個問題,我們先來看一下Mysql中利用B+樹的具體實現。
Mysql中MyISAM和innodb使用B+樹
通常我們認為B+樹的非葉子節點不存儲數據,只有葉子節點才存儲數據;而B樹的非葉子和葉子節點都會存儲數據,會導致非葉子節點存儲的索引值會更少,樹的高度相對會比B+樹高,平均的I/O效率會比較低,所以使用B+樹作為索引的數據結構,再加上B+樹的葉子節點之間會有指針相連,也方便進行范圍查找。上圖的data區域兩個存儲引擎會有不同。
B+ 樹
MyISAM 中的 B+ 樹
MYISAM 中葉子節點的數據區域存儲的是數據記錄的地址,MyISAM 存儲引擎在使用索引查詢數據時,會先根據索引查找到數據地址,再根據地址查詢到具體的數據。並且主鍵索引和輔助索引沒有太多區別。
主鍵索引
輔助索引
InnoDB 中的 B+ 樹
InnoDB 中主鍵索引的葉子節點的數據區域存儲的是數據記錄,輔助索引存儲的是主鍵值
主鍵索引
輔助索引
InnoDB 中的主鍵索引和實際數據時綁定在一起的,也就是說 InnoDB 的一個表一定要有主鍵索引,如果一個表沒有手動建立主鍵索引,InnoDB 會查看有沒有唯一索引,如果有則選用唯一索引作為主鍵索引,如果連唯一索引也沒有,則會默認建立一個隱藏的主鍵索引(用戶不可見)。另外,InnoDB 的主鍵索引要比 MyISAM 的主鍵索引查詢效率要高(少一次磁盤 IO),並且比輔助索引也要高很多。所以,我們在使用 InnoDB 作為存儲引擎時,我們最好:
- 手動建立主鍵索引
- 盡量利用主鍵索引查詢
回到我們的問題:為什么一個節點為1頁(16k)就夠了?
對着上面 MySQL 中 InnoDB 中對 B+ 樹的實際應用(主要看主鍵索引),可以發現 B+ 樹中的一個節點存儲的內容是:
- § 非葉子節點:主鍵+指針
- § 葉子節點:數據
假設我們一行數據大小為1K,那么一頁就能存16條數據,也就是一個葉子節點能存16條數據;再看非葉子節點,假設主鍵ID為bigint類型,那么長度為8B,指針大小在Innodb源碼中為6B,一共就是14B,那么一頁里就可以存儲16K/14=1170個(主鍵+指針),那么一顆高度為2的B+樹能存儲的數據為:117016=18720條,一顆高度為3的B+樹能存儲的數據為:11701170*16=21902400(千萬級條)。所以在InnoDB中B+樹高度一般為1-3層,它就能滿足千萬級的數據存儲。在查找數據時一次頁的查找代表一次IO,所以通過主鍵索引查詢通常只需要1-3次IO操作即可查找到數據。所以也就回答了我們的問題,1頁=16k這么設置是比較合適的,是適用大多數的企業的,當然這個值是可以修改的,所以也能根據業務的時間情況進行調整。
最左前綴原則
我們模擬數據建立一個聯合索引 select *,concat(right(emp_no,1),"-",right(title,1),"-",right(from_date,2)) from employees.titles limit 10;
我們判斷一個查詢條件能不能用到索引,我們要分析這個查詢條件能不能利用某個索引縮小查詢范圍
對於 select from employees.titles where emp_no=1是能用到索引的,因為它能利用上面的索引所有查詢范圍,首先和第一個節點“4-r-01”比較,1<4,所以可以直接確定結果在左子樹,同理,依次按順序進行比較,逐步可以縮小查詢范圍。對於select from employees.titles where title='1'是不能用到索引的,因為它不能用到上面的所以,和第一節點進行比較時,沒有empno這個字段的值,不能確定到底該去左子樹還是右子樹繼續進行查詢。對於 select * from employees.titles where title='1' and emp_no=1是能用到索引,按照我們的上面的分析,先用title='1'這個條件和第一個節點進行比較,是沒有結果的,但是mysql會對這個sql進行優化,優化之后會將empno=1這個條件放到第一位,從而可以利用索引。