1. InnoDB邏輯存儲結構
在InnoDB存儲引擎的邏輯存儲結構中,每一個表下的所有數據都會被放在同一個空間中,這個空間又被稱爲表空間(tablespace)。往下細分,表空間又由段(segment)組成,段由區(extent)組成,區由頁(page,或者被稱爲塊,block)組成,如下圖所示
1.1 表空間
表空間可以看做是InnoDB存儲引擎邏輯結構的最高層,表的所有相關數據都會放在表空間中。默認情況下,InnoDB有一個共享表空間ibdatal,即所有的的數據都會存放在該表空間中,但是如果開啓了配置參數innodb_file_per_table,那麼每張表內的數據會單獨的放在一個表空間內。
但是,即使開啓了innodb_file_per_table配置,也只是將每張表的數據、索引和插入緩衝bitmap頁放在了單獨的表空間而已,而對於其他數據(比如回滾信息、插入緩衝索引頁、系統事務信息、二次寫緩衝等)都還是存放在共享表空間中。所以,即使開啓了innodb_file_per_table配置,共享表空間的大小仍然會不斷增長。
1.2 段
表空間是由段組成的,常見的段有數據段、索引段、回滾段等。數據段就是B+樹的葉子節點,索引段就是B+樹的非葉子節點。而一個段由多個區組成,但並沒有對區的數量限制。
1.3 區
區是有連續的頁組成的空間,在任何情況下每個區的大小都爲1mb,爲了保證區中頁的連續性,innoDB存儲引擎每次都會從磁盤中申請4到5個區大小的完整連續空間,在默認情況下,innoDB存儲引擎的頁的大小爲16kb,即一個區中會有64個連續的頁。
在InnoDB1.0版本開始引入壓縮頁,即每個頁的大小可以通過參數key_block_size設置爲2kb、4kb或8kb,那麼每個區對應輝持有連續頁的數量就是512、256、128。總之,無論頁的大小如何變化,區的大小始終是1mb。
1.4 頁
頁是Innodb磁盤文件管理的最小單位(注意,這裏要和操作系統磁盤操作的最小單位區分開,大部分操作系統一次讀取的最小單位基本都是4kb,可以去了解一下操作系統中的文件管理,扇區、簇、頁,其實與數據庫系統很相似的,所以對應的數據庫系統的磁盤文件管理的最小單位基本都是操作系統文件管理最小單位的倍數)。在InnoDB存儲引擎中,默認每個頁的大小爲16kb。從InnoDB1.2以後,可以通過參數innodb_page_size設置頁的大小爲4k、8k或16k。若設置完成,則飆中所有頁的大小都爲設置的數值,而且不可以再次修改。
在Innodb中,常見的頁類型有:
(1)數據頁(B-tree Node)。
(2)Undo頁(Undo Log Page)。
(3)系統頁(System Page)。
(4)事務數據頁(Transaction system Page)。
(5)插入緩衝位圖頁(Insert Buffer Bitmap)。
(6)插入緩衝空閒列表頁(Insert Buffer Free List)。
(7)未壓縮的二進制大對象頁(Uncompressed BLOB Page)。
(8)壓縮的二進制大對象頁(Compressed BLOB Page)。
1.5 行
innodb存儲引擎是面向行的,也就是說數據是按照行進行存放的。每個頁存放的行記錄也是有數量限制的,最多允許存放(16kb/2)-200,也就是7992行數據。
2. innodb行記錄的格式
InnoDB存儲引擎是面向行存儲數據的,這意味着在數據頁中保存着表中一行行的數據。在InnoDB中,一共有兩種格式來存放行記錄,分別是Compact和Redundant,Redundant是爲了兼容老版本的數據格式而保留的,MySQL5.1之後基本都是默認爲Compact格式或者是基於Compact的格式(Dynamic)來存儲行記錄。
2.1 Compact行記錄
Compact行記錄的出現提高了MySQL的存儲性能,簡單來說,一個頁中存儲的行數據越多,那麼其性能就越高,原因是爲什麼,這個涉及到索引以及B+樹的問題(後續在索引中具體進行講解),簡單來說就是一個表的行記錄格式決定表的行物理存儲模式,決定query和dml操作性能,越多的行匹配進獨立的磁盤頁,query和index查找會快一些,需要的緩存及io操作就會少一些(專業術語來說,就是在其他博文中常見到的扇入扇出)。存儲結構如下圖所示
(1)依據上圖中我們可以看到,Compact行記錄格式的首部是一個非Null變長字段長度列表,並且是按照列的順序逆序放置的,若列(字段)的長度小於255字節,用1字節表示。若列的長度大於255字節,用2字節表示。變長字段的字節長度最大不可以超過2字節,這是因爲MySQL數據庫中Varchar類型的數據最大長度限制爲65535字節。
(2)變長字段之後的第二個部分是NULL標誌位,該位表示了該行數據中是否有NULL值,有則用1表示,佔一個字節。
(3)記錄頭信息:固定佔用5字節,每位的含義見下表
名稱 | 大小 (bit) | 描述 |
---|---|---|
預留位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 | 表示下一條記錄的相對位置 |
(4)後面的就是實際存儲的每個列的數據,在每個列的存儲數據中,NULL不佔該部分任何空間。此外還有兩個隱藏列,事務ID列和回滾指針列,分別爲6字節和7字節。若innodb表沒有定義主鍵,每行還會增加一個6字節的rowid列。
2.2 Redundant行記錄格式
redundant是MySQL5.0之前的InnoDB的行記錄存儲方式,MySQL5.0支持Redundant是爲了兼容之前版本的頁格式,其行記錄結構如下圖所示
Redundant行記錄格式的首部是一個字段長度偏移列表,同樣是按照列的順序逆序放置的。同樣的,若表中各列的長度之和小於255字節,用1字節表示;若大於255字節,用2字節表示。
第二個部分是記錄頭信息,佔用6字節,每個比特位的含義如下所示。其中n_fields值代表的就是一行中列的數量,佔用10位,這也是爲什麼mysql中一張表的字段數量最多爲1023個。
後續的數據就是每個列的具體數據了。
名稱 | 大小 (bit) | 描述 |
---|---|---|
預留位1 | 1 | 沒有使用 |
預留位2 | 1 | 沒有使用 |
delete_mask | 1 | 標記該記錄是否被刪除 |
min_rec_mask | 1 | B+樹的每層非葉子節點中的最小記錄都會添加該標記 |
n_owned | 4 | 表示當前記錄擁有的記錄數 |
heap_no | 13 | 表示當前記錄在頁面堆的位置信息 |
n_field | 10 | 表示記錄中列的數量 |
1byte_offs_flag | 1 | 標記字段長度偏移列表中每個列對應的偏移量是使用1字節還是2字節表示的 |
next_record | 16 | 表示下一條記錄的相對位置 |
2.3 行溢出數據
InnoDB會將一條記錄中的某些數據存儲在真正的數據頁之外,比如BLOB、TEXT這類大對象列類型的存儲會把數據存放在數據頁面之外。但是,這個說法其實不完全正確,BLOB也有可能會把數據放在溢出頁面,而且即便是VARCHAR類型的數據列,也有可能被存放在行溢出數據。
首先可以看看爲什麼VARCHAR可能會被存儲在行溢出數據之中。首先,在MySQL中定義VARCHAR類型的字段時,VARCHAR字段的限制空間大小爲65535字節,但是通過實驗發現,其實並不能支持定義一個65535字節長度的字段,如下圖所示
導致報錯的原因有多個:
(1)如果將SQL_MODE設置爲嚴格模式,建表語句中如果有65535字節長度的的VARCHAR,肯定會報錯的,因爲肯定還會有一些其他開銷,實際上最多存儲65532字節的數據。如果沒有設置爲嚴格模式,那麼數據庫會自動的將該列的數據類型轉爲TEXT類型,這樣就不會報錯了。
(2)VARCHAR後面跟的限制數量,實際上並不是指的字節數量,而是指字符數量,那麼這就導致了一個問題:如果字符編碼使用的是Latin1字符集,那麼這麼說是沒錯的,因爲Latin1字符集中1個字符佔用1個字節;而如果是採用UTF-8字符集,那麼就是有問題的了,因爲UTF-8是採用了3個字節空間來表示一個字符(所以最大限制爲21845),而GBK用2個字節空間來表示字符(最大32767),所以在不同的字符集編碼規則下,VARCHAR的長度限制也是不同的。
(3)最重要的一點,這個65535實際上並不是指某一列的字節長度限制,而是指所有VARCHAR列的長度之和要小於65535字節。也就是說,如果你定義了三個VARCHAR列,每個列的限制長度爲2400,那麼同樣也會報錯,無法建表。
搞清楚VARCHAR列的65535字節限制到底是限制了什麼,再來看看另一個問題:mysql中一個數據頁最大爲16kb,也就是16384字節,那麼爲什麼一個數據頁能存儲65532個字節的數據呢?這就涉及到InnoDB對於行溢出數據數據的處理方式。
一般情況下,InnoDB引擎的數據都是存放在B tree Node類型的數據頁中,但是當發生行溢出時,數據就會存放在UNcompress Blob類型的數據頁中。而相應的在原數據頁的行記錄中(Compact 和 Reduntant兩種格式 ),'記錄的真實數據' 處只會存儲一部分 (768 字節的) 數據,剩下的數據存儲在幾個其他的頁 (溢出頁) 中 (以鏈表的方式連接),在 '記錄的真實數據' 處用 20 個字節存儲這些頁的地址 。如下圖所示
其實換句話說,如果數據頁中一行記錄的大小超過了頁的大小,那麼該行數據的完整數據一定會被放在uncompress blob類型的數據頁中,但一定要注意,並不是說如果插入的一行數據大於頁的空間,那麼該頁就會只存放這一行記錄,原因就在於InnoDB是將數據按照B+樹結構進行的存儲,那麼每個數據頁中至少會存放兩條行記錄,如果一個數據頁只存放一行數據,那麼整個數據結構幾乎就變成了鏈表,這樣也就失去了B+樹的意義(這一塊就涉及到平衡二叉樹爲什麼要優於普通二叉樹)。所以,對於行溢出數據主體會存儲在uncompress blob頁中,這樣就能保證每個數據頁(B tree Node)中能夠存儲多條行記錄。同樣的,對於BLOB這種類型的數據,如果長度不足以發生行溢出的話,那麼數據仍然還是會存儲在數據頁中,但如果發了行溢出,那麼存儲方式還是和上面所說的一樣。
InnoDB1.0版本之後又引入了兩種新的行記錄存儲格式,分別是compressed和dynamic。這兩種數據格式對於行溢出數據的處理則採用了不同的方式,這兩種格式在發生行溢出的時候,數據頁中只存放20個字節的指針,該指針會指向實際存放數據的off page中,而且對於BLOB會完全採用行溢出處理,無論其長度是否會造成行溢出。而且對於Compressed的另一個功能就是,存儲在其中的行數據會以zlib算法壓縮數據,因此對於BLOB、TEXT這類大類型數據有效存儲。
通常我們都知道,VARCHAR是變長字符類型,而CHAR是定長字符類型。但必須要記住一點,這個定長和變長指的是字符數量,而不是字節數量。
前面也說過了,在不同的編碼字符集下,每個字符佔用的空間大小是不同的,在Latin1編碼格式中一個字符只有1字節,但是在UTF-8中一個字符就佔了3字節。所以,一定要搞清楚這個定長定的是什麼的數量。
3 InnoDB數據頁的結構(大概瞭解即可)
頁是InnoDB存儲引擎管理數據庫的最小磁盤單位,頁類型爲B-tree Node的頁存放的就是表中行記錄的數據。一個頁的大小一般都是16k,InnoDB每次從磁盤中將數據讀取到內存中時,也都是整頁的讀取。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頁 |
其中數據頁由以下幾個部分組成:結構圖如下
(1)File Header (文件頭)
(2)Page Header(頁頭)
(3)Infimun和Supremum Records
(4)User Records(用戶記錄,即行記錄)
(5)Free Space(空閒空間)
(6)Page Directory(頁目錄)
(7)File Trailer(文件結尾信息)
InnoDB的頁結構分爲七個部分,各個部分對應的作用如下面的表格所示:
名稱 | 中文名 | 佔用空間大小 | 簡單描述 |
---|---|---|---|
File Header | 文件頭 | 38字節 | 描述頁的信息 |
Page Header | 頁頭 | 56字節 | 頁的狀態信息 |
Infimum + SupreMum | 最小記錄和最大記錄 | 26字節 | 兩個虛擬的行記錄(後面會說明) |
User Records | 用戶記錄 | 不確定 | 實際存儲的行記錄內容 |
Free Space | 空閒空間 | 不確定 | 頁中尚未使用的空間 |
Page Directory | 頁目錄 | 不確定 | 頁中的記錄相對位置 |
File Trailer | 文件結尾 | 8字節 | 結尾信息 |
3.1 File Header
File Header用來記錄頁的一些頭信息,由8個部分組成,共佔用38字節,大小固定不變,如下表所示:
名稱 | 大小(字節) | 說明 |
FIL_PAGE_SPACE_OR_CHKSUM | 4 | 當 MySQL爲 MySQL40.14之前的版本時,該值爲0。在之後的 MySQL版本中,該值代表頁的 checksum值(一種新的 checksum值) |
FIL_PAGE_OFFSET | 4 | 表空間中頁的偏移值。如某獨立表空間a.ibd的大小爲1GB,如果頁的FIL_PAGE_OFFSET大小爲16KB,那麼總共有65536個頁。 FIL_PAGE_OFFSET表示該頁在所有頁中的位置。若此表空間的ID爲10,那麼搜索頁(10,1)就表示查找表a中的第二個頁 |
FIL_PAGE_PREV | 4 | 當前頁的上一個頁,B+Tree特性決定了葉子節點必須是雙向列表 |
FIL_PAGE_NEXT | 4 | 當前頁的下一個頁,B+Tree特性決定了葉子節點必須是雙向列表 |
FIL_PAGE_LSN | 8 | 該值代表該頁最後被修改的日誌序列位置LSN( Log Sequence Number) |
FIL_PAGE_TYPE | 2 | InnoDB存儲引擎頁的類型。常見的類型見下表。記住0x45BF,該值代表了存放的是數據頁,即實際行記錄的存儲空間 |
LSN_FLUSH_LSN | 8 | 該值僅在系統表空間的一個頁中定義,代表文件至少被更新到了該LSN_FLUSH_LSN值。對於獨立表空間,該值都爲0 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 從 MySQL4.1開始,該值代表頁屬於哪個表空間 |
3.2 Page Header
Page Header用來記錄數據頁的狀態信息,由14個部分組成,共佔用56個字節,大小固定不變。如下表所示
名稱 | 大小(字節) | 說明 |
PAGE_N_DIR SLOTS | 2 | 在 Page Directory(頁目錄)中的Slot(槽)數,“Page Directory”小節中會介紹 |
PAGE_HEAP_TOP | 2 | 堆中第一個記錄的指針,記錄在頁中是根據堆的形式存放的 |
PAGE_N_HEAP | 2 | 堆中的記錄數。一共佔用2字節,但是第15位表示行記錄格式 |
PAGE_FREE | 2 | 指向可重用空間的首指針 |
PAGE_GARBAGE | 2 | 已刪除記錄的字節數,即行記錄結構中 delete flag爲1的記錄大小的總數 |
PAGE_LAST_INSERT | 2 | 最後插入記錄的位置 |
PAGE_DIRECTION | 2 | 最後插入的方向。可能的取值爲: PAGE_LEFT(0x01) PAGE_RIGHT(0x02) PAGE_DIRECTION PAGE_SAME_REO(0x03) PAGE_SAME_PAGE(0x04) PAGE_NO_DIRECTION (Ox05) |
PAGE_N_DIRECTION | 2 | 一個方向連續插人記錄的數量 |
PAGE_NRECS | 2 | 該頁中記錄的數量 |
PAGE_MAX_TRX_ID | 8 | 修改當前頁的最大事務ID,注意該值僅在 Secondary Index中定義 |
PAGE_LEVEL | 2 | 當前頁在索引樹中的位置,0x00代表葉節點,即葉節點總是在第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頁中定義 |
3.3 Infimum和Supremum Record
在InnoDB存儲引擎中,每個數據頁中都有兩個虛擬的行記錄,用來限定記錄的邊界。Infimum行記錄是比該頁中任何主鍵值都要小的值,Supremum指比任何可能大的值還要大的值。這兩個值在頁創建的時候被建立,並且在任何情況下都不會被刪除。
3.4 User Records和Free Space
User Records就是指存儲記錄的地方和Free Space就是指頁中的剩餘空閒空間,我們在存儲數據的時候,記錄會存儲到User Records部分 。但是在一個頁新形成的時候是不存在User Records這個部分的,每當我們在插入一條記錄的時候,都會從Free Space中去申請一塊大小符合該記錄大小的空間並劃分到User Records,當Free Space的部分空間全部被User Records部分替換掉之後,就意味着當前頁使用完畢,如果還有新的記錄插入,需要再去申請新的頁,過程如下:
3.5 Page Directory
Page Directory(頁目錄)中存放了行記錄的相對位置(存放的是在頁中的相對位置,而不是偏移量),這些記錄指針也被稱爲Slots(槽)或目錄槽,在InnoDB中並不是每個記錄擁有一個槽位,InnoDB存儲引擎的槽位是一個稀疏目錄,即一個槽位可能包含頁中多個行記錄的指針。怎麼說呢,其實很類似於二分法查找(或者說二叉樹查找),頁中的所有行記錄其實就是一個數組,而在page directory中存儲的就是其中的幾條比較標誌性的行記錄指針,比如說[a,b,c,d,e,f,g,h,i,l]是所有的行記錄(相當於一個有序鏈表),那麼page directory中就包含3條指針,分別指向行記錄[a, e, l],當查詢某一個具體行記錄時,只需要於page directory中的指針行記錄進行比較,就可以然後一個粗略的結果或者說區間範圍,比如說查找行記錄數據 g ,那麼就會得到一個區間範圍是e到l,然後從行記錄e開始向後遍歷進行查找就可以得到具體的目標行記錄數據。
3.6 File Trailer
爲了檢測頁是否完整的寫入了磁盤,InnoDB中設置了頁的File Trailer部分,File Trailer只有一部分內容file_page_end_lsn,佔用8字節。前4個字節代表該頁的checksum值,最後的4個字節代表該頁最後被修改的日誌序列位置。將這兩個值與File Header中的file_page_space_or_chksum和file_page_end_lsn兩個值進行比較是否一致,以此判斷頁的完整性,當然不是簡單的等值比較法,有一套特殊的比較算法。