通過閱讀本文可以瞭解什麼?
- 磁盤預讀概念
- 數據頁結構
- innoDb的行格式
- 行溢出
- VARCHAR(65535) 大小的列能不能創建
- 怎麼定位到一條數據
- 爲什麼儘可能設置爲非NULL字段
從InnoDB存儲引擎的邏輯存儲結構來看,所有的數據都被邏輯邏輯的存放在一個空間中,成爲表空間。表空間又由 段、區、頁組成。本文着重將頁和行記錄相關的數據
磁盤預讀
mysql服務器是我們在生產環境中常用的服務器,大部分的存儲引擎數據都是寫入到磁盤中的而服務器每次讀取數據時會先從pagecache
讀取數據,如果pagecache
中沒有,需要從磁盤進行讀取。
我們知道當我們從磁盤讀取數據時,需要經過磁盤尋址加旋轉的過程,相對於從緩存中讀數據來說是很慢的,所以爲了保證數據的讀取效率,mysql服務器不是按需讀取,而是進行磁盤預讀
預讀的長度一般爲頁(page)的整倍數。磁盤會找到數據的起始位置並向後連續讀取一頁或幾頁載入內存中。
數據頁結構
定義
頁是計算機管理存儲器的邏輯塊,硬件及操作系統往往將主存和磁盤存儲區分割爲連續的大小相等的塊,每個存儲塊稱爲一頁(在許多操作系統中,頁得大小通常爲4k),主存和磁盤以頁爲單位交換數據。
InnoDB中的實現
InnoDB中頁的大小一般爲 16 KB,也就是說,當需要從磁盤中讀數據時 每一次最少將從磁盤中讀取16KB的內容到內存中,每一次最少也會把內存中的16KB內容寫到磁盤中。
默認的頁大小爲 16KB,每個頁中至少存儲有 2 條或以上的行記錄
從InnoDB 1.2X開始 我們可以通過參數 innodb_page_size
將頁的大小設置爲4K、8K、16K
頁的類別
- 數據頁(B-tree Node)
- undo 頁 (undo Log Page)
- 系統頁 (System Page)
- 事物數據頁(Transaction system page)
- 插入緩衝空閒列表頁 (Insert Buffer Free List)
- 插入緩衝位圖頁(Insert Buffer Bitmap)
- 未壓縮的二進制大對象頁(UnCompresser BLOB Page)
- 壓縮的二進制大對象頁(Compressed BLOB Page)
查看默認頁的大小
SHOW GLOBAL STATUS like 'Innodb_page_size';
頁的結構
各模塊作用
名稱 | 佔用空間 | 描述 |
---|---|---|
File Header | 38字節 | 頁的一些通用信息 |
Page Header | 56字節 | 數據頁專有的一些信息 |
Infimum + Supremum | 26字節 | 兩個虛擬的行記錄 |
User Records | 不確定 | 實際存儲的行記錄內容 |
Free Space | 不確定 | 頁中尚未使用的空間,被刪除的行記錄會被記錄成空閒空間 |
Page Directory | 不確定 | 頁中的某些記錄的相對位置,二叉查找相關的信息 |
File Trailer | 8字節 | 存儲用於檢測數據完整性的校驗和等數據 |
File Heade
各種頁的通用信息,固定爲38個字節
名稱 | 佔用空間大小(字節) | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | 頁的校驗和(checksum值) |
FIL_PAGE_OFFSET | 4 | 頁號,InnoDB通過頁號來可以唯一定位一個頁 |
FIL_PAGE_PREV | 4 | 上一個頁的頁號 |
FIL_PAGE_NEXT | 4 | 下一個頁的頁號 |
FIL_PAGE_LSN | 8 | 頁面被最後修改時對應的日誌序列位置(Log Sequence Number) |
FIL_PAGE_TYPE | 2 | 該頁的類型 記住0x45BF 代表數據頁 |
FIL_PAGE_FILE_FLUSH_LSN | 8 | 僅在系統表空間的一個頁中定義,代表文件至少被刷新到了對應的LSN值 對於獨立表空間該值都爲0 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 頁屬於哪個表空間 |
其中比較重要的是 FIL_PAGE_PREV 和 FIL_PAGE_NEXT 字段,通過這兩個字段,我們可以找到該頁的上一頁和下一頁,實際上所有頁通過兩個字段可以形成一條雙向鏈表
並不是所有頁都有這個屬性的,但是索引頁有。
Page Header
Page Header 字段用於記錄 Page 的狀態信息
如下結構僅針對索引頁
名稱 | 佔用空間大小(字節) | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS | 2 | 在頁目錄中的槽數量 |
PAGE_HEAP_TOP | 2 | 還未使用的空間最小地址,也就是說從該地址之後就是Free Space |
PAGE_N_HEAP | 2 | 本頁中的記錄的數量(包括最小和最大記錄以及標記爲刪除的記錄) |
PAGE_FREE | 2 | 第一個已經標記爲刪除的記錄地址(各個已刪除的記錄通過next_record也會組成一個單鏈表,這個單鏈表中的記錄可以被重新利用) |
PAGE_GARBAGE | 2 | 已刪除記錄佔用的字節數 |
PAGE_LAST_INSERT | 2 | 最後插入記錄的位置 |
PAGE_DIRECTION | 2 | 記錄插入的方向 |
PAGE_N_DIRECTION | 2 | 一個方向連續插入的記錄數量 |
PAGE_N_RECS | 2 | 該頁中記錄的數量(不包括最小和最大記錄以及被標記爲刪除的記錄) |
PAGE_MAX_TRX_ID | 8 | 修改當前頁的最大事務ID,該值僅在二級索引中定義 |
PAGE_LEVEL | 2 | 當前頁在B+樹中所處的層級 |
PAGE_INDEX_ID | 8 | 索引ID,表示當前頁屬於哪個索引 |
PAGE_BTR_SEG_LEAF | 10 | B+樹葉子段的頭部信息,僅在B+樹的Root頁定義 |
PAGE_BTR_SEG_TOP | 10 | B+樹非葉子段的頭部信息,僅在B+樹的Root頁定義 |
PAGE_DIRECTION:
假如新插入的一條記錄的主鍵值比上一條記錄的主鍵值小,我們說這條記錄的插入方向是左邊,反之則是右邊。
PAGE_DIRECTION用來表示最後一條記錄插入方向的狀態。
PAGE_N_DIRECTION:
假設連續幾次插入新記錄的方向都是一致的,InnoDB會把沿着同一個方向插入記錄的條數記到PAGE_N_DIRECTION這個狀態上。
如果最後一條記錄的插入方向改變了,這個狀態的值會被清零。
Infimum 和 Supremum
Infimum 和 Supremum 是兩個僞行記錄,Infimum(下确界)記錄比該頁中任何主鍵值都要小的值,Supremum (上確界)記錄比該頁中任何主鍵值都要大的值,這個僞記錄分別構成了頁中記錄的邊界。
Page Directory
存儲二分查找槽的信息 (頁的相對位置而不是偏移量)
- 將所有正常的記錄劃分爲幾個組。
- 每個組的最後一條記錄(也就是組內最大的那條記錄)的頭信息中的n_owned屬性表示該記錄擁有多少條記錄,也就是該組內共有幾條記錄
- 將每個組的最後一條記錄的地址偏移量(slot)單獨提取出來按順序存儲到Page Directory
- 最小記錄所在的分組只能有 1條記錄,最大記錄所在的分組擁有的記錄條數只能在 1~8 條之間,剩下的分組中記錄的條數範圍只能在是 4~8 條之間即Infimum的n_owned=1,Supremum的n_owned=[1,8],其他的用戶記錄n_owned=[4,8]
按主鍵查找的順序
1.先通過二分查找定位主鍵所在的slot
2.找到slot中最小的行記錄
3.根據記錄上的next_record屬性遍歷整個slot
4.返回
需要牢記的是B+數索引本身並不能找到具體一條記錄,能找到的是該記錄所在的頁。數據庫把頁加載到內存,然後通過 Page Directory 再進行二分查找。只不過二分查找的複雜度低,同時在內存中的查找很快,因此通常忽略這部分查找所用的時間
行
InnoDB存儲引擎是面向列的。也就是說數據是按行存儲的。
每個頁存放的行記錄也是有硬性定義的,最多允許存放 16K/2-200行的記錄,即7992行記錄。
行格式
- Compact
- Redundant
- Dynamic
- Compressed
通過執行show table status like 'tableName'
來查看行格式
指定行格式
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名稱
ALTER TABLE 表名 ROW_FORMAT=行格式名稱
Compact行格式
從上圖可以看到,一行記錄邏輯上主要分爲兩部分:
- 額外信息
- 記錄的真實數據
記錄的額外信息
主要包含三部分:變成字段列表,NULL值列表,記錄頭信息
變成字段長度列表
變長字段列表中存儲多少字節的數據不是固定的,實際長度取決於列數和每一列的長度。
變成字段定義
比如由 VARCHAR(M)、VARBINARY(M)、TEXT類型,BLOB類型等數據類型修飾的列稱爲變長字段
注意VARCHAR(M),M代表最大能存多少個字符。( MySQL5.0.3以前是字節,以後就是字符)
字節長度需要跟編碼方式相關聯,例如 UTF-8 一箇中文字符需要 3 字節來表示,ascii一個字符就是一個字節
作用
記錄變長數據的真實字節數
在Compact行格式中,把所有變長字段的真實數據佔用的字節長度都存放在記錄的開頭部位,從而形成一個變長字段長度列表。
特點
1.變長字段實際佔用字節數以逆序方式存儲在變長字段長度列表中
2.允許的最大字節超過255且實際存儲超過127字節, 使用兩個字節存儲其長度, 否則使用一個字節
InnoDB在讀字段變長列表時會先查表結構, 允許的最大字節數超過255時纔會使用這個二進制位作爲標識位來判斷是讀一個字節還是兩個字節,沒有超過就直接讀一個字節也就不存在標識位了
變長字段的長度最大不可以超過兩字節
3.變長列不爲NULL時, InnoDB纔會存儲其字節長度
4.如果沒有變長字段或爲NULL,則當前記錄沒有此部分
5.第一個字節的第一位是標誌位,表示是否雙字節表示
NULL值列表
一個字節 一共8位
作用
如果有的列可以爲NULL,那麼 Compac行格式會把這些NULL字段統一管理起來,存一個標誌位在NULL值列表中。
- 二進制的1 代表該列爲NULL
- 二進制的0 代表該列非NULL
- 逆序存放
如果一個表中的列都不允許爲NULL 則不會存在這個列表
在建表時,字段進行都設置爲非NULL,這樣就可以節省這一部分空間了
記錄頭信息
記錄頭爲定長結構,長度爲5字節,但是每個位都代表不同的含義
作用
如下用來描述記錄的相關信息
名稱 | 長度(bit) | 作用 |
---|---|---|
預留位1 | 1 | 保留 |
預留位2 | 1 | 保留 |
delete_mask | 1 | 標記該記錄是否被刪除 |
min_rec_mask | 1 | 標記B+樹中每層非葉子節點的最小記錄 (Mysql技術內幕寫的是:如果該記錄是被預先定義爲最小記錄) |
n_owned | 4 | 當前記錄擁有的記錄數(這個組有多少條記錄) |
heap_no | 13 | 當前記錄在堆中的位置 |
record_type | 3 | 000:普通記錄,001:B+樹非葉子節點記錄,010:最小記錄,011:最大記錄 1xx:保留 |
next_record | 16 | 下一條記錄的相對位置 |
一個頁中的所有記錄使用next_record 字段形成了一條單鏈表
記錄的真實數據
記錄的真實數據中除了存放真實數據之外,還有三個隱藏列
名稱 | 長度(字節) | 作用 | 是否必須 |
---|---|---|---|
DB_ROW_ID | 6 | 唯一標示一條記錄 | 否 |
DB_TRX_ID | 6 | 事務ID | 是 |
DB_ROLL_PTR | 7 | 回滾指針,指向一條undo日誌記錄 | 是 |
只有當一個表沒有手動定義主鍵,且沒有Unique鍵時,纔會爲表默認添加一個名爲row_id(DB_ROW_ID)的隱藏列作爲主鍵。
NULL不佔該部分的任何空間,即NULL除了佔有NULL標誌位,實際存儲不佔有任何空間
行溢出數據
mysql中一條記錄佔用的最大存儲空間是有限制的。除了Text和Blob大字段外,其餘的字段(不包括隱藏列和記錄頭信息)加起來不能超過65535個字節。myisam存儲引擎不受此限制
我們知道數據頁的大小是 16KB即16384字節,Innodb 存儲引擎保證了每一頁至少有兩條記錄
一般情況下數據都存放在頁類型爲 B-Tree node中,但是當發生了行溢出時,會截取前768字節的前綴數據和每個溢出頁的偏移量存放在B-Tree node中剩下的數據存放在多個類型爲Uncompressed BLOB頁中。
所以可能會存在一個頁放不完數據的情況,針對這種情況,不同的行格式處理的方式不同
Compact和Reduntant
對於佔用存儲空間非常大的列,在記錄的真實數據處只會存儲該列的一部分數據,把剩餘的數據分散存儲在幾個其他的頁中。然後記錄的真實數據處用20個字節存儲指向這些頁的地址,從而可以找到剩餘數據所在的頁。
20個字節中還包含每頁的大小
Dynamic和Compressed
它們不會在記錄的真實數據處存儲一部分數據,而是把所有的數據都存儲到其他頁面中,只在記錄的真實數據處存儲其他頁面的地址。
Dynamic和Compressed行格式除了上述說的處理溢出問題時和別的格式不一樣,其他的都幾乎一樣
Compressed行格式會採用壓縮算法對頁面進行壓縮。
注意
Mysql數據庫的VARCHAR沒辦法存儲65535字節的實際數據,這是因爲還有別的開銷
如果表中只有這一個字段
- 非null情況下 不定長度列表佔2字節 所以 VARCHAR的大小爲 65532
- null的情況 還要考慮一個null標誌位 1字節 所以 VARCHAR的大小爲 65531
文當中說的支持最大VARCHAR(65535),單位是字節。
65535 是說的所有的 VARCHAR 列的總和