聚簇索引並不是一種單獨的索引類型,而是一種數據存儲方式(不是數據結構,而是存儲結構),具體細節依賴於其實現方式,但innodb的聚簇索引實際上是在同一個結構中保存了btree索引和數據行。
當表有索引時,它的數據行實際上存放在索引的葉子頁中,屬於聚簇表示數據行和相鄰的鍵值緊湊地存儲在一起,因為無法同時把數據行存放在兩個不同的地方,所以一個表只能有一個聚簇索引。因為是存儲引擎負責實現索引,因此不是所有的存儲引擎都支持聚簇索引。下面主要介紹innodb,但下面討論的原理對於任何支持聚簇索引的引擎都適用:
葉子頁包含了行的全部數據,但是節點頁只包含了索引列(或者可以說非葉子節點的節點頁包含的是索引值的索引,因為這些節點頁包含的值是從索引列中提取出來的)。
innodb將通過主鍵聚集數據,如果沒有定義主鍵,Innodb會選擇第一個非空的唯一索引代替,如果沒有非空唯一索引,Innodb會隱式定義一個6字節的rowid主鍵來作為聚集索引。innodb只聚集在同一個頁面中的記錄,包含相鄰鍵值的頁面可能會相距甚遠。
要注意:聚簇主鍵可能對性能有幫助,但也可能導致嚴重的性能問題,尤其是將表的存儲引擎從innodb轉換成其他引擎的時候。
聚集的數據有一些重要的優點:
A:可以把相關數據保存在一起,如:實現電子郵箱時,可以根據用戶ID來聚集數據,這樣只需要從磁盤讀取少量的數據頁就能獲取某個用戶全部郵件,如果沒有使用聚集索引,則每封郵件都可能導致一次磁盤IO
B:數據訪問更快,聚集索引將索引和數據保存在同一個btree中,因此從聚集索引中獲取數據通常比在非聚集索引中查找要快
C:使用覆蓋索引掃描的查詢可以直接使用頁節點中的主鍵值
聚集索引的缺點:
A:聚簇數據最大限度地提高了IO密集型應用的性能,但如果數據全部放在內存中,則訪問的順序就沒有那么重要了,聚集索引也沒有什么優勢了
B:插入速度嚴重依賴於插入順序,按照主鍵的順序插入是加載數據到innodb表中速度最快的方式,但如果不是按照主鍵順序加載數據,那么在加載完成后最好使用optimize table命令重新組織一下表
C:更新聚集索引列的代價很高,因為會強制innodb將每個被更新的行移動到新的位置
D:基於聚集索引的表在插入新行,或者主鍵被更新導致需要移動行的時候,可能面臨頁分裂的問題,當行的主鍵值要求必須將這一行插入到某個已滿的頁中時,存儲引擎會將該頁分裂成兩個頁面來容納該行,這就是一次頁分裂操作,頁分裂會導致表占用更多的磁盤空間
E:聚集索引可能導致全表掃描變慢,尤其是行比較稀疏,或者由於頁分裂導致數據存儲不連續的時候
F:二級索引可能比想象的更大,因為在二級索引的葉子節點包含了引用行的主鍵列。
G:二級索引訪問需要兩次索引查找,而不是一次
因為二級索引葉子節點中保存的不是指向行的物理位置的指針,而是行的主鍵值。這意味着通過二級索引查找行,存儲引擎需要找到二級索引的葉子節點獲得對應的主鍵值,然后根據這個主鍵值去聚集索引中查找對應的行,這里做了重復的工作,兩次btree查找而不是一次,對於innodb,自適應哈希索引能減少這樣的重復工作。
innodb和myisam物理存儲的數據分布對比:
myisam:
是按照數據插入的順序存儲在磁盤上的,myisam中的主鍵索引和二級索引在結構上並沒有什么不同,主鍵索引就是一個名為primary的唯一非空索引。
innodb:
因為innodb支持聚集索引,所以使用非常不同的方式存儲同樣的數據,innodb聚集索引包含了整個表的數據,而不是只有索引,因為在Innodb中,聚集索引就是表,所以不像myisam那樣需要獨立的行存儲。聚集索引的每一個葉子節點都包含了主鍵值,事務ID,用於事務和MVCC的回滾指針以及所有剩余列的值,如果主鍵是一個列前綴索引,innodb也會包含完整的主鍵列和剩下的列的值。
還有一點和myisam不同的是,innodb的二級索引和聚集索引很不同,innodb二級索引的葉子節點中存儲的不是行指針,而是主鍵值,並以此作為指向行的指針,這樣的策略減少了當出現行移動或者數據頁的分裂時二級索引的維護工作,使用主鍵值當做指針會讓二級索引占用更多的空間,換來的好處是,innodb在移動行時無須更新二級索引中的這個指針。
在innodb表中按主鍵順序插入行,如果正在使用Innodb表並且沒有什么數據需要聚集,那么可以定義一個代理鍵作為主鍵,這種主鍵的數據應該和應用無關,最簡單的方法是使用auto_increment自增列,這樣可以保證數據行是按順序插入的,對於根據主鍵做關聯操作的性能也會更好。
不要使用UUID來作為聚集索引,否則性能會很糟糕,因為它使得聚集索引的插入變得完全隨機,使得數據沒有任何聚集特性。因為UUID作為主鍵插入行不僅花費的時間更長,而且索引也更大,這一方面是因為主鍵字段變長了,另外一方面毫無疑問是由於頁分裂導致時間變長和碎片導致的索引變大。因為主鍵的值是順序的,所以Innodb把每一條記錄都存儲在上一條記錄的后面,當達到頁的最大填充因子時(innodb默認的最大填充因子是頁大小的十六分之十五,留出部分空間用於以后修改),下一條記錄就會寫入新的頁中,一旦數據按照這種順序的方式加載,主鍵頁就會近似被順序的記錄填滿,這也正是所期望的結果(然而,二級索引頁可能是不一樣的)。
在UUID主鍵下,因為新插入行的主鍵值不一定比前面的大,所以innodb無法簡單地總是把新行插入到索引的最后,而是需要為新的行尋找合適的位置,通常是已有數據的中間位置,並且分配新的空間,這會增加很多額外的工作,並導致數據分布不夠優化,下面是使用UUID作為主鍵的一些缺點:
A:寫入的目標頁可能已經刷到磁盤上並從緩存中移除,或者是還沒有被加載到緩存中,innodb在插入前不得不先找到並從磁盤讀取目標頁到內存中,這將導致大量的隨機IO
B:因為寫入是亂序的,innodb不得不頻繁地做頁分裂操作,以便為新的行分配空間,頁分裂會導致移動大量數據,一次插入最少需要修改三個頁不是一個頁
C:由於頻繁的頁分裂,頁會變得稀疏並被不規則地填充,所以最終數據會有碎片
把這些隨機值載入到聚集索引之后,也許需要做一次optimize table來重建表並優化頁的填充。使用innodb時應該盡可能地按照主鍵順序插入數據,並且盡可能地使用簡單增加的聚簇鍵的值來插入新行。
注:順序的主鍵什么時候會造成更壞的結果?
對於高並發工作負載,在Innodb中按主鍵順序插入可能會造成明顯的爭用,主鍵的上界會稱為熱點,因為所有的插入都發生在這里,所以並發插入可能導致間隙鎖爭用,另一個熱點可能是auto_increment鎖機制,如果遇到這個問題,則可能需要重新設計表或者應用,或者更改innodb_autoinc_lock_mode配置。