自己分析一下ibd文件還是蠻有意思的,能夠學到不少東西,建議跟著走一遍,慢慢領會作者設計的意圖
人學東西總是先感性的認識,慢慢到理性 —— 過程中大腦需要理解和消化
mysql版本5.7.26
?
先貼一張數據頁的結構圖-方便對整體有個印象
利用工具查看數據庫文件ibd的頁分布情況 (工具名py_innodb_page_info)
當你熟悉page的格式后,自己也能寫一個這樣的工具
test.ibd文件頁分布情況
- page offset 00000003, page type <B-tree Node>, page level <0001>
- page offset 00000004, page type <B-tree Node>, page level <0000>
- page offset 00000005, page type <B-tree Node>, page level <0000>
- page offset 00000006, page type <B-tree Node>, page level <0000>
以上四個都是數據頁
我們分析 page offset 00000006, page type <B-tree Node>, page level <0000>
page level <0000> 表示的是葉子節點
使用十六進制工具Synalyze It! 打開/usr/local/mysql/data/nishui/test.ibd 文件(文件權限問題此處忽略,二進制工具任意)
找到 00000006 頁在文件中的位置
因為看的是00000006數據頁, 用6 * 16 * 1024 = 98304B (innodb引擎默認一頁16KB(可通過innodb_page_size改變頁大小),然而1KB= 1024B) 轉換成十六進制為 0x18000
show global status like 'Innodb_page_size' 可查看當前頁大小
找到該位置截圖如下:
這個頁便是 00000006 數據頁開始的位置了, 可以開始分析詳細數據了
一、先分析File Header(38字節-描述頁信息)
- 72 08 C8 7F -> 數據頁的checksum值
- 00 00 00 06 -> 頁號(偏移量)
- 00 00 00 05 -> 前一頁是第5頁
- FF FF FF FF -> 由于沒有下一頁,因此為該值
- 00 00 00 00 00 38 23 77 -> 頁的LSN
- 45 BF -> 頁的類型 0x45BF代表數據頁,剛好這頁是數據頁
- 00 00 00 00 00 00 00 00 -> 獨立表空間,該值為0
- 00 00 00 5B -> 表空間的SPACE ID
二、分析Page Header(56字節-記錄頁的狀態信息)
SHOW TABLE STATUS LIKE 'test'查看表行記錄格式
- 00 07 -> 代表Page Directory 有7個槽
-
17 27 -> 代表空閑空間開始位置的偏移量,即 0x18000 + 0x1727 = 0x19727,觀察這個位置,這是最后一行的結束,后面都是空閑的
06頁空閑邏輯位置 - 80 1D -> 當前為 Compact 格式,第15位表示行記錄格式,再加上兩條偽記錄, 因此0x801D - 0x8002 = 0x001B,代表該頁中實際的記錄有27條記錄
- 00 00 -> 指向頁中空閑位置(偏移量)
- 00 00 -> PAGE_GARBAGE 表示沒有刪除的數據
-
16 4A -> PAGE_LAST_INSERT 最后插入記錄的位置偏移 0x18000 + 0x164A = 0x1964A 直接指向最后一行數據存儲的地址,也就是id為199,這條確實是最后一條插入的
test表中最后一條記錄圖 - 00 02 -> PAGE_DIRECTION 最后插入的方向,向右邊插入
- 00 1A -> PAGE_N_DIRECTION 一個方向連續插入記錄的數量 連續插入26個
- 00 1B -> PAGE_N_RECS 當前數據頁中含有27條記錄
- 00 00 00 00 00 00 00 00 修改當前頁的最大事務ID
- 00 00 -> 代表頁為葉子節點
- 00 00 00 00 00 00 00 43 -> 索引ID,表示當前頁屬于哪個索引
- 00 00 00 42 00 00 00 02 00 F2 -> B+樹數據頁非葉子節點所在段的segment header。注意該值僅在樹的root頁中定義
- 00 00 00 42 00 00 00 02 00 32 ->B+樹數據頁所在段的segment header。
小結一下
1.innodb在整個頁可以使用的空間當成heap,當需要插入記錄的時候,首先會檢查PAGE_FREE指向的空閑空間,若申請的空間小于等于該空間容量時,那么使用該空閑空間,否者從PAGE_HEAP_TOP指向的空閑空間進行分配
heap中存儲的記錄非物理連續的,只是邏輯上連續的,可用下圖表示
2.PAGE_LAST_INSERT、PAGE_DIRECTION、PAGE_N_DIRECTION主要使用來做頁分裂操作的
數據頁中行記錄存儲的排列方式
三、偽記錄分析Infimum + Supremum Record (26字節-兩個虛擬行記錄)
innodb存儲引擎有兩個偽記錄,用來界定行記錄的邊界
數據從 0x1805E 到 0x18077
- 01 00 02 00 1E -> recorder header (5字節)
- 69 6E 66 69 6D 75 6D 00 -> 只有一個列的偽記錄,記錄內容就是Infimum(多了一個 0x00 字節) (8字節)
- 08 00 0B 00 00 -> recorder header (5字節)
- 73 75 70 72 65 6D 75 6D -> 只有一個列的偽記錄,記錄內容就是Supremum (8字節)
分析下偽記錄中的recorder header中的next_record
recorder header最后兩個字節 0x001E,表示下一個記錄位置的偏移量,即當前行記錄“內容”的位置0x18063 + 0x001E,即0x18081,這個位置就是存放第一條實際用戶記錄
四、分析User Record
當前 Row Format 為Compact格式 可通過命令show table status like 'table_name' 進行查看
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`t1` varchar(10) DEFAULT NULL,
`t2` varchar(15) DEFAULT NULL,
`t3` int(11) DEFAULT NULL,
`t4` varchar(1500) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=200 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
創建數據的腳本
CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_test`( )
BEGIN
#Routine body goes here...
declare i int;
declare tmp int;
set i=1;
set tmp = 1;
while i<200 do
if tmp=1 then
insert into test (t1, t4) values('a', REPEAT('a', i));
set tmp = 0;
else
insert into test (t1, t3, t4) values('a', i, REPEAT('a', i));
set tmp = 1;
end if;
set i=i+1;
end while;
END
由上面偽記錄Infimum的Record Header可知下一條記錄的開始地址是0x18081,順便把前面的extra info也分析下,截圖如下
- AD 80 01 -> 變長字段長度列表, 逆序,第一列是1字節,第四列是2字節,所以第一列包含1個字符,第四列包含173個 字符80 AD存的是補碼,換算成原碼為0x00AD,轉換成10進制就是173
-
02 -> 二進制(00000010)表示第二個字段為null
- 00 00 10 00 CC -> Record Header 固定5字節長度
- 00 00 00 AD -> 由于是自動創建的int自增id ,固是4個字節,當前行記錄id為173, 由于該id是無符號的,所以最高位不是符號位
- 00 00 00 00 69 E7 -> TransactionId
- D7 00 00 01 5C 01 10 -> Roll Pointer
- 61 -> 第一列字段數據 a
- 80 00 00 AD -> 第二列,存的是補碼,因此原碼為0xAD,故值為173
- 第三列為null,不占用空間
- 61 .... 61 -> 第四列字段數據 a ... a 173個 省略
分析User Record中Record Header中的內容
0x 00 00 10 00 CC 轉換成十進制如下 00000000 00000000 00010000 00000000 11001100
下面都是二進制的,其他的都是十六進制的
- 0 -> 預留位1
- 0 -> 預留位2
- 0 -> delete_mask 標記該記錄是否刪除,0表示沒有刪除 說明刪除的數據很可能還在頁中,并且占用著空間
- 0 -> min_rec_mask 標記該記錄是否為B+樹的非葉子節點中的最小記錄
- 0000 -> n_owned 表示當前槽管理的記錄數
- 00000000 00010 -> heap_no 表示當前記錄在記錄堆的位置信息,這個值表示當前記錄在heap中的位置為2
- 000 -> record_type 表示當前記錄的類型,0表示普通記錄
-
00000000 11001100 -> next_record 表示下一條記錄的相對位置,轉換16進制為0xCC,0x18081 + 0xCC = 0x1814D,下一條記錄的值地址為0x1814D,截圖如下
簡單用圖可表示如下(忽略實際內容):
五、分析Page Directory
這一頁的末尾是0x1BFFF,并且加上Page Header中PAGE_N_DIR_SLOTS,能夠知道Page Directory中包含了7個slot 截圖如下
位置是從 0x1BFEA - 0x1BFF7,一共14個字節,因此展開如下:
- 00 70 -> supremum記錄所在行偏移量地址
- 10 2C -> id為192的行偏移量地址
- 0C C2 -> id為188的行偏移量地址
- 09 68 -> id為184的行偏移量地址
- 06 1E -> id為180的行偏移量地址
- 02 E4 -> id為176的行偏移量地址
- 00 63 -> infimum記錄所在行偏移量地址
六、分析File Tailer
固定占用8個字節,并且是在頁尾部,可以直接得出位置為0x1BFF8 開始的
- 72 08 C8 7F -> Old-style Checksum
- 00 38 23 77 -> Low 32 bit of LSN
為了保證頁能夠完整地寫入磁盤(如可能發生的寫入過程中磁盤損壞、機器宕機等原因),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)。
數據頁格式
File Header
名稱 | 大?。ㄗ止潱?/th> | 說明 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | 當mysql為4.0.14之前的版本時,該值為0。在之后的mysql版本中,該值代表頁的checksum值(一種新的checksum值) |
Fil_PAGE_OFFSET | 4 | 表空間中頁的偏移值,如果獨立表空間a.ibd的大小為1GB,如果頁的大小為16kb,那么總共有65536個頁.FIL_PAGE_OFFSET表示該頁在所有頁中的位置。若此表空間的ID為10,那么搜索頁(10, 1)就表示在表a中的第二頁 |
FIL_PAGE_PREV | 4 | 當前頁的上一頁,B+樹的特性就決定了葉子節點必須是雙向列表 |
FIL_PAGE_NEXT | 4 | 當前頁的下一頁 |
FIL_PAGE_LSN | 8 | 該值代表頁最后被修改的日志序列位置LSN |
FIL_PAGE_TYPE | 2 | INNODB 存儲頁的類型, |
FIL_PAGE_FILE_FLUSH_LSN | 8 | 該值僅在系統表空間的一個頁中定義,代表文件至少更新到了LSN值。對于獨立表空間,該值為0 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 從mysql4.1開始,該值代表屬于哪個表空間 |
Innodb存儲引用中頁的類型
名稱 | 十六進制 | 解釋 |
---|---|---|
FIL_PAGE_INDEX | 0x45BF | B+樹葉節點 |
FIL_PAGE_UNDO_LOG | 0x0002 | undo log頁 |
FIL_PAGE_INODE | 0x0003 | 索引節點 |
FIL_PAGE_IBUF_FREE_LIST | 0x0004 | insert buffer空閑列表 |
FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 該頁為最新分配頁 |
FIL_PAGE_IBUF_BITMAP | 0x0005 | insert buffer 位圖 |
FIL_PAGE_TYPE_SYS | 0x0006 | 系統頁 |
FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事務系統數據 |
FIL_PAGE_TYPE_FSP_HDR | 0x0008 | File space Header |
FIL_PAGE_TYPE_XDES | 0x0009 | 擴展描述頁 |
FIL_PAGE_TYPE_BLOB | 0x000A | BLOB頁 |
Page Header
用來數據頁的狀態信息,14部分組成,共56字節
名稱 | 大?。ㄗ止潱?/th> | 說明 |
---|---|---|
PAGE_N_DIR_SLOTS | 2 | 在Page Directory (頁目錄〉中 的Slot (槽〉 數,“4.4.S Page Directory” 小節中會介紹 |
PAGE HEAP TOP | 2 | 堆中第一個記錄的指針, 記錄在頁中是根據堆 的形式存放的 堆中空閑空間的位置(偏移量) |
PAGE N HEAP | 2 | 堆中的記錄數. 一共占用2 字節, 但是第15 位表示行記錄格式 (包括最小和最大記錄以及標記為刪除的記錄) |
PAGE FREE | 2 | 指向可重用空間的首指針 指向頁中空閑空間的位置(偏移量)(就是標記為刪除的記錄地址) |
PAGE GARBAGE | 2 | 己刪除記錄的字節 數, 即行記錄結構中也陽也在為1的記錄大小的總數 |
PAGE LAST INSERT | 2 | 最后捕入記錄的位置(偏移量) |
PAGE DIRECTION | 2 | 最后插入的方向. 可能的取值為2 。 1.PAGE LEFT (0x01) 2.PAGE RIGHT (Ox02) 3.PAGE SAME REC (Ox03) 4. PAGE SAME PAGE (Ox04) |
PAGE N DIRECTION | 2 | 一個方向連續插入記錄的數量 |
PAGE N RECS | 2 | 該頁中記錄的數量 |
AGE MAX TRX ID | 8 | 修改當前頁 的最大事務ID, 注意該值僅在Secondary Index中定義 |
PAGE LEVEL | 2 | 當前頁 在索引樹中的位置, OxOO代表葉節點, l!P時節 J點總是在第0層 |
PAGE INDEX ID | 8 | 索引ID, 表示當前頁屬于哪個索引 |
PAGE BTR SEG LEAF | 10 | B+樹數據頁非頁節點所在段的segment header。 注意該值僅在 B+ 樹的 Root 頁中定義 |
PAGE BTR SEG TOP | 10 | B+樹數據頁所在段的 segment header. 注意該值僅在 B+樹的 Root 頁中定義 |
COMPACT行記錄格式
名稱 | 大?。╞it) | 描述 |
---|---|---|
預留位1 | 1 | 沒有使用 |
預留位2 | 1 | 沒有使用 |
delete_mask | 1 | 標記該記錄是否被刪除 |
min_rec_mask | 1 | 標記該記錄是否為B+樹的非葉子節點中的最小記錄 |
n_owned | 4 | 表示當前槽管理的記錄數 |
heap_no | 13 | 表示當前記錄在記錄堆的位置信息 |
record_type | 3 | 表示當前記錄的類型,0表示普通記錄,1表示B+樹非葉節點記錄,2表示最小記錄,3表示最大記錄 |
next_record | 16 | 表示下一條記錄的相對位置 |
Total | 40(Byte) | nothing |
參考借鑒:
InnoDB數據頁結構
MYSQL內核:INNODB存儲引擎 卷1-4
MySQL技術內幕InnoDB存儲引擎第2版