InnoDB的數據頁結構

是InnoDB存儲引擎管理數據庫的最小磁盤單位頁類型爲B-tree node的頁,存放的便是表中行的實際數據了。node

InnoDB數據頁由如下七個部分組成,如圖所示:數據庫

  1. File Header(文件頭)。
  2. Page Header(頁頭)。
  3. Infimun+Supremum Records。
  4. User Records(用戶記錄,即行記錄)。
  5. Free Space(空閒空間)。
  6. Page Directory(頁目錄)。
  7. File Trailer(文件結尾信息)。

 

File Header、Page Header、File Trailer的大小是固定的,用來標示該頁的一些信息,如Checksum、數據所在索引層等。其他部分爲實際的行記錄存儲空間,所以大小是動態的。數據結構

File Header

File Header用來記錄頁的一些頭信息,由以下8個部分組成,共佔用38個字節,如表4-3所示:函數

FIL_PAGE_SPACE_OR_CHKSUM:當MySQL版本小於MySQL-4.0.14,該值表明該頁屬於哪一個表空間,由於若是咱們沒有開啓innodb_file_per_table,共享表空間中可能存放了許多頁,而且這些頁屬於不一樣的表空間。以後版本的MySQL,該值表明頁的checksum值(一種新的checksum值)。工具

FIL_PAGE_OFFSET:表空間中頁的偏移值。spa

FIL_PAGE_PREV,FIL_PAGE_NEXT:當前頁的上一個頁以及下一個頁。B+Tree特性決定了葉子節點必須是雙向列表。3d

FIL_PAGE_LSN:該值表明該頁最後被修改的日誌序列位置LSN(Log Sequence Number)。指針

FIL_PAGE_TYPE:頁的類型。一般有如下幾種,見表4-4。請記住0x45BF,該值表明了存放的數據頁。日誌

FIL_PAGE_FILE_FLUSH_LSN:該值僅在數據文件中的一個頁中定義,表明文件至少被更新到了該LSN值。code

FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID:從MySQL 4.1開始,該值表明頁屬於哪一個表空間。

Page Header

接着File Header部分的是Page Header,用來記錄數據頁的狀態信息,由如下14個部分組成,共佔用56個字節。見表4-5。

PAGE_N_DIR_SLOTS:在Page Directory(頁目錄)中的Slot(槽)數。Page Directory會在後面介紹。

PAGE_HEAP_TOP:堆中第一個記錄的指針。

PAGE_N_HEAP:堆中的記錄數。

PAGE_FREE:指向空閒列表的首指針。

PAGE_GARBAGE:已刪除記錄的字節數,即行記錄結構中,delete flag爲1的記錄大小的總數。

PAGE_LAST_INSERT:最後插入記錄的位置。

PAGE_DIRECTION:最後插入的方向。可能的取值爲PAGE_LEFT(0x01),PAGE_RIGHT(0x02),PAGE_SAME_REC(0x03),PAGE_SAME_PAGE(0x04),PAGE_NO_DIRECTION(0x05)。

PAGE_N_DIRECTION:一個方向連續插入記錄的數量。

PAGE_N_RECS:該頁中記錄的數量。

PAGE_MAX_TRX_ID:修改當前頁的最大事務ID,注意該值僅在Secondary Index定義。

PAGE_LEVEL:當前頁在索引樹中的位置,0x00表明葉節點。

PAGE_INDEX_ID:當前頁屬於哪一個索引ID。

PAGE_BTR_SEG_LEAF:B+樹的葉節點中,文件段的首指針位置。注意該值僅在B+樹的Root頁中定義。

PAGE_BTR_SEG_TOP:B+樹的非葉節點中,文件段的首指針位置。注意該值僅在B+樹的Root頁中定義。

Infimum和Supremum記錄

在InnoDB存儲引擎中,每一個數據頁中有兩個虛擬的行記錄,用來限定記錄的邊界Infimum記錄是比該頁中任何主鍵值都要小的值,Supremum指比任何可能大的值還要大的值。這兩個值在頁建立時被創建,而且在任何狀況下不會被刪除。在Compact行格式和Redundant行格式下,二者佔用的字節數各不相同。下圖顯示了Infimum和Supremum Records。 

User Records與FreeSpace

User Records即實際存儲行記錄的內容。再次強調,InnoDB存儲引擎表老是B+樹索引組織的。

Free Space指的就是空閒空間,一樣也是個鏈表數據結構。當一條記錄被刪除後,該空間會被加入空閒鏈表中。

Page Directory

Page Directory(頁目錄)中存放了記錄的相對位置(注意,這裏存放的是頁相對位置,而不是偏移量),有些時候這些記錄指針稱爲Slots(槽)或者目錄槽(Directory Slots)。與其餘數據庫系統不一樣的是,InnoDB並非每一個記錄擁有一個槽,InnoDB存儲引擎的槽是一個稀疏目錄(sparse directory),即一個槽中可能屬於(belong to)多個記錄,最少屬於4條記錄,最多屬於8條記錄。

Slots中記錄按照鍵順序存放,這樣能夠利用二叉查找迅速找到記錄的指針。假設咱們有('i','d','c','b','e','g','l','h','f','j','k','a'),同時假設一個槽中包含4條記錄,則Slots中的記錄多是('a','e','i')。

因爲InnoDB存儲引擎中Slots是稀疏目錄,二叉查找的結果只是一個粗略的結果,因此InnoDB必須經過recorder header中的next_record來繼續查找相關記錄。同時,slots很好地解釋了recorder header中的n_owned值的含義,即還有多少記錄須要查找,由於這些記錄並不包括在slots中。

須要牢記的是,B+樹索引自己並不能找到具體的一條記錄,B+樹索引能找到只是該記錄所在的頁數據庫把載入內存,而後經過Page Directory再進行二叉查找。只不過二叉查找的時間複雜度很低,同時內存中的查找很快,所以一般咱們忽略了這部分查找所用的時間。

File Trailer

爲了保證頁可以完整地寫入磁盤(如可能發生的寫入過程當中磁盤損壞、機器宕機等緣由),InnoDB存儲引擎的頁中設置了File Trailer部分。File Trailer只有一個FIL_PAGE_END_LSN部分,佔用8個字節。前4個字節表明該頁的checksum值,最後4個字節和File Header中的FIL_PAGE_LSN相同。經過這兩個值來和File Header中的FIL_PAGE_SPACE_OR_CHKSUM和FIL_PAGE_LSN值進行比較,看是否一致(checksum的比較須要經過InnoDB的checksum函數來進行比較,不是簡單的等值比較),以此來保證頁的完整性(not corrupted)。

InnoDB數據頁結構示例分析

首先咱們創建一張表,並導入必定量的數據:

drop table if exists t;

create table t (a int unsigned not null auto_increment,b char(10),primary key(a))ENGINE=InnoDB CHARSET=UTF-8;

delimiter$$

  create procedure load_t(count int unsigned)

    begin

      set@c=0;

      while@c<count do

        insert into t select null,repeat(char(97+rand()*26),10);

        set@c=@c+1;

      end while;

    end;

$$

delimiter;

call load_t(100);

select * from t limit 10;

接着咱們用工具py_innodb_page_info來分析t.ibd,  py_innodb_page_info.py -v t.ibd

看到第四個頁(page offset 3)是數據頁,經過hexdump來分析t.ibd文件,打開整理獲得的十六進制文件,數據頁在0x0000c000(16K*3=0xc000)處開始:

先來分析前面File Header的38個字節:

52 1b 24 00數據頁的Checksum值。

00 00 00 03頁的偏移量,從0開始。

ff ff ff ff前一個頁,由於只有當前一個數據頁,因此這裏爲0xffffffff。

ff ff ff ff下一個頁,由於只有當前一個數據頁,因此這裏爲0xffffffff。

00 00 00 0a 6a e0 ac 93頁的LSN。

45 bf頁類型,0x45bf表明數據頁。

00 00 00 00 00 00 00這裏暫時無論該值。

00 00 00 dc表空間的SPACE ID。

先不急着看下面的Page Header部分,咱們來看File Trailer部分。由於File Trailer經過比較File Header部分來保證頁寫入的完整性

95 ae 5d 39 Checksum值,該值經過checksum函數和File Header部分的checksum值進行比較。

6a e0 ac 93注意到該值和File Header部分頁的LSN後4個值相等。

接着咱們來分析56個字節的Page Header部分,對於數據頁而言,Page Header部分保存了該頁中行記錄的大量細節信息。分析後可得:

Page Header(56 bytes):

PAGE_N_DIR_SLOTS=0x001a

PAGE_HEAP_TOP=0x0dc0

PAGE_N_HEAP=0x8066

PAGE_FREE=0x0000

PAGE_GARBAGE=0x0000

PAGE_LAST_INSERT=0x0da5

PAGE_DIRECTION=0x0002

PAGE_N_DIRECTION=0x0063

PAGE_N_RECS=0x0064

PAGE_MAX_TRX_ID=0x0000000000000000

PAGE_LEVEL=00 00

PAGE_INDEX_ID=0x00000000000001ba

PAGE_BTR_SEG_LEAF=0x000000dc0000000200f2

PAGE_BTR_SEG_TOP=0x000000dc000000020032

PAGE_N_DIR_SLOTS=0x001a,表明Page Directory有26個槽,每一個槽佔用2個字節。

咱們能夠從0x0000ffc4到0x0000fff7找到以下內容:

0000ffc0 00 00 00 00 00 70 0d 1d 0c 95 0c 0d 0b 85 0a fd|……p……
0000ffd0 0a 75 09 ed 09 65 08 dd 08 55 07 cd 07 45 06 bd|.u……e……U……E..
0000ffe0 06 35 05 ad 05 25 04 9d 04 15 03 8d 03 05 02 7d|.5……%……}
0000fff0 01 f5 01 6d 00 e5 00 63 95 ae 5d 39 6a e0 ac 93|……m……c..]9j……

PAGE_HEAP_TOP=0x0dc0表明空閒空間開始位置的偏移量,即0xc000+0x0dc0=0xcdc0處開始,咱們觀察這個位置的狀況,能夠發現這的確是最後一行的結束,接下去的部分都是空閒空間了:

0000cdb0 00 00 00 2d 01 10 70 70 70 70 70 70 70 70 70 70|……-..pppppppppp
0000cdc0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|……
0000cdd0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|……
0000cde0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|……

PAGE_N_HEAP=0x8066,當行記錄格式爲Compact時,初始值爲0x0802,當行格式爲Redundant時,初始值是2。其實這些值表示頁初始時就已經有Infinimun和Supremum的僞記錄行,0x8066-0x8002=0x64,表明該頁中實際的記錄有100條記錄。

PAGE_FREE=0x0000表明刪除的記錄數,由於這裏咱們沒有進行過刪除操做,因此這裏的值爲0。

PAGE_GARBAGE=0x0000,表明刪除的記錄字節爲0,一樣由於咱們沒有進行過刪除操做,因此這裏的值依然爲0。

PAGE_LAST_INSERT=0x0da5,表示頁最後插入的位置的偏移量,即最後的插入位置應該在0xc0000+0x0da5=0xcda5,查看該位置:

0000cda0 00 03 28 f2 cb 00 00 00 64 00 00 00 51 6e 4e 80|..(……d……QnN.
0000cdb0 00 00 00 2d 01 10 70 70 70 70 70 70 70 70 70 70|……-..pppppppppp
0000cdc0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00|……

能夠看到,最後這的確是最後插入a列值爲100的行記錄,可是此次直接指向了行記錄的內容,而不是指向行記錄的變長字段長度的列表位置。

PAGE_DIRECTION=0x0002,由於咱們是經過自增加的方式進行行記錄的插入,因此PAGE_DIRECTION的方向是向右。

PAGE_N_DIRECTION=0x0063,表示一個方向連續插入記錄的數量,由於咱們是以自增加的方式插入了100條記錄,所以該值爲99。

PAGE_N_RECS=0x0064,表示該頁的行記錄數爲100,注意該值與PAGE_N_HEAP的比較,PAGE_N_HEAP包含兩個僞行記錄,而且是經過有符號的方式記錄的,所以值爲0x8066。

PAGE_LEVEL=0x00,表明該頁爲葉子節點。由於數據量目前較少,所以當前B+樹索引只有一層。B+數葉子層老是爲0x00。

PAGE_INDEX_ID=0x00000000000001ba,索引ID。

上面就是數據頁的Page Header部分了,接下去就是存放的行記錄了,前面提到過InnoDB存儲引擎有2個僞記錄行,用來限定行記錄的邊界,咱們接着往下看:

0000c050 00 02 00 f2 00 00 00 dc 00 00 00 02 00 32 01 00|……2..
0000c060 02 00 1c 69 6e 66 69 6d 75 6d 00 05 00 0b 00 00|……infimum……
0000c070 73 75 70 72 65 6d 75 6d 0a 00 00 00 10 00 22 00|supremum……".

觀察0xc05E到0xc077,這裏存放的就是這兩個僞行記錄,InnoDB存儲引擎設置僞行只有一個列,且類型是Char(8)。僞行記錄的讀取方式和通常的行記錄並沒有不一樣,咱們整理後能夠獲得以下的結果:

#Infimum僞行記錄
01 00 02 00 1c/*recorder header*/
69 6e 66 69 6d 75 6d 00/*只有一個列的僞行記錄,記錄內容就是Infimum(多了一個0x00字節)
*/
#Supremum僞行記錄
05 00 0b 00 00/*recorder header*/
73 75 70 72 65 6d 75 6d/*只有一個列的僞行記錄,記錄內容就是Supremum*/

咱們來分析infimum行記錄的recorder header部分,最後2個字節位00 1c表示下一個記錄的位置的偏移量,即當前行記錄內容的位置0xc063+0x001c,獲得0xc07f。0xc07f應該很熟悉了,咱們前面的分析的行記錄結構都是從這個位置開始。咱們來看一下:

0000c070 73 75 70 72 65 6d 75 6d 0a 00 00 00 10 00 22 00|supremum……".
0000c080 00 00 01 00 00 00 51 6d eb 80 00 00 00 2d 01 10|……Qm……-..
0000c090 64 64 64 64 64 64 64 64 64 64 0a 00 00 00 18 00|dddddddddd……
0000c0a0 22 00 00 00 02 00 00 00 51 6d ec 80 00 00 00 2d|"……Qm……-
能夠看到這就是第一條實際行記錄內容的位置了,若是整理後能夠獲得:
/*第一條行記錄*/
00 00 00 01/*由於咱們建表時設定了主鍵,這裏ROWID即位列a的值1*/
00 00 00 51 6d eb/*Transaction ID*/
80 00 00 00 2d 01 10/*Roll Pointer*/
64 64 64 64 64 64 64 64 64 64/*b列的值'aaaaaaaaaa'*/

這和咱們查表獲得的數據是一致的:select a,b,hex(b) from t order by a limit 1;

經過recorder header最後2個字節記錄的下一行記錄的偏移量,咱們就能夠獲得該頁中全部的行記錄;經過page header的PAGE_PREV,PAGE_NEXT就能夠知道上一個頁和下個頁的位置。這樣,咱們就能讀到整張表全部的行記錄數據。

最後咱們來分析Page Directory,前面咱們已經提到了從0x0000ffc4到0x0000fff7是當前頁的Page Directory,以下:

0000ffc0 00 00 00 00 00 70 0d 1d 0c 95 0c 0d 0b 85 0a fd|……p……
0000ffd0 0a 75 09 ed 09 65 08 dd 08 55 07 cd 07 45 06 bd|.u……e……U……E..
0000ffe0 06 35 05 ad 05 25 04 9d 04 15 03 8d 03 05 02 7d|.5……%……}
0000fff0 01 f5 01 6d 00 e5 00 63 95 ae 5d 39 6a e0 ac 93|……m……c..]9j……

須要注意的是,Page Directory是逆序存放的,每一個槽2個字節。所以咱們能夠看到:00 63是最初行的相對位置,即0xc063;0070就是最後一行記錄的相對位置,即0xc070。咱們發現,這就是前面咱們分析的infimum和supremum的僞行記錄。Page Directory槽中的數據都是按照主鍵的順序存放,所以找具體的行就須要經過部分進行。前面已經提到,InnoDB存儲引擎的槽是稀疏的,還需經過recorder header的n_owned進行進一步的判斷。如,咱們要找主鍵a爲5的記錄,經過二叉查找Page Directory的槽,咱們找到記錄的相對位置在00 e5處,找到行記錄的實際位置0xc0e5:

0000c0e0 04 00 28 00 22 00 00 00 04 00 00 00 51 6d ee 80|..(."……Qm..
0000c0f0 00 00 00 2d 01 10 69 69 69 69 69 69 69 69 69 69|……-..iiiiiiiiii
0000c100 0a 00 00 00 30 00 22 00 00 00 05 00 00 00 51 6d|……0."……Qm
0000c110 ef 80 00 00 00 2d 01 10 6e 6e 6e 6e 6e 6e 6e 6e|……-..nnnnnnnn
0000c120 6e 6e 0a 00 00 00 38 00 22 00 00 00 06 00 00 00|nn……8."……
0000c130 51 6d f0 80 00 00 00 2d 01 10 71 71 71 71 71 71|Qm……-..qqqqqq
0000c140 71 71 71 71 0a 00 00 00 40 00 22 00 00 00 07 00|qqqq……@."……

能夠看到第一行的記錄是4不是咱們要找的5,可是咱們看前面的5個字節的recordheader,04 00 28 00 22,找到4~8位表示n_owned值的部分,該值爲4,表示該記錄有4個記錄,所以還須要進一步查找。經過recorder和ader最後2個字節的偏移量0x0022,找到下一條記錄的位置0xc107,這纔是咱們要找的主鍵爲5的記錄。

相關文章
相關標籤/搜索