InnoDB的B+樹索引


B+樹索引其本質就是B+樹在數據庫中的實現,但是B+索引在數據庫中有一個特點就是其高扇出性,因此在數據庫中,B+樹的高度一般都在2~3層,也就是對於查找某一鍵值的行記錄,最多只需要2到3次IO,這倒不錯。因為我們知道現在一般的磁盤每秒至少可以做100次IO,2~3次的IO意味着查詢時間只需0.02~0.03秒。

數據庫中的B+樹索引可以分為聚集索引(clustered index)和輔助聚集索引(secondary index)輔助聚集索引有時也稱非聚集索引(non-clustered index)。

但是不管是聚集還是非聚集的索引,其內部都是B+樹的,即高度平衡的,葉節點存放着所有的數據聚集索引與非聚集索引不同的是,葉節點存放的是否是一整行的信息

聚集索引

InnoDB存儲引擎表是索引組織表,即表中數據按照主鍵順序存放。而聚集索引就是按照每張表的主鍵構造一顆B+樹,並且葉節點中存放着整張表的行記錄數據,因此也讓聚集索引的葉節點成為數據頁。聚集索引的這個特性決定了索引組織表中數據也是索引的一部分。同B+樹數據結構一樣,每個數據頁都通過一個雙向鏈表來進行鏈接。

由於實際的數據頁只能按照一顆B+樹進行排序,因此每張表只能擁有一個聚集索引。在許多情況下,查詢優化器非常傾向於采用聚集索引,因為聚集索引能夠讓我們在索引的葉節點上直接找到數據。此外,由於定義了數據的邏輯順序,聚集索引能夠特別快地訪問針對范圍值的查詢。查詢優化器能夠快速發現某一段范圍的數據頁需要掃描。

現在我們來看一張表,我們以人為的方式讓其每個頁只能存放兩個行記錄,如:

create table t(a int not null primary key,b varchar(8000));

insert into t select 1,repeat('a',7000);

insert into t select 2,repeat('a',7000);

insert into t select 3,repeat(a',7000);

insert into t select 4,repeat('a',7000);

可以看到,我們表的定義和插入方式使得目前每個頁只能存放兩個行記錄,我們用py_innodb_page_info工具來分析表空間,可得:py_innodb_page_info.py-v mytest/t.ibd

page level為0000的即是數據頁。我們要分析的是page level為0001的頁,該頁是B+樹的根,我們來看看索引的根頁中存放的數據。

我們直接通過頁尾的Page Directory來分析,從00 63可以知道該頁中行開始的位置。接着通過Recorder Header來分析,0xc063開始的值為69 6e 66 69 6d 75 6d 00,就代表infimum偽行記錄。之前的5個字節01 00 02 00 1b就是Recorder Header,分析第4位到第8位的值1代表該行記錄中只有一個記錄(需要記住的是,InnoDB的Page Directory是稀疏的),即infimum記錄本身。我們通過Recorder Header中最后的兩個字節00 1b來判斷下一條記錄的位置,即c063+1b=c073,讀取鍵值可得80 01,就是主鍵為1的鍵值(我們制定的INT是無符號的,因此二進制是0x8001,而不是0x0001),80 01后值00 00 00 04代表指向數據頁的頁號。以同樣的方式,可以找到80 02和80 04這兩個鍵值以及它們指向的數據頁。

通過以上對於非數據頁節點的分析,我們發現數據頁上存放的是完整的行記錄,而在非數據頁的索引頁中,存放的僅僅是鍵值以及指向數據頁的偏移量,而不是一個完整的行記錄。因此我們構造的這顆二叉樹大致如圖5-14所示。

 

許多數據庫的文檔會這樣告訴讀者:聚集索引按照順序物理地存儲數據。但是試想,如果聚集索引必須按照特定順序存放物理記錄的話,則維護成本即顯得非常之高了。所以,聚集索引的存儲並不是物理上的連續,相反是邏輯上連續的。這其中有兩點:一是我們前面說過的頁通過雙向鏈表鏈接,頁按照主鍵的順序排列。另一點是每個頁中的記錄也是通過雙向鏈表進行維護,物理存儲上可以同樣不按照主鍵存儲。

聚集索引的另一個好處是,它對於主鍵的排序查找和范圍查找速度非常快。葉節點的數據就是我們要查詢的數據,如我們要查詢一張注冊用戶的表,查詢最后注冊的10位用戶,由於B+樹索引是雙向鏈表的,我們可以快速找到最后一個數據頁,並取出10條記錄,我們用Explain進行分析,可得:

explain select * from Profile order by id limit 10;

另一個是范圍查詢(range query),如果要查找主鍵某一范圍內的數據,通過葉節點的上層中間節點就可以得到頁的范圍,之后直接讀取數據頁即可,又如:

explain select * from Profile where id>10 and id<10000;

Explain得到了MySQL的執行計划(execute plan),並且rows列給出了一個查詢結果的預估返回行數。要注意的是,rows代表的是一個預估值,不是確切的值,如果我們實際進行這句SQL的查詢,可以看到實際上只有9 946行記錄:

select count(*) from Profile where id>10 and id<10000; 

輔助索引

對於輔助索引(也稱非聚集索引),葉級別不包含行的全部數據。葉節點除了包含鍵值以外,每個葉級別中的索引行中還包含了一個書簽(bookmark),該書簽用來告訴InnoDB存儲引擎,哪里可以找到與索引相對應的行數據。因為InnoDB存儲引擎表是索引組織表,因此InnoDB存儲引擎的輔助索引的書簽就是相應行數據的聚集索引鍵。顯示了InnoDB存儲引擎中輔助索引與聚集索引的關系。

輔助索引的存在並不影響數據在聚集索引中的組織,因此每張表上可以有多個輔助索引。當通過輔助索引來尋找數據時,InnoDB存儲引擎會遍歷輔助索引並通過葉級別的指針獲得指向主鍵索引的主鍵,然后再通過主鍵索引來找到一個完整的行記錄。舉例來說,如果在一顆高度為3的輔助索引樹中查找數據,那么需要對這顆輔助索引遍歷3次找到指定主鍵;如果聚集索引樹的高度同樣為3,那么還需要對聚集索引進行3次查找,才能最終找到一個完整的行數據所在的頁,因此一共需要6次邏輯IO來訪問最終的一個數據頁。

對於其他的一些數據庫,如Microsoft SQL Server數據庫,其表類型有一種不是索引組織表,稱為堆表在數據的存放按插入順序方面,與MySQL的MyISAM存儲引擎有些類似。堆表的特性決定了堆表上的索引都是非聚集的,但是堆表沒有主鍵。因此這時書簽是一個行標識符(row identifier,RID),可以用如“文件號:頁號:槽號”的格式來定位實際的行。

堆表的非聚集索引既然不需要再通過主鍵對聚集索引進行查找,那不是速度會更快嗎?答案是也許,在某些只讀的情況下,書簽為行標識符方式的非聚集索引可能會比書簽為主鍵方式的非聚集索引快。但是考慮在OLTP(OnLine Transaction Processing,在線事務處理)應用的情況下,表可能還需要發生插入、更新、刪除等DML操作。當進行這類操作時,書簽為行標識符方式的非聚集索引可能需要不斷更新行標識符所指向數據頁的位置,這時的開銷可能就會大於書簽為主鍵方式的非聚集索引了。

Microsoft SQL Server數據庫DBA問過這樣的問題,為什么在SQL Server上還要使用索引組織表?堆表的書簽性使得非聚集查找可以比主鍵書簽方式更快,並且非聚集可能在一張表中存在多個,我們需要對多個非聚集索引的查找。而且對於非聚集索引的離散讀取,索引組織表上的非聚集索引會比堆表上的聚集索引慢一些。當然,在一些情況下,使用堆表的確會比索引組織表更快,但是我覺得大部分是由於存在於OLAP(On-Line Analytical Processing,在線分析處理)的應用。其次就是前面提到的,表中數據是否需要更新,並且更新會否影響到物理地址的變更。此外另一個不能忽視的是對於排序和范圍查找,索引組織表可以通過B+樹的中間節點就找到要查找的所有頁,然后進行讀取,而堆表的特性決定了這對其是不能實現的。最后,非聚集索引的離散讀,的確是存在上述情況,但是一般的數據庫都通過實現預讀(read ahead)技術來避免多次的離散讀操作。因此,具體是建堆表還是索引組織表,這取決於你的應用,不存在哪個更優的情況。這和InnoDB存儲引擎好還是MyISAM存儲引擎好的問題是一樣的,具體情況具體分析。

接下來,我們通過閱讀表空間文件來分析InnoDB存儲引擎的非聚集索引,我們還是分析上一小節所用的表t。

不同的是,在表t上再建立一個列c,並對列c創建非聚集索引: 

alter table t add c int not null;

update t set c=0-a;

alter table t add key idx_c(c); 

show index from t;

select a,c from t;

然后用py_innodb_page_info工具來分析表空間,可得:py_innodb_page_info.py-v t.ibd

對比前一次我們的分析,可以看到這次多了一個頁。分析page offset為4的頁,該頁為非聚集索引所在頁,通過工具hexdump分析可得:

因為只有4行數據,並且列c只有4個字節,因此在一個非聚集索引頁中即可完成,整理分析可得下圖所示的關系:

顯示了表t中輔助索引idx_c和聚集索引的關系。可以看到輔助索引的葉節點中包含了列c的值和主鍵的值。這里鍵值因為我特意設為負值,你會發現-1以7f ff ff ff的方式進行內部存儲。7(0111)最高位為0,代表負值,實際的值應該取反后,加1,即得-1。

B+樹索引的管理

索引的創建和刪除可以通過兩種方法,一種是ALTER TABLE,另一種是CREATE/DROP INDEX。ALTER TABLE創建索引的語法為:

ALTER TABLE tbl_name

|ADD{INDEX|KEY}[index_name]

[index_type] (index_col_name,……) [index_option]……

ALTER TABLE tbl_name

DROP PRIMARY KEY

|DROP {INDEX|KEY} index_name

CREATE/DROP INDEX的語法同樣很簡單:

CREATE [UNIQUE] INDEX index_name

[index_type]

ON tbl_name(index_col_name,……)

DROP INDEX index_name ON tbl_name

索引可以索引整個列的數據,也可以只索引一個列的開頭部分數據,如前面我們創建的表t,b列為varchar(8000),但是我們可以只索引前100個字段,如:

alter table t add key idx_b (b(100));

目前MySQL數據庫存在的一個普遍問題是,所有對於索引的添加或者刪除操作,MySQL數據庫是先創建一張新的臨時表,然后把數據導入臨時表,刪除原表,再把臨時表重名為原來的表名。因此對於一張大表,添加和刪除索引需要很長的時間。對於從Microsoft SQL Server或Oracle數據庫的DBA來說,MySQL數據庫的索引維護始終讓他們非常苦惱。

InnoDB存儲引擎從版本InnoDB Plugin開始,支持一種稱為快速索引創建方法。當然這種方法只限定於輔助索引,對於主鍵的創建和刪除還是需要重建一張表。對於輔助索引的創建,InnoDB存儲引擎會對表加上一個S鎖。在創建的過程中,不需要重建表,因此速度極快。但是在創建的過程中,由於上了S鎖,因此創建的過程中該表只能進行讀操作。刪除輔助索引操作就更簡單了,只需在InnoDB存儲引擎的內部視圖更新下,將輔助索引的空間標記為可用,並刪除MySQL內部視圖上對於該表的索引定義即可。

查看表中索引的信息可以使用SHOW INDEX語句。如我們來分析表t,之前先加一個聯合索引,可得:

alter table t add key idx_a_b(a,c);

show index from t;

因為在表t上有3個索引:一個主鍵索引,c列上的索引,和b列前100個字節構成的索引。

接着我們來具體講解每個列的含義:

  1. Table:索引所在的表名。
  2. Non_unique:非唯一的索引,可以看到primary key是0,因為必須是唯一的。
  3. Key_name:索引的名稱,我們可以通過這個名稱來DROP INDEX。
  4. Seq_in_index:索引中該列的位置,如果看聯合索引idx_a_b就比較直觀了。
  5. Column_name:索引的列
  6. Collation:列以什么方式存儲在索引中。可以是'A'或者NULL。B+樹索引總是A,即排序的。如果使用了Heap存儲引擎,並且建立了Hash索引,這里就會顯示NULL了。因為Hash根據Hash桶來存放索引數據,而不是對數據進行排序。
  7. Cardinality:非常關鍵的值,表示索引中唯一值的數目的估計值。Cardinality/表的行數應盡可能接近1,如果非常小,那么需要考慮是否還需要建這個索引。
  8. Sub_part:是否是列的部分被索引。如果看idx_b這個索引,這里顯示100,表示我們只索引b列的前100個字符。如果索引整個列,則該字段為NULL。
  9. Packed:關鍵字如何被壓縮。如果沒有被壓縮,則為NULL。
  10. Null:是否索引的列含有NULL值。可以看到idx_b這里為Yes。因為我們定義的b列允許NULL值。
  11. Index_type:索引的類型。InnoDB存儲引擎只支持B+樹索引,所以這里顯示的都是BTREE。
  12. Comment:注釋。

Cardinality值非常關鍵,優化器會根據這個值來判斷是否使用這個索引。但是這個值並不是實時更新的,並非每次索引的更新都會更新該值,因為這樣代價太大了。因此這個值是不太准確的,只是一個大概的值。上面顯示的結果主鍵的Cardinality為2,但是很顯然我們表中有4條記錄,這個值應該是4。如果需要更新索引Cardinality的信息,可以使用ANALYZE TABLE命令。如:

analyze table t;

show index from t;

這時的Cardinality的值就對了。不過,在每個系統上可能得到的結果不一樣,因為ANALYZE TABLE現在還存在一些問題,可能會影響得到最后的結果。

另一個問題是MySQL數據庫對於Cardinality計數的問題,在運行一段時間后,可能會看到下面的結果:

show index from Profile;

Cardinality為NULL,在某些情況下可能會發生索引建立了、但是沒有用到,或者explain兩條基本一樣的語句,但是最終出來的結果不一樣。一個使用索引,另外一個使用全表掃描,這時最好的解決辦法就是做一次ANALYZE TABLE的操作。因此我建議在一個非高峰時間,對應用程序下的幾張核心表做ANALYZE TABLE操作,這能使優化器和索引更好地為你工作。

 


免責聲明!

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



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