《高性能MySQL》讀后感——聚簇索引

聚簇索引并不是一種單獨的索引類型,而是一種數據存儲方式。比如,InnoDB的聚簇索引使用B+Tree的數據結構存儲索引和數據。聚簇索引與非聚簇對比如下圖。


圖0 聚簇和非聚簇表形象對比圖

當表有聚簇索引時,它的數據行實際上存放在索引的葉子頁(leaf page)中。因為無法同時把數據行存放在兩個不同的地方,所以一個表只能有一個聚簇索引(不過,覆蓋索引可以模擬多個聚簇索引的情況)。

  • 術語“聚簇”表示數據行和相鄰的鍵值緊湊地存儲在一起。
  • 聚簇索引的二級索引:葉子節點不會保存引用的行的物理位置,而是保存行的主鍵值。

對于聚簇索引的存儲引擎,數據的物理存放順序與索引順序是一致的,即:只要索引是相鄰的,那么對應的數據一定也是相鄰地存放在磁盤上的,如果主鍵不是自增id,可以想象,它會干些什么,不斷地調整數據的物理地址、分頁,當然也有其他一些措施來減少這些操作,但卻無法徹底避免。但,如果是自增的,那就簡單了,它只需要一頁一頁地寫,索引結構相對緊湊,磁盤碎片少,效率也高。

對于非聚簇索引的存儲引擎,表數據存儲順序與索引順序無關,葉結點包含索引字段值及指向數據頁數據行的邏輯指針,其行數量與數據表行數據量一致。

下圖1展示了聚簇索引的記錄是如何存放的。注意到,節點頁只包含了索引列,葉子頁包含行的全部數據,這是B+Tree的數據結構。在這個案例中,索引列包含的是整數值。

圖1 聚簇索引的數據分布

InnoDB將通過主鍵聚集數據,圖1中的“被索引的列”就是主鍵列。如果沒有定義主鍵,InnoDB會選擇一個唯一的非空索引代替。如果沒有這樣的索引,InnoDB會隱式定義一個主鍵來作為聚簇索引。InnoDB只聚集在同一個頁面中的記錄,包含相鄰鍵值的頁面可能會相距甚遠。

聚簇主鍵可能對性能有幫助,但也可能導致嚴重的性能問題。所以需要仔細地考慮聚簇索引,尤其是將表的存儲引擎從InnoDB改成其他引擎的時候(反過來也一樣)。

聚簇的數據有一些重要的優點:

  • 可以把相關數據保存在一起。例如實現電子郵箱時,可以根據用戶ID來聚集數據,這樣只需要從磁盤讀取少數的數據頁就能獲取某個用戶的全部郵件。如果沒有聚簇索引,則每封郵件都可能多一次磁盤IO。
  • 數據訪問更快。聚簇索引將索引和數據保存在同一個B+Tree中,因此從聚簇索引中獲取數據通常比在非聚簇索引中查找要快。
  • 使用覆蓋索引掃描的查詢可以直接使用頁節點中的主鍵值。

如果設計表和查詢時能充分利用上面的優點,就能極大地提升性能。但是,聚簇索引也有一些缺點:

  • 聚簇數據最大限度地提高了IO密集型應用的性能,但如果數據全部放在內存中,則訪問的順序就沒那么重要了,聚簇索引也就沒什么優勢了。
  • 插入速度嚴重依賴于插入順序。按照主要的順序插入是加載數據到InnoDB表中速度最快的方式。但如果不是按照主鍵順序加載數據,那么在加載完成后最好使用optimize table命令重新組織一下表。
  • 更新聚簇索引列的代價很高,因為會強制InnoDB將每個被更新的行移動到新的位置。
  • 基于聚簇索引的表插入新行,或者主鍵被更新導致需要移動行的時候,可能面臨”頁分裂(page split)“的問題。當行的主鍵值要求必須將這一行插入到某個已滿的頁中時,存儲引擎會將該頁分裂成兩個頁面來容納該行,這就是一次分裂操作。頁分裂會導致表占用更多的磁盤空間。
  • 聚簇索引可能導致全表掃描變慢,尤其是行比較稀疏,或者由于頁分裂導致數據存儲不連續的時候。
  • 二級索引(非聚簇索引)可能比想象的要更大,因為在二給索引的葉子節點包含了引用行的主鍵列。
  • 二級索引訪問需要兩次索引查找,而不是一次。

最后一點可能讓人有些疑惑,為什么二級索引需要兩次索引查找?答案在于二級索引中保存的”行指針“的實質。要記住,二級索引葉子節點保存的不是指向行的物理位置的指針,而是行的主鍵值。

這意味著通過二級索引查找行,存儲引擎需要找到二級索引的葉子節點獲得對應的主鍵值,然后根據這個值去聚簇索引中查找到對應的行。這里做了重復的工作:兩次B-Tree查找而不是一次。對于 InnoDB,自適應哈希索引能夠減少這樣的重復工作。

InnoDB和MyISAM的數據分布對比

聚簇索引和非聚簇索引的數據分布有區別,以及對應的主要索引和二級索引的數據分布也有區別,通常會讓人感到困擾和意外。來看看InnoDB和MyISAM是如何存儲下面這個表的:

create table layout_test(
    col1 int not null,
    col2 int not null,
    primary key(col1),
    key(col2)
);

假設該表的主鍵取值為1~10000,按照隨機順序播放并使用optimize table命令做了優化。換句話說,數據在磁盤上的存儲方式已經最優,但行的順序是隨機的。列col2的值是從1~100之間隨機賦值,所以有很多重復的值。

MyISAM的數據布局

MyISAM的B+Tree的葉子節點上的data,并不是數據本身,而是數據存放的地址。MyISAM按照數據插入的順序存儲在磁盤上,如下圖2所示,左邊為行號(row number),從0開始。因為元組的大小固定,所以MyISAM很容易的從表的開始位置找到某一字節的位置。

圖2 MyISAM表layout_test的數據分布

MyISAM建立的primary key的索引結構大致如圖3和圖4所示。MyISAM不支持聚簇索引,索引中每一個葉子節點僅僅包含行號(row number),且葉子節點按照col1的順序存儲。MyISAM是按列值與行號來組織索引的。

圖3 MyISAM表layout_test的主鍵分布

在圖4中,表一共有三列,假設以Col1為主鍵,可以看出,MyISAM的葉子節點中保存的實際上是指向存放數據的物理塊的指針。從MYISAM存儲的物理文件看出,MyISAM引擎的索引文件(.MYI)和數據文件(.MYD)是相互獨立的,索引文件僅僅保存數據記錄的地址。

圖4 MyISAM主鍵索引的分布

下圖5顯示col2 的索引結構,與圖3的primary key對比,索引中每一個葉子節點僅僅包含行號(row number),且葉子節點按照col2的順序存儲。在圖6中,在Col2建立一個輔助索引,與圖4對比,MyISAM的葉子節點也是保存指向存放數據的物理塊的指針。

所以,結論是MyISAM的primary key和輔助索引沒有任何區別。只是Primary key要求key唯一非空,而輔助索引的key可以重復。

圖5 MyISAM表layout_test的col2列索引的分布
圖6 MyISAM輔助索引的分布

因此,MyISAM中索引檢索的算法為首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,則取出其data域的值,然后以data域的值為地址,讀取相應數據記錄。

InnoDB的數據布局

MyISAM索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址。而在InnoDB中,表數據文件本身就是按B+Tree組織的一個索引結構,這棵樹的葉節點data域保存了完整的數據記錄。這個索引的key是數據表的主鍵,因此InnoDB表數據文件本身就是主索引。

圖7和與圖3 MyISAM對比看出,InnoDB索引的每一個葉子節點都包含了主鍵值、事務ID、用于事務和MVCC的回流指針以及所有的剩余列(在這個例子中是col2)。如果主鍵是一個列前綴索引,InnoDB也會包含完整的主鍵列和剩下的其他列。這種索引叫做聚簇索引

圖8可以看到葉節點包含了完整的數據記錄。

因為InnoDB的數據文件本身要按主鍵聚集,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定,則MySQL系統會自動選擇一個可以唯一標識數據記錄的列作為主鍵,如果不存在這種列,則MySQL自動為InnoDB表生成一個隱含字段作為主鍵,這個字段長度為6個字節,類型為長整形。

圖7 InnoDB表layout_test的主鍵分布
圖8 InnoDB主鍵索引的分布

還有一點和MyISAM的不同是,InnoDB的二級索引和聚簇索引很不相同。InnoDB二級索引的葉子節點中存儲的不是”行指針“,而是主鍵值,并以此作為指向行的“指針”。這樣的策略減少了當出現行移動或者數據頁分裂時二級索引的維護工作。使用主鍵值當作指針會讓二級索引占用更多的空間,換來的好處是,InnoDB在移動行時無須更新二級索引中的這個“指針”。

下圖9展示了示例表的二級索引col2索引。每一個葉子節點都包含了索引列(這里是col2),緊接著是主鍵值(col1)。圖10展示了InnoDB的所有輔助索引都引用主鍵作為data域。

圖9 InnoDB表layout_test的col2列索引的分布
圖10 InnoDB輔助索引的分布

InnoDB 表是基于聚簇索引建立的。因此InnoDB 的索引能提供一種非常快速的主鍵查找性能。不過,它的輔助索引(Secondary Index, 也就是非主鍵索引)也會包含主鍵列,所以,如果主鍵定義的比較大,其他索引也將很大。如果想在表上定義 、很多索引,則爭取盡量把主鍵定義得小一些。InnoDB 不會壓縮索引。

InnoDB與MyIASM索引和數據布局對比

圖7描述InnoDB和MyISAM如何存放表的抽象圖。對比InnoDB和MyISAM的主鍵索引與二級索引。

InnoDB的的二級索引的葉子節點存放的是KEY字段加主鍵值。因此,通過二級索引查詢首先查到是主鍵值,然后InnoDB再根據查到的主鍵值通過主鍵索引找到相應的數據塊。而MyISAM的二級索引葉子節點存放的還是列值與行號的組合,葉子節點中保存的是數據的物理地址。所以可以看出MYISAM的主鍵索引和二級索引沒有任何區別,主鍵索引僅僅只是一個叫做PRIMARY的唯一、非空的索引,且MYISAM引擎中可以不設主鍵。

圖7 聚簇和非聚簇表對比圖

為了更形象說明這兩種索引的區別,我們假想一個表如下圖8存儲了4行數據。其中id作為主索引,name作為輔助索引。圖示清晰的顯示了聚簇索引和非聚簇索引的差異。

對于聚簇索引存儲來說,行數據和主鍵B+樹存儲在一起,輔助鍵B+樹只存儲輔助鍵和主鍵,主鍵和非主鍵B+樹幾乎是兩種類型的樹。對于非聚簇索引存儲來說,主鍵B+樹在葉子節點存儲指向真正數據行的指針,而非主鍵。

InnoDB使用的是聚簇索引,將主鍵組織到一棵B+樹中,而行數據就儲存在葉子節點上,若使用"where id = 14"這樣的條件查找主鍵,則按照B+樹的檢索算法即可查找到對應的葉節點,之后獲得行數據。若對Name列進行條件搜索,則需要兩個步驟:第一步在輔助索引B+樹中檢索Name,到達其葉子節點獲取對應的主鍵。第二步使用主鍵在主索引B+樹種再執行一次B+樹檢索操作,最終到達葉子節點即可獲取整行數據。

MyISM使用的是非聚簇索引,非聚簇索引的兩棵B+樹看上去沒什么不同,節點的結構完全一致只是存儲的內容不同而已,主鍵索引B+樹的節點存儲了主鍵,輔助鍵索引B+樹存儲了輔助鍵。表數據存儲在獨立的地方,這兩顆B+樹的葉子節點都使用一個地址指向真正的表數據,對于表數據來說,這兩個鍵沒有任何差別。由于索引樹是獨立的,通過輔助鍵檢索無需訪問主鍵的索引樹。

圖8 聚簇和非聚簇表形象對比圖

我們重點關注聚簇索引,看上去聚簇索引的效率明顯要低于非聚簇索引,因為每次使用輔助索引檢索都要經過兩次B+樹查找,這不是多此一舉嗎?聚簇索引的優勢在哪?
1 由于行數據和葉子節點存儲在一起,這樣主鍵和行數據是一起被載入內存的,找到葉子節點就可以立刻將行數據返回了,如果按照主鍵Id來組織數據,獲得數據更快。
2 輔助索引使用主鍵作為"指針" 而不是使用地址值作為指針的好處是,減少了當出現行移動或者數據頁分裂時輔助索引的維護工作,使用主鍵值當作指針會讓輔助索引占用更多的空間,換來的好處是InnoDB在移動行時無須更新輔助索引中的這個"指針"。也就是說行的位置(實現中通過16K的Page來定位,后面會涉及)會隨著數據庫里數據的修改而發生變化(前面的B+樹節點分裂以及Page的分裂),使用聚簇索引就可以保證不管這個主鍵B+樹的節點如何變化,輔助索引樹都不受影響。

在InnoDB表中按主鍵順序插入行

如果正在使用InnoDB表并且沒有什么數據需要聚集,那么可以定義一個代理鍵作為主鍵,這種主鍵的數據應該和應用無關,最簡單的方法是使用auto_increment自增列。這樣可以保證數據行是按照順序寫入,對于根據主鍵做關聯操作的性能也會更好。

最好避免隨機的聚簇索引,特別對于I/O密集型的應用。例如,從性能的角度考慮,使用UUID作為聚簇索引會很糟糕:它使得聚簇索引的插入變得完全隨機,這是最壞的情況,使得數據沒有任何聚集特性。

為了演示這一點,我們做如下兩個基準測試。第一個使用整數ID插入shopinfo表,整數ID自增且為主鍵:

CREATE TABLE `shopinfo` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '記錄ID',
  `shop_id` int(11) NOT NULL COMMENT '商店ID',
  `goods_id` int(11) NOT NULL COMMENT '物品ID',
  `pay_type` int(11) NOT NULL COMMENT '支付方式',
  `price` decimal(10,2) NOT NULL COMMENT '物品價格',
  `comment` varchar(4000) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `shop_id` (`shop_id`,`goods_id`),
  KEY `price` (`price`),
  KEY `pay_type` (`pay_type`),
  KEY `idx_comment` (`comment`(255))
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商店物品表';

第二個例子是shopinfo_uuid表,除了主鍵改為UUID,其余和前面的shopinfo表完全相同。

CREATE TABLE `shopinfo_uuid` (
  `uuid` varchar(36) NOT NULL,
  `shop_id` int(11) NOT NULL COMMENT '商店ID',
  `goods_id` int(11) NOT NULL COMMENT '物品ID',
  `pay_type` int(11) NOT NULL COMMENT '支付方式',
  `price` decimal(10,2) NOT NULL COMMENT '物品價格',
  `comment` varchar(4000) DEFAULT NULL,
  PRIMARY KEY (`uuid`),
  UNIQUE KEY `shop_id` (`shop_id`,`goods_id`),
  KEY `price` (`price`),
  KEY `pay_type` (`pay_type`),
  KEY `idx_comment` (`comment`(255))
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商店物品表';

我們先向這兩個表各插入1萬條記錄。然后再向這兩個表繼續插入9萬條記錄,觀察這兩個表的插入耗時和表索引大小,下表對測試結果進行比較。其中,查看指定庫的指定表shopinfo的索引大小SQL語句:
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024), 2), ' MB') AS 'Total Index Size' FROM TABLES WHERE table_schema = 'study' and table_name = 'shopinfo';

表名 行數 時間 索引大小(MB)
shopinfo 10000 0.755s 4.08
shopinfo_uuid 10000 1.699s 8.16
shopinfo 90000 8.014s 29.47
shopinfo_uuid 90000 46.111s 60.58

通過測試,插入同樣的行數和內容(除主鍵內容),向UUID主鍵插入行不僅花費的時間更長,而且索引占用的空間也更大。這一方面是由于主鍵字段更長,另一方面毫無疑問是由于頁分裂和碎片導致的。

如圖9所示,由于主鍵的值是順序的,InnoDB把每一條記錄都存儲在上一條記錄的后面。當達到頁的最大填充因子時(InnoDB默認的最大填充因子是頁大小的15/16,留出的部分空間用于以后修改),下一條記錄就會寫入新的頁中。一旦數據按照這樣順序的方式加載,主鍵頁就會近似于被順序的記錄填滿,這也是所期望的結果。

圖9 向聚簇索引插入順序的索引值

而當采用UUID的聚簇索引的表往插入數據,如圖10所示,因為新行的主鍵值不一定比之前的插入值大,所以InnoDB無法簡單的總是把新行插入到索引的最后,而是需要為新的行尋找合適的位置----通常是已有數據的中間位置----并且分配空間。這會增加很多額外的工作,并導致數據分布不夠優化。

圖10 向聚簇索引插入無序的值

下面總結使用UUID作為主鍵的一些缺點:

  • 寫入目標頁可能已經刷到磁盤上并從緩存中移除,或者是還沒有被加載到緩存中,InnoDB在插入之前不得不先找到并從磁盤讀取目標頁到內存中,這將導致大量的隨機I/O;
  • 因為寫入是亂序的,InnoDB不得不頻繁的做頁分裂操作,以便為新的行分配空間。頁分裂會導致移動大量數據,一次插入最少需要修改三個頁而不是一個,包含兩個葉子節點和一個父節點。
  • 由于頻繁的頁分裂,頁會變得稀疏并被不規則的填充,所以最終數據會有碎片。

把這些隨機值載入到聚簇索引以后,需要做一次optimize table來重建表并優化頁的填充。

注意,順序主鍵也有缺點:對于高并發工作負載,在InnoDB中按主鍵順序插入可能會造成明顯的爭用。主鍵的上界會成為“熱點”。因為所有的插入都發生在這里,所以并發插入可能導致間隙鎖競爭。另一個熱點可能是auto_increment鎖機制;如果遇到這個問題,則可能需要考慮重新設計表或者應用,比如應用層面生成單調遞增的主鍵ID,插表不使用auto_increment機制,或者更改innodb_autonc_lock_mode配置。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容