文章目錄
- 一、前言
- 二、InnoDB 行格式
- 1. COMPACT 行格式
- 1.1 記錄的額外信息
- 1.2 記錄的真實數據
- 1.3 綜上
- 2. REDUNDANT 行格式
- 2.1 字段長度偏移列表
- 2.2 記錄頭信息
- 3. DYNAMIC 行格式和 COMPPESED 行格式
- 三、InnoDB 數據頁結構
- 1. File Header (文件頭部)
- 2. Page Header (頁面頭部)
- 3. Infimum + Supremum
- 4. User Records + Free Space
- 5. Page Directory
- 6. File Trailer
- 四、InnoDB 表空間
- 1. 獨立表空間
- 1.1 XDES Entry (Extent Descriptor Entry)
- 1.1.1 XDES Entry 鏈表
- 1.1.2 鏈表基節點
- 1.2 段的結構
- 1.3 總結
- 2. 系統表空間
- 五、補充內容
- 1. CHAR(M) 列的存儲格式
- 2. 溢出列
- 六、參考內容
一、前言
最近在讀《MySQL 是怎樣運行的》、《MySQL技術內幕 InnoDB存儲引擎 》,后續會隨機將書中部分內容記錄下來作為學習筆記,部分內容經過個人刪改,因此可能存在錯誤,如想詳細了解相關內容強烈推薦閱讀相關書籍
由于存儲介質的特性,磁盤本身存取就比主存慢很多,再加上機械運動耗費,磁盤的存取速度往往是主存的幾百分之一,因此為了提高效率,要盡量減少磁盤I/O。為了達到這個目的,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序向后讀取一定長度的數據放入內存。由于磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對于具有局部性的程序來說,預讀可以提高I/O效率。即磁盤預讀原理。
InnoDB 中頁的大小一般為 16KB, 因此利用磁盤預讀原理在一般情況下 InnoDB 利用磁盤預讀原理,一次讀寫操作的都是 16KB,即每次讀寫都是操作一個 InnoDB 頁的大小。
二、InnoDB 行格式
在創建表或修改表的時候可以執行記錄所使用的行格式, 如下:
CREATE TABLE [tableName] [列信息] ROW_FORMAT=行格式名稱;
ALTER TABLE [tableName] ROW_FORMAT=行格式名稱;
InnoDB 有四種不同類型的行格式:COMPACT 、REDUNDANT、DYNAMIC、COMPRESSED,具體如下:
1. COMPACT 行格式
COMPACT 行格式示意圖如下:
基于上圖,大體分為如下部分:
1.1 記錄的額外信息
-
變長字段長度列表:用于記錄 VARCHAR、TEXT 等變長字段的實際長度。因為變長字段的長度是不固定的,所以需要一個額外的地方存儲變長字段的實際長度列表,該部分記錄在【記錄的額外信息】的【變長字段長度列表】中,而字段實際內容在【記錄的真實數據】部分。
需要注意的是:
- 【變成字段長度列表】是按照字段定義逆序存放。因為記錄的next_record 指針指向【記錄的額外信息】和【記錄的真實數據】之間,向左則是頭信息,向右則是數據信息。
- 變長字段長度列表只會存儲值為非 NULL 的列的字段長度,不存儲值為 NULL 的列的內容長度。
- 如果表中所有列都不是變長數據類型或者所有列的值都是 NULL 的話,就不需要有變長字段長度列表。
-
NULL 值列表:一條記錄中的某些列可能存儲 NULL 值,如果把這些 NULL值都放到記錄的真實數據中存儲起來會占用空間,所以COMPACT 行格式把一條記錄中值為 NULL 的列統一管理起來,存儲到NULL 值列表。
需要注意的是:
- NULL 值列表不是必須的,如果沒有 NULL 列則就不存在該頭信息。
-
記錄頭信息:由固定的5字節組成,用于描述記錄一些屬性。具體如下:
名稱 大小(比特) 描述 預留位1 1 未使用 預留位2 1 未使用 deleted_flag 1 標記該記錄是否被刪除 min_rec_flag 1 B+Tree 中每層非葉子節點中的最小的目錄項記錄都會添加該標記 (索引頁為1,數據頁為0) n_owned 4 一個頁面中的記錄會被分成若干個組(Page Directory),其中每組的最后一條記錄中的 頭信息中的 n_owned 屬性表示該組中共有幾條記錄。 head_no 13 表示當前記錄在頁面堆中的相對位置,每申請一條記錄的存儲空間時,head_no 加1 record_type 3 表示當前記錄的類型,0 表示普通記錄,1 表示 B+Tree 非葉子節點的目錄項記錄,2 表示 Infimum 記錄,3 表示 Supremum 記錄 next_record 16 表示下一條記錄的相對位置。
1.2 記錄的真實數據
除了真實數據列外,MySQL會為每行記錄增加幾個隱藏列:
列名 | 是否必需 | 占用空間 | 描述 |
---|---|---|---|
row_id | 否 | 6字節 | 行ID,如果記錄不滿足主鍵生成策略,則使用該列,如果該行記錄已經有了主鍵,則該屬性不需要創建。 |
trx_id | 是 | 6字節 | 事務id |
roll_pointer | 是 | 7字節 | 回滾指針 |
1.3 綜上
-
deleted_flag :標記該記錄是否被刪除,需要注意的是:
- 當記錄被刪除后,并不會從磁盤中移除,因為移除它們之后,還需要再磁盤上重新排列其他記錄。因此為了避免這種性能消耗,所以只打一個刪除標記。所有被刪除的記錄會形成一個垃圾鏈表,記錄在這個鏈表中占用的空間成為可重用空間。
-
next_record : 表示下一條記錄的相對位置,需要注意的是:
- 這里指向的位置是下一條行記錄的 【記錄的額外信息】和【記錄的真實信息】之間的位置,如果該值為負數,則說明下一條記錄在當前記錄前面,否則在后面。因為記錄是以鏈表形式指向,所以不需要嚴格的物理順序性。
- 這里的下一條記錄指的并不是插入順序中的下一條記錄,而是按照主鍵值從小到大的順序排列的下一條記錄,如果一個記錄被刪除,則 deleted_flag 會被標記為 1,當下次插入的時候,可能會復用該空間(如果主鍵合適的話)
- 之所以指向 【記錄的額外信息】和【記錄的真實信息】之間的位置因為在這個位置,向左讀取就是記錄頭信息,向右讀取就是真實數據,并且 變長字段長度列表、NULL 值列表都是逆序存放到,因此使得記錄中位置靠前的字段和他對應的字段長度信息在內存中距離更近,這可能會提高高速緩存命中率。如下圖:
-
一條記錄的結構如下(其中隱藏列row_id、trx_id、roll_pointer沒有畫出,并不是不存在):
2. REDUNDANT 行格式
REDUNDANT 是 MySQL 5.0 之前就在使用的一種古老的行格式。格式如下:
2.1 字段長度偏移列表
REDUNDANT 行格式的 字段長度列表 會將所有字段的長度信息都按照逆序存儲到字段長度偏移列表。
2.2 記錄頭信息
REDUNDANT 記錄頭信息占用 6 字節,如下:
需要注意的是:
- REDUNDANT 行格式對于 NULL 值的處理:將列對應的偏移量值的第一個比特位作為是否為 NULL 的依據,該比特位可以稱之為 NULL 比特位,如果該位為1 則該列的值為 NULL,否則就不是 NULL。
3. DYNAMIC 行格式和 COMPPESED 行格式
DYNAMIC 行格式和 COMPPESED 行格式與 COMPACT 行格式基本類似。區別在與處理溢出頁時不會在記錄的真實數據處存儲該溢出列真實數據的前768字節,而是把該列的所有真實數據都存儲到溢出頁中,只在記錄的真實數據處存儲20字節大小指向溢出頁的地址。
COMPPESED 與 DYNAMIC 不同的是:COMPPESED 行格式會采用壓縮算法對頁面進行壓縮。
三、InnoDB 數據頁結構
InnoDB 數據頁默認 16KB,可以劃分為多個部分,如下:
各個字段如下:
名稱 | 中文名 | 占用空間大小(字節) | 簡單描述 |
---|---|---|---|
File Header | 文件頭部 | 38 | 頁的一些通用信息 |
Page Header | 頁面頭部 | 56 | 數據頁專有的一些信息 |
Infimum + Supremum | 頁面中的最小記錄和最大記錄 | 26 | 兩個虛擬的記錄,MySQL硬性規定,Infimum 記錄頁面中最小的記錄,Supremum記錄頁面中最大的記錄。即任何用戶寫的記錄都比 Supremum 記錄小,比 Infimum 記錄大 (類似于鏈表中的頭節點和尾節點) |
User Records | 用戶記錄 | 不確定 | 用戶存儲的記錄內容 |
Free Space | 空閑空間 | 不確定 | 頁中尚未使用的空間 |
Page Directory | 頁目錄 | 不確定 | 頁中某些記錄的相對位置 |
File Trailer | 文件尾部 | 8 | 校驗頁是否完整 |
1. File Header (文件頭部)
File Header 通用與各種類型的頁,也就是說各種類型的頁都以 File Header 作為第一個組成部分,他描述了一些通用于各種頁的信息。該部分固定占用38字節。包括 每頁的頁號(InnoDB 通過頁號確定唯一頁)、前后頁的地址(形成雙向鏈表)、頁的類型(undo 日志頁、數據頁、change buffer 頁等),具體如下:
狀態名稱 | 占用空間大小(字節) | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | MySQL 4.014 版本代表本頁所屬表空間Id,之后表示頁的校驗和 |
FIL_PAGE_OFFSET | 4 | 頁號, 每個頁都有一個單獨的頁號 |
FIL_PAGE_PREV | 4 | 上一個的頁號,InnoDB 允許頁之前不連續,前后的頁通過鏈表連接 |
FIL_PAGE_NEXT | 4 | 下一個的頁號,InnoDB 允許頁之前不連續,前后的頁通過鏈表連接 |
FIL_PAGE_LSN | 8 | 頁面被最后修改時對應的 LSN值 |
FIL_PAGE_TYPE | 2 | 該頁的類型(undo 日志頁、數據頁、change buffer 頁等) |
FIL_PAGE_FILE_FLUSH_LSN | 8 | 僅在系統表空間的第一個頁中定義,代表文件至少被刷新到了對應的LSN 值 |
FIL+PAGE_ARCH_LOG_NO_OR_SPACE_IF | 4 | 頁屬于哪個表空間 |
2. Page Header (頁面頭部)
File Header 通用與各種類型的頁, 而Page Header 針對的是數據頁記錄的各種狀態。Page Header 是在數據頁中的特殊部分,存儲在數據頁中的記錄的狀態信息,比如:數據頁中已經存儲了多少條記錄, Free Space 在頁面中的地址偏移量,頁目錄中存儲了多少槽等。具體如下:
狀態名稱 | 占用空間(字節) | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS | 2 | 在頁目錄中的槽數量 |
PAGE_HEAP_TOP | 2 | 還未私用的空間最小地址,也就是說從該地址之后就是 Free Space |
PAGE_N_HEAP | 2 | 第一位表示本記錄是否為緊湊型記錄,剩15位表示本頁的堆中記錄的數量 |
PAGE_FREE | 2 | 各個已刪除的記錄通過 next_record 組成的一個單向鏈表,這個鏈表中的記錄所占用的空間可以被重新利用;PAGE_FREE 表示該鏈表頭節點對應記錄在頁面中的偏移量 |
PAGE_GARBAGE | 2 | 已刪除記錄占用的字節數 |
PAGE_LAST_INSERT | 2 | 最后插入記錄的位置 |
PAGE_DIRECTION | 2 | 記錄插入的方向 ,如果新插入的記錄主鍵值比上一條記錄大,則認為這個記錄的插入方向是右側,否則是左邊 |
PAGE_N_DIRECTION | 2 | 一個方向連續插入的記錄數量,如果多條記錄連續插入方向相同,則通過該參數記錄下來條數,如果最后一條記錄的插入方向發送改變,則這個狀態的值會清零后重新統計 |
PAGE_N_RECS | 2 | 該頁中用戶記錄的數量(不包括 Infimum 、Supremum以及被刪除的記錄) |
PAGE_MAX_TRX_ID | 2 | 修改當前頁的最大事務ID,該值僅在二級索引頁面中定義 |
PAGE_LEVEL | 2 | 當前頁在B+Tree 中所處的層級 |
PAGE_INDEX_ID | 8 | 索引ID,表示當前頁屬于哪個索引 |
PAGE_BTR_SEG_LEAF | 10 | B+Tree 葉子節點段的頭部信息,僅在 B+Tree 的根頁面中定義 |
PAGE_BTR_SEG_TOP | 10 | B+Tree 非葉子節點段的頭部信息,僅在 B+Tree 的根頁面中定義 |
3. Infimum + Supremum
兩個虛擬的記錄,MySQL硬性規定,Infimum 記錄頁面中最小的記錄,Supremum記錄頁面中最大的記錄。即任何用戶寫的記錄都比 Supremum 記錄小,比 Infimum 記錄大 (類似于鏈表中的頭節點和尾節點),如下圖:
4. User Records + Free Space
一開始生成頁的時候,并沒有 User Records 部分,每當插入一條記錄的時候會從 Free Space 部分(尚未使用的部分)申請一個記錄大小的空間,并將空間劃到 User Records 部分。當 Free Space 部分的空間全部被 User Records 部分替代后,就意味著頁使用完了,需要申請新的頁了。
在 User Records 中,一條條記錄親密無間的排列結構稱為堆。為了方便管理堆,將一條記錄在堆中的相對位置成為head_no, head_no 單調遞增,每條插入的記錄都有其對應的 head_no。
5. Page Directory
記錄在頁中是按照主鍵值由小到大的順序串聯成一個單向鏈表。如果想要根據主鍵值查找頁中的某條記錄,如果從 Infimum 到 Supremum 中查找,則效率太低,因此InnoDB 采用 Page Directory 的方式對記錄做了目錄(利用二分查找的思想)。制作過程如下:
- 將所有正常的記錄劃分為幾個組(包括 Infimum + Supremum 記錄,但不包括已經移除到垃圾鏈表的記錄),分組的規定如下:
- Infimum 記錄分組只能有一條記錄。即只有 Infimum 一條記錄,所以 Infimum 記錄的 n_owned 值為 1, 因為該分組有且只有一條記錄
- Supremum 記錄所在的分組只能有1-8條之間。
- 其他分組中的記錄只能是4-8條之間。
- 每個組的最后一條記錄中的 頭信息中的 n_owned 屬性表示該組中共有幾條記錄。
- 將每組中最后一條記錄在頁面中的地址偏移量(就是該記錄的真實數據與頁面中第0個字節之間的距離)單獨提取出來,按順序存儲到靠近頁尾部的地方。這個地方就是 Page Directory(頁目錄)。頁目錄中的這些地址偏移量成為 槽,每個槽占用 2字節。頁目錄就是由多個槽組成的。
如下圖:
6. File Trailer
占用 8 字節,用于驗證頁寫入是否完整。
四、InnoDB 表空間
所有的數據都被邏輯地存放在一個空間中,稱為表空間(tablespace), 表空間由 段(segment)、區(extent)、頁(page)組成,頁在一些文檔中頁稱為塊(block),如下圖
基礎概念如下:
-
表空間(tablespace) :表空間可以看是做 InnoDB 結構的最高層,所有數據都存放在表空間中。InnoDB 支持多種類型的表空間,而表空間是一個抽象概念,對于系統表空間來說對應著文件系統中的一個或多實際文件;對于每個獨立表空間來說,對應文件系統中一個名為 “表名.ibd” 的實際文件。
在默認情況下 InnoDB 有一個系統表空間 ibdata1,即所有的數據都存放在這個表空間中。而如果設置了 innodb_file_per_table 參數,則可以將每個基于 InnoDB 的表產生一個獨立的表空間。但是需要注意這里的獨立表空間文件進存儲該表的數據、索引和插入緩沖 BITMAP 等,其余信息(如回滾信息、插入緩沖索引頁、系統事務信息、二次寫緩沖等)還是存放在默認表空間中。這也就是說,即使啟用了 innodb_file_per_table 參數,ibdata1的大小仍會增加,并且在事務回滾后大小因為事務而增大的空間并不會被回收(即 ibdata1 不會減小),但是會被判斷這些空間是否還需要,如果不需要則標記為可用空間,供下次使用,那么下次提交事務時 ibdata1 大小則可能不會再增長。 -
段(segment) :表空間由段組成,常見的段有數據段、索引段、回滾段等。段并不對應表空間的某一個連續的物理區域,而是一個邏輯上的概念:B+Tree 的葉子節點和非葉子節點進行區分,否則葉子節點和非葉子節點統統在一個區則查詢效果會大打折扣。因此葉子節點和非葉子節點都有自己獨有的區。而存放葉子節點的區的集合就是一個 段(Leaf node sgement),存放非葉子節點點的區的集合也算是一個段(Non-Leaf node segment)。也就是說一個索引會生成一個 葉子結點段 和一個 非葉子節點段。
-
區(extent) :對于 16KB 的頁來說,連續 64 個頁就是一個區(extent), 即一個區默認占 1MB 空間大小。無論是系統表空間還是獨立表空間,都可以看做是由若干個連續的區組成的,每 256 個區被劃分成一組。 其中第一個組最開始的3個頁時固定的,即 Extend0 這個區最開始3個頁面類型是固定的,其余各組最開始的兩個頁類型是固定的。
總結:表空間被劃分為了許多連續的區,每個區默認由 64 個頁組成,每256個區劃分為一組,每個組最開始的幾個頁面類型是固定的。
在 InnoDB 1.0.x 版本中引入了壓縮頁,即每個頁的大小可以通過參數控制,1.2.x版本可以設置默認頁的大小,但是無論頁的大小怎么變化,區的大小總是1M,無非就是每個區對應的頁的數量不同。在使用了 innodb_file_per_table 參數后創建的表默認大小事 96KB,而區的大小是1M,為了解決這種情況,在每個段開始時先用32個連續頁大小的碎片頁(fragment page) 來存放數據,在使用完這些頁后才是64個連續頁的申請,目的是對于一些小表或者undo這類的段可以在開始時申請較小的空間,節省磁盤容量開銷。(那就會出現,磁盤占用跳躍的情況,即一個區用完后即使插入1KB數據也會增加1M磁盤開銷的情況)
每256個區被劃分成一組,每個組的最開始幾個頁面類型是固定的。
注意:
-
為什么要引入區 (extend) 的概念?
因為要盡量使得頁面鏈表中相鄰的頁的物理位置頁相鄰。這樣掃描葉子節點中大量的記錄時才可以使用順序 IO。如果不使用區,則頁與頁之間物理位置可能離得非常遠,而對于傳統機械硬盤來說,如果雙向鏈表中相鄰的兩個頁物理位置不連續,則需要重新定位磁頭位置。因此引入了 區(extend) 的概念。一個區就是在物理位置上連續的64個頁(區里的頁面的頁號都是連續的)。在表數據非常多的時候,為某個索引分配空間的時候就不再按照頁為單位進行分配,而是以區位單位分配。甚至在表中數據非常非常多的時候,可以一次性分配多個連續的區。雖然可能會造成一些空間浪費(區的空間未填滿)但是可以消除很多隨機 IO。 -
碎片區的概念
默認情況下,一個索引生成兩個段(存放葉子節點的段和存放非葉子節點的段),而段是以區位單位,因此一個段至少有一頁即1M,即一個索引至少占用2M空間?實際并非如此,因為存在碎片(fragment)區的概念。也就是在一個碎片區中,并不是所有的頁都是為了存儲同一個段的數據而存在的,碎片區中的頁可以用于不同的目的,如有些頁可以屬于段A,有些頁可以屬于段B,有些頁不屬于任何段,因此碎片區直屬于表空間,并不屬于任何一個段。因此為某個段分配存儲空間的策略如下:
- 在剛開始向表中插入數據時,段是從某個碎片區以單個頁面為單位來分配存儲空間的。
- 當某個段已經占用了32 個碎片區頁面之后就會以完整的區位單位來分配存儲空間(原先占用的碎片區頁面并不會被復制到新申請的完整的區中。)
因此更精確的來說,段是某些零散的頁面以及一些完整的區的集合。
-
區的分類
狀態名 含義 FREE 空閑區 FREE_FRAG 有剩余空閑頁面的碎片區 FULL_FRAG 沒有剩余空閑頁面的碎片區 FSEG 附屬于某個段的區
-
-
頁(page): 頁也被成為 塊。頁是InnoDB 磁盤管理的最小單位。默認16Kb,可通過參數調整大小,但是調整后不可再修改,除非通過mysqldump 導入導出重新建表。
1. 獨立表空間
1.1 XDES Entry (Extent Descriptor Entry)
每個區都對應著一個 XDES Entry 結構,這個結記錄了對應的區的一些屬性。結構如下:
從上圖看出,每個 XDES Entry 結構有40字節,大致劃分如下:
- Segment ID (8字節) : 每個段都有自己的唯一編號,用Id表示。如果當前區被分配給了某個段,這里保存的就是段該區所在段的ID,如果該區沒被分配給某個段,則該字段值沒有意義。
- List Node(12字節) :這個部分可以將若干個 XDES Entry 結構串成一個鏈表,結構如上圖。如果我們想定位表空間內的某一個位置,只需要指定頁號以及該位置在指定頁號中的頁內偏移量即可。
- State(4字節) :這個字段表名區的狀態。可選值分別是 FREE(空閑區)、FREE_FRAG(有剩余空閑頁面的碎片區)、FULL_FRAG(沒有剩余空閑頁面的碎片區)、FSEG(附屬于某個段的區)
- Page State Bitmap(16字節) :該部分占用16字節,即128位。一個區默認64個頁,即每兩位對應該區中的一個頁,第一位表示是否空閑,第二位還未用到。
1.1.1 XDES Entry 鏈表
當向段中插入數據時,申請新頁面的過程:當段中數據較少時,首先會查看表空間中是否有狀態為 FREE_FRAG 的區(空閑頁面的碎片區),如果找到了則從該區中取出一個零散頁把數據插進去;否則到表空間申請一個狀態為 FREE的區 (空閑的區),把該區的狀態變為 FREE_FRAG ,然后從該新申請的區中取出一個零散頁把數據插入。之后在不同的段使用零散頁的時候都從該區中取,直到該區中沒有空閑頁面;然后該區的狀態就變成了 FULL_FRAG(沒有剩余空閑頁面的碎片區)。
在上述過程中,我們需要知道表空間的區的狀態(FREE、FREE_FRAG、FULL_FRAG以及FSEG),而當表空間不斷增大時,區的數量也會隨之增加,如果通過遍歷區的方式來獲取區的狀態則效率會非常低下(因為當表空間增長到 GB級別,區的數量也會增加到幾千個),所以該部分是通過 XDES Entry 的 List Node 部分來實現 : 通過 List Node 把狀態為 FREE的區對應的 XDES Entry結構連接成一個鏈表,這個鏈表成為 FREE 鏈表,同理還存在 FREE_FRAG 鏈表、FULL_FRAG 鏈表。這樣當想查找一個狀態為 FREE_FRAG 狀態的區時,直接就把 FREE_FRAG 鏈表的頭節點拿出來,從這個節點對應的區中取一些零散頁來插入數據。當這個節點對應的區中沒有空閑頁時,就修改其 State 字段值,然后將其從 FREE_FRAG 鏈表移動到 FULL_FRAG 鏈表中。同理,如果 FREE_FRAG 鏈表中一個節點都沒有,那么直接從 FREE 鏈表中取出一個節點移動到 FREE_FRAG 鏈表,并修改該節點的 State 字段值為 FREE_FRAG ,然后再從這個節點對應的區中獲取零散頁即可。當段中的數據已經占滿32 個零散的頁后,就直接申請完整的區來插入數據了。
綜上:每個段中的區對應的 XDES Entry 結構建立了 3 個鏈表:
- FREE 鏈表:同一個段中,所有頁面都是空閑頁面的區對應的 XDES Entry 結構會被加入到這個鏈表中。需要注意的是此處的 FREE 鏈表并不是直屬表空間的 FREE 鏈表,而是附屬于某個段的鏈表。
- NOT_FULL 鏈表:同一個段中,仍有空閑頁面的區對應的 XDES Entry 結構會被加入到這個鏈表中。
- FULL 鏈表:同一個段中,已經沒有空閑頁面的區對應的 XDES Entry 結構會被加入到這個鏈表中。
即:每一個索引都對應兩個段,每個段都會維護上述三個鏈表。
1.1.2 鏈表基節點
InnoDB通過鏈表基節點(List Base Node)來在表空間中快速定位鏈表嗎,其結構如下:
- List Length : 表名該鏈表中一共有多少節點
- First Node Page Number 和 First Node Offset 表示該鏈表的頭結點在表空間中的位置。
- Last Node Page Number 和 Last Node Offset 表示該鏈表在尾節點在表空間中的位置。
具體結構如下圖:
綜上:表空間是由若干區組成,每個區都對應一個 XDES Entry 結構。直屬于表空間的區對應的 XDES Entry 結構可以分為 FREE、FREE_FRAG 和 FULL_FRAG 三個鏈表。每個段可以擁有若干個區,每個段中的區對應的 XDES Entry 結構可以構成 FREE、NOT_FULL 和 FULL 這三個鏈表。每個鏈表對應一個 List Base Node 結構,這個結構中記錄了鏈表的頭尾節點的位置以及該鏈表中包含的節點數。
1.2 段的結構
段是一個邏輯上的概念,由若干個零散的頁面以及一些完整的區組成。和每個區都有 XDES Entry 類似,每個段中都定義了一個 INODE Entry 結構來記錄這個段中的屬性。
- Segment ID :當前 INODE Entry 結構對應的 段 ID。
- NOT_FULL_N_USED :在 NOT_FULL 鏈表中已經使用了多少頁面
- 3 個 List Base Node :分別為段的 FREE鏈表、NOT_FULL 鏈表、FULL鏈表定義了 List Base Node,這樣當想查找某個鏈表的頭結點和尾節點時,可以直接到這個部分找到對應鏈表的 List Base Node。
- Magic Number :用來標記這個 INODE Entry 是否已經被初始化(即把哥哥字段的值都填進去了)。如果Magic Number = 97937874 則表名已經初始化。
- Fragment Array Entry :由于段是一些零散頁面和一些完整的區的集合。每個 Fragment Array Entry結構都對應著一個零散的頁面,這個結構一共 4字節,表示一個零散頁面的頁號。
整體結構如下圖:
1.3 總結
INODE Entry 與 XDES Entry 的關系圖如下:
每個段都對應一個 INODE Entry 結構,每個 INODE Entry 結構中存在三個 List Base Node 鏈表:分別為段的 FREE鏈表、NOT_FULL 鏈表、FULL鏈表,指向在當前段空間內狀態為 FREE、NOT_FULL 和 FULL 狀態的區的信息。當段進行空間分配的時候,可以通過 List Base Node 鏈表可以快速找到 FULL 或者 NOT_FULL 的區信息并根據區的 XDES Entry 結構確定區中的頁的空閑狀態來分配頁
2. 系統表空間
與獨立表空間相比系統表空間開頭有許多記錄整個系統屬性的頁面。如下:
可以看到系統表空間和獨立表空間的前三個頁面(頁號為 0,1,2,類型為 FSR_HDR、IBUF_BITMAP、INODE)的類型是一致的,但是頁號3-7的頁面是系統表空間獨有的,如下:
頁號 | 頁面類型 | 英文描述 | 描述 |
---|---|---|---|
3 | SYS | Insert Buffer Header | 存儲 Change Buffer 頭部信息 |
4 | INDEX | Insert Buffer Root | 存儲 Change Buffer 的根頁面 |
5 | TRX_SYS | Transaction System | 事務系統的相關信息 |
6 | SYS | First Rollback Segment | 第一個回滾段的信息 |
7 | SYS | Data Directory Header | 數據字典頭部信息 |
五、補充內容
1. CHAR(M) 列的存儲格式
-
在 COMPACT 行格式下,變長字段長度列表值用來存放一條記錄中的變長字段值占用的字節長度,而 CHAR 類型不屬于變長字段類型,理應不應該保存在邊長字段長度列表中。
但上述情況僅僅建立在表使用 ascii 字符集的情況下,因為 ascii 是一個定長字符集,如果采用變長編碼的字符集(也就是表示一個字符需要的字節不確定,比如 gbk 表示一個字符要 1-2 個字節 utf-8 表示一個字符需要 1-3個字節),這種情況下即使是 列是 CHAR 類型,該列的值占用的字節仍讓會被已存儲到變長字段長度列表中。即:對于 CHAR(M) 類型的列來說,當列采用的是定長編碼字符集時,該列占用的字節數不會被加到變長字段長度列表;而如果采用變長編碼的字符集時,該列占用的字節數就會被加到變長字段長度列表。
另外,COMPACT 行格式還規定,采用變長編碼字符集的 CHAR(M) 類型的列要求至少占用 M 個字節,而 VARCHAR(M) 并沒有這個要求。如對于一個采用 utf8 字符集的 CHAR(10) 的列來說,該列的存儲占用的字節長度的范圍就是 10-30字節(因為 utf8 字符集表示一個字符需要1-3個字節),即使存儲的是一個空字符串也會占用10字節。
-
在 REDUNDANT 行格式下,只要使用了 CHAR(M) 類型,該列的真實數據占用的存儲空間大小就是該字符集表示一個字符最多需要的字節數的類型,如對于一個采用 utf8 字符集的 CHAR(10) 的列來說,其真實數據占用的存儲空間大小始終為30字節。
2. 溢出列
一個頁的大小一般是16KB,即16384 個字節,如果某一列數據存儲65532個字節數據,那么一個頁就存放不下,此時就會出現溢出列。對于溢出列的處理,不同的行格式處理方案不同。
- COMPACT 和 REDUNDANT 格式中會在記錄列數據的地方存儲數據中的部分數據:768 個字節 + 20 個指向真正數據頁的指針。如果真正數據頁存儲不下,數據頁會指向下一個數據頁,形成鏈表結構。
- DYNAMIC 和 COMPRESSED 在處理溢出列的方案有些不同,主要是他們不會存儲 768 個字節 + 20個指針,而是直接存儲指向真實數據的20字節的指針。
如下圖:
注意:
- 產生溢出頁的臨界點:MySQL 規定一個頁中至少存放兩行記錄(不是兩列),否則就需要通過溢出列來完成該規定。
- 存放正常記錄的頁和溢出頁時兩種不同的類型,對于溢出頁來說并沒有規定一個頁中至少存放兩條記錄。
六、參考內容
書籍:《MySQL是怎樣運行的——從根兒上理解MySQL》、《MySQL技術內幕 InnoDB存儲引擎 》
如有侵擾,聯系刪除。 內容僅用于自我記錄學習使用。如有錯誤,歡迎指正