MySQL筆記(8)-- 索引類型


一、背景

  前面我們講了SQL分析索引優化都涉及到了索引,那么什么是索引,它的模型有什么,實現的機制是什么,今天我們來好好討論下。

二、索引的介紹

  索引就相當書的目錄,比如一本500頁的書,如果你想快速找到其中的某一個知識點,在不借助目錄的情況下,你得一點點慢慢的找,要找好一會兒。同樣,對於數據庫的表,而言,索引就是它的“目錄”,提高了數據查詢的效率。

  比如要運行下面的查詢:

select first_name from actor where actor_id=5;

  如果在actor_id列上建有索引,則MySQL將使用該索引找到actor_id為5的行,也就是說,MySQL先在索引上按值查找,然后返回所有包含該值的數據行。

三、索引的常見模型和實現機制

1.哈希索引

  哈希索引是一種以鍵-值(key-value)存儲數據的結構,只有精確匹配索引所有列的查詢才有效。對於每一行數據,存儲引擎都會對所有的索引列計算一個哈希碼,哈希碼是一個較小的值,並且不同鍵值的行計算出來的哈希碼也不一樣。哈希索引將所有的哈希碼存儲在索引中,同時在哈希表中保存指向每個數據行的指針。即我們只要輸入待查找的值即key,就可以找到其對應的值即value。

  哈希的思路很簡單,把值放在數組里,用一個哈希函數把key換算成一個確定的位置,然后把value放在數組的這個位置。

  當然不可避免地,多個key值經過哈希函數的換算,會出現同一個值的情況。處理這種情況的一種方法是,拉出一個鏈,即索引會以鏈表的方式存放多個記錄指針到同一個哈希條目中。

  在MySQL中,只有Memory引擎顯式支持哈希索引,也是 Memory引擎表的默認索引類型。

  下面來看一個例子:

create table testhash(
 fname varchar(50) not null,
 lname varchar(50) not null,
 key using HASH(fname)
)ENGINE=MEMORY;

  表中包含如下數據:

  假設索引使用假想的哈希函數f(),它返回下面的值:

  則哈希索引的數據結構如下:

  注意每個槽的編號是順序的,但是數據行不是。假如我們進行下面的查詢:

select lname from testhash where fname='Peter';

  MySQL先計算'Peter'的哈希值,並使用該值尋找對於的記錄指針。因為f('Peter')=8784,所以MySQL在索引中查找8784,可以找到指向第三行的指針,最后一步是比較第三行的值是否為'Peter',以確保就是要查找的行。

  因為索引自身只需存儲對應的哈希值,所以索引的結構十分緊湊,所以哈希索引的查找很快。但它還是有下面的缺點:

  • 哈希索引只包含哈希值和行指針,而不存儲字段值,所以不能使用索引中的值來避免讀取行。不過,訪問內存中的行速度很快,大部分情況這一點對性能的影響並不明顯;
  • 哈希索引數據並不是按照索引值順序存儲的,所以無法用於排序;
  • 哈希索引不支持部分索引列匹配查找,因為哈希索引是使用索引列的全部內容來計算哈希值的,例如在數據列(A,B)上建立哈希索引,如果查詢只有數據列A,則無法使用該索引;
  • 哈希索引只支持等值比較查詢,包括=、in()、<=>,不支持任何范圍查詢,比如where price>100,因為如果你進行范圍查找,就必須進行全表掃描;
  • 當出現哈希沖突時,存儲引擎必須遍歷鏈表中所有的行指針,逐行進行比較,直到找到所有符合條件的行。
  • 如果哈希沖突很多的話,一些索引維護操作的代價很高。比如當哈希沖突很多時,當從表中刪除一行時,存儲引擎需要遍歷對應哈希值的鏈表中的每一行,找到並刪除對應行的引用,沖突越多,代價越大。

  InnoDB引擎有一個特殊的功能叫“自適應哈希索引”,當InnoDB注意到某些索引列被使用得非常頻繁時,它會在內存中基於B-Tree索引之上再建立一個哈希索引,這樣就讓B-Tree索引也具有哈希索引的一些優點,比如快速的哈希查找。這是一個完全自動的、內部的行為,用戶無法控制或配置,不過如果有必要,完全可以關閉該功能。

2.有序數組索引

  有序數組索引在等值查詢和范圍查詢場景中的性能都非常優秀。比如我們維護一個身份證信息和姓名的表,根據身份證號查找名字,我們使用有序數組來實現的話,示意圖如下:

  這里我們假設身份證號沒有重復,這個數組就是按照身份證號遞增的順序保存的。這個時候如果你要查ID_card_n2對應的名字,用二分法就可以快速得到,這個時間復雜度是O(log(N))。

  同時很顯然,這個索引結構支持范圍查詢。如果你要查身份證號在[ID_card_X,ID_card_Y]區間的User,可以先用二分法找到ID_card_X(如果不存在ID_card_X,就找到大於ID_card_X的第一個User),然后向右遍歷,直到找到第一個大於ID_card_Y的身份證號,退出循環。

  如果僅僅看查詢效率,有序數組就是最好的數據結構了,但是,對於更新數據時就很麻煩了,你往中間插入一個記錄就必須挪動后面的所有記錄,成本太高了。所以有序數組索引只適用於靜態存儲引擎。

3.二叉搜索樹

 根據上面身份證號查名字的例子,使用二叉搜索樹來實現的示意圖:

  二叉搜索樹的特點是:每個節點的左兒子小於父節點,父節點又小於右節點。這樣如果你要查ID_card_n2的話,按照圖中的搜索順序就是按照UserA->UserC->UserF->User2這個路徑得到。這個時間復雜度是O(log(N))。當然為了維持O(log(N))的查詢復雜度,你就需要保持這棵樹是平衡二叉樹。為了做這個保證,更新的時間復雜度也是O(log(N))。

  樹可以有二叉,也可以有多叉。多叉樹就是每個節點有多個兒子,兒子之間的大小保證從左到右遞增。二叉樹是搜索效率最高的,但是實際上大多數的數據庫存儲卻並不使用二叉樹。其原因是,索引不止存在內存中,還要寫到磁盤上。

  你可以想象一下一棵 100 萬節點的平衡二叉樹,樹高 20。一次查詢可能需要訪問 20 個數據塊。在機械硬盤時代,從磁盤隨機讀一個數據塊需要 10 ms 左右的尋址時間。也就是說,對於一個 100 萬行的表,如果使用二叉樹來存儲,單獨訪問一個行可能需要 20 個 10 ms 的時間,這個查詢可真夠慢的。

  為了讓一個查詢盡量少地讀磁盤,就必須讓查詢過程訪問盡量少的數據塊。那么,我們就不應該使用二叉樹,而是要使用“N 叉”樹。這里,“N 叉”樹中的“N”取決於數據塊的大小。

  以 InnoDB 的一個整數字段索引為例,這個 N 差不多是 1200。這棵樹高是 4 的時候,就可以存 1200 的 3 次方個值,這已經 17 億了。考慮到樹根的數據塊總是在內存中的,一個 10 億行的表上一個整數字段的索引,查找一個值最多只需要訪問 3 次磁盤。其實,樹的第二層也有很大概率在內存中,那么訪問磁盤的平均次數就更少了。

  N 叉樹由於在讀寫上的性能優點,以及適配磁盤的訪問模式,已經被廣泛應用在數據庫引擎中了。

  在InnoDB中使用的是B+ 樹索引,數據都是存儲在 B+ 樹中的。表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱為索引組織表。

  每一個索引在 InnoDB 里面對應一棵 B+ 樹。

  假設,我們有一個主鍵列為 ID 的表,表中有字段 k,並且在 k 上有索引。下面是表創建語句:

create table T(
id int primary key, 
k int not null, 
name varchar(16),
index (k)
)engine=InnoDB;

   表中 R1~R5 的 (ID,k) 值分別為 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),兩棵樹的示例示意圖如下:

  從圖中不難看出,根據葉子節點的內容,索引類型分為主鍵索引和非主鍵索引。

  主鍵索引的葉子節點存的是整行數據,在 InnoDB 里,主鍵索引也被稱為聚簇索引(clustered index)。

  非主鍵索引的葉子節點內容是主鍵的值。在 InnoDB 里,非主鍵索引也被稱為二級索引(secondary index)。

  根據上面的索引結構說明,我們來討論一個問題:基於主鍵索引和普通索引的查詢有什么區別?

  • 如果語句是 select * from T where ID=500,即主鍵查詢方式,則只需要搜索 ID 這棵 B+ 樹;
  • 如果語句是 select * from T where k=5,即普通索引查詢方式,則需要先搜索 k 索引樹,得到 ID 的值為 500,再到 ID 索引樹搜索一次。這個過程稱為回表。

  也就是說,基於非主鍵索引的查詢需要多掃描一棵索引樹。因此,我們在應用中應該盡量使用主鍵查詢。

  那么額外討論下一個問題:在下面這個表 T 中,如果我執行 select * from T where k between 3 and 5,需要執行幾次樹的搜索操作,會掃描多少行?

  現在,我們一起來看看這條SQL查詢語句的執行流程:

  1. 在k索引樹上找到k=3的記錄,取得ID=300;
  2. 再到ID索引樹查到ID=300對應的R3;
  3. 在k索引樹取下一個值k=5,取得ID=500;
  4. 到ID索引樹查到ID=500對應的R4;
  5. 在k索引樹取下一個值k=6,不滿足條件,循環結束。

  在這個過程中,回到主鍵索引樹搜索的過程為回表。可以看到,這個查詢過程讀了k索引樹的3條記錄(步驟1、3和5),回表了兩次(步驟2和4)。

  在這個例子中,由於查詢結果所需要的數據只在主鍵索引上有,所以不得不回表。【可以通過索引優化策略來避免回表,比如覆蓋索引、聚簇索引等】

  B+ 樹為了維護索引有序性,在插入新值的時候需要做必要的維護。以上面這個圖為例,如果插入新的行 ID 值為 700,則只需要在 R5 的記錄后面插入一個新記錄。如果新插入的 ID 值為 400,就相對麻煩了,需要邏輯上挪動后面的數據,空出位置。

  而更糟的情況是,如果 R5 所在的數據頁已經滿了,根據 B+ 樹的算法,這時候需要申請一個新的數據頁,然后挪動部分數據過去。這個過程稱為頁分裂。

  在這種情況下,性能自然會受影響。除了性能外,頁分裂操作還影響數據頁的利用率。原本放在一個頁的數據,現在分到兩個頁中,整體空間利用率降低大約 50%。

  當然有分裂就有合並。當相鄰兩個頁由於刪除了數據,利用率很低之后,會將數據頁做合並。合並的過程,可以認為是分裂過程的逆過程。

  基於上面的索引維護過程說明,我們來討論一個案例:

你可能在一些建表規范里面見到過類似的描述,要求建表語句里一定要有自增主鍵。當然事無絕對,我們來分析一下哪些場景下應該使用自增主鍵,而哪些場景下不應該。

  自增主鍵是指自增列上定義的主鍵,在建表語句中一般是這么定義的: NOT NULL PRIMARY KEY AUTO_INCREMENT。

  插入新記錄的時候可以不指定 ID 的值,系統會獲取當前 ID 最大值加 1 作為下一條記錄的 ID 值。

  也就是說,自增主鍵的插入數據模式,正符合了我們前面提到的遞增插入的場景。每次插入一條新記錄,都是追加操作,都不涉及到挪動其他記錄,也不會觸發葉子節點的分裂。

  而有業務邏輯的字段做主鍵,則往往不容易保證有序插入,這樣寫數據成本相對較高。

  除了考慮性能外,我們還可以從存儲空間的角度來看。假設你的表中確實有一個唯一字段,比如字符串類型的身份證號,那應該用身份證號做主鍵,還是用自增字段做主鍵呢?

  由於每個非主鍵索引的葉子節點上都是主鍵的值。如果用身份證號做主鍵,那么每個二級索引的葉子節點占用約 20 個字節,而如果用整型做主鍵,則只要 4 個字節,如果是長整型(bigint)則是 8 個字節。

  顯然,主鍵長度越小,普通索引的葉子節點就越小,普通索引占用的空間也就越小。

  所以,從性能和存儲空間方面考量,自增主鍵往往是更合理的選擇。

  有沒有什么場景適合用業務字段直接做主鍵的呢?還是有的。比如,有些業務的場景需求是這樣的:

  • 只有一個索引;
  • 該索引必須是唯一索引。

  你一定看出來了,這就是典型的 KV 場景。

  由於沒有其他索引,所以也就不用考慮其他索引的葉子節點大小的問題。

  這時候我們就要優先考慮上一段提到的“盡量使用主鍵查詢”原則,直接將這個索引設置為主鍵,可以避免每次查詢需要搜索兩棵樹。

4.空間數據索引

  MyISAM表支持空間索引,可以用作地理數據存儲,該索引無須前綴查詢。空間索引會從所有維度來索引數據。查詢時,可以有效地使用任意維度來組合查詢。必須使用MySQL的GIS相關函數如MBRCONTAINS()等來維護數據。

5.全文索引

  全文索引是一種特殊類型的索引,它查找的是文本中的關鍵詞,而不是直接比較索引中的值。全文索引適用於MATCH AGAINST操作,而不是普通的WHERE條件操作。

  全文索引支持各種字符內容的搜索(包括char、varchar和text類型),也支持自然語言搜索和布爾搜索。

 四、討論

  對於上面例子中的 InnoDB 表 T,如果你要重建索引 k,你的兩個 SQL 語句可以這么寫:

alter table T drop index k;
alter table T add index(k);

   如果你要重建主鍵索引,也可以這么寫:

alter table T drop primary key;
alter table T add primary key(id);

   對於上面這兩個重建索引的作法,有什么不合適的,為什么,更好的方法是什么?

  答案:重建索引 k 的做法是合理的,可以達到省空間的目的。但是,重建主鍵的過程不合理。不論是刪除主鍵還是創建主鍵,都會將整個表重建。所以連着執行這兩個語句的話,第一個語句就白做了。這兩個語句,你可以用這個語句代替 : alter table T engine=InnoDB。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM