1. 程式人生 > >《高效能Mysql》--聚簇索引

《高效能Mysql》--聚簇索引

聚簇索引並不是一種單獨的索引型別,而是一種資料儲存方式。比如,InnoDB的聚簇索引使用B+Tree的資料結構儲存索引和資料。

當表有聚簇索引時,它的資料行實際上存放在索引的葉子頁(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配置。



作者:大頭8086
連結:https://www.jianshu.com/p/54c6d5db4fe6
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。