InnoDB頁
InnoDB是一個將數據存儲到磁盤上的存儲引擎,所以就算我們關閉、重啟服務器,數據還是存在的。而在真正處理數據的時候是在內存中進行的,所以需要把磁盤中的內容加載到內存中。
我們知道讀寫磁盤是很慢的。當我們想從表里獲取數據的時候,InnoDB會一條一條的從磁盤中讀出來嗎?不會的!因為那樣太慢了。它采取的方式是:將數據劃分為若干頁,以頁做為磁盤和內存交互的基本單位。InnoDB中頁的大小一般為16KB。
在服務器運行的過程中不可以修改頁的大小,只能在初始化數據目錄的時候指定。
InnoDB 行格式
行格式有哪些
行格式(row_format):一條數據記錄在磁盤上的存儲結構。
InnoDB 提供了 4 種行格式,分別是 Redundant、Compact、Dynamic和 Compressed 行格式。
我們可以在創建表或者修改表的語句中指定所使用的行格式
create table 'table info ..' row_format = '行格式名稱'
alter table 'table name' row_format = '行格式名稱'
- Redundant:是很古老的行格式了, MySQL 5.0 版本之前用的行格式,現在基本沒人用了。
- Compact:由于 Redundant 不是一種緊湊的行格式,所以 MySQL 5.0 之后引入了 Compact 行記錄存儲方式,Compact 是一種緊湊的行格式,設計的初衷就是為了讓一個數據頁中可以存放更多的行記錄,從 MySQL 5.1 版本之后,行格式默認設置成 Compact。
- Dynamic 和 Compressed 兩個都是緊湊的行格式,它們的行格式都和 Compact 差不多,因為都是基于 Compact 改進一點東西。從 MySQL5.7 版本之后,默認使用 Dynamic 行格式。
Redundant 行格式因為現在基本沒人用了,重點介紹 Compact 行格式,因為 Dynamic 和 Compressed 這兩個行格式跟 Compact 非常像。
Compact 格式
話不多說,直接看圖
記錄額外的信息
這部分信息是服務器為了更好的管理記錄而不得不額外添加的一些信息,這些額外信息分為三個部分,分別是:變長字段長度列表、NULL值列表和記錄頭信息。
- 變長字段長度列表
在mysql中有一些變長的數據類型,比如varchar( )、varbinary( )、text類型、blob類型,我們把使用這個變長類型的列成為變長字段。
所以,在存儲數據的時候,也要把數據占用的大小存起來,存到「變長字段長度列表」里面,讀取數據的時候才能根據這個「變長字段長度列表」去讀取對應長度的數據。
這些變長字段的真實數據占用的字節數會按照列的順序逆序存放(后面會說為什么要這么設計)。
為了展示具體是怎么保存「變長字段的真實數據占用的字節數」,我們先創建這樣一張表,字符集是 ascii(所以每一個字符占用的 1 字節),行格式是 Compact,student 表中 name 和 dream_school 字段是變長字段:
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` VARCHAR(20) DEFAULT NULL,
`dream_school` VARCHAR(20) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB DEFAULT CHARACTER SET = ascii ROW_FORMAT = COMPACT;
我們插入三條記錄
我們看看這三條記錄的行格式中的 變長字段長度列表 是怎樣存儲的。
先來看第一條記錄:
- name 列的值為 1,真實數據占用的字節數是 1 字節,十六進制 0x01;
- dream_school 列的值為 qinghua,真實數據占用的字節數是 7 字節,十六進制 0x07;
- age 列和 id 列不是變長字段,這里不用管。
再來看第二條
- name 列的值為 1,真實數據占用的字節數是 2 字節,十六進制 0x02;
- dream_school 列的值為 beida,真實數據占用的字節數是 5 字節,十六進制 0x05;
第三條記錄
第三條記錄 dream_school 列的值是 NULL,NULL 是不會存放在行格式中記錄的真實數據部分里的。
- null值列表
一條記錄中的某些列可能存儲 NULL 值,如果把這些 NULL 值都放到記錄的 真實數據中存儲會很占地方,所以 OMPACT 行格式把一條記錄中值為 NULL 的列統一管理起來,存儲到 NULL 值列表中.
處理過程:
- 首先統計表中允許存儲 NULL 的列有哪些,主鍵列以及使用 NOT NULL 修飾的列都是不可以存儲 NULL 值的,所以在統計的時候不會把這些列算進去。
- 如果表中沒有允許存儲 NULL 的列,則 NULL 值列表也就不存在了,否則將每個允許存儲 NULL 的列對應一個 進制位,二進制位按照列的順序逆序排列。二進制位表示的意義如下
-
- 進制位的值為1時,代表該列的值為NULL。
- 迸制位的值為0時,代表該列的值不為NULL。
- 另外,NULL 值列表必須用整數個字節的位表示(1字節8位),如果使用的二進制位個數不足整數個字節,則在字節的高位補 0。
先來看第一條記錄
第一條記錄所有列都有值,不存在 NULL 值,用二進制來表示是這樣的:
第二條記錄
接下來看第二條記錄,第二條記錄 age 列是 NULL 值,用二進制來表示是這樣的:
第三條記錄
第三條記錄 dream_school 列 和 age 列是 NULL 值,用二進制來表示是這樣的:
- 記錄頭信息
記錄頭信息由固定5個字節組成,用于描述記錄的一些屬性,這里的屬性比較多,我們就列舉幾個相對來說重要點的。
- deleted_flag :標識此條數據是否被刪除。從這里可以知道,我們執行 detele 刪除記錄的時候,并不會真正的刪除記錄,只是將這個記錄的 delete_mask 標記為 1。
- next_record:下一條記錄的位置。所以我們可以知道,記錄與記錄之間是通過鏈表組織的。在前面我也提到了,指向的是下一條記錄的「記錄頭信息」和「真實數據」之間的位置,這樣的好處是向左讀就是記錄頭信息,向右讀就是真實數據,比較方便。
- record_type:表示當前記錄的類型,0表示普通記錄,1表示B+樹非葉子節點記錄,2表示最小記錄,3表示最大記錄
記錄真實數據
記錄真實數據除了記錄我們自定義的列的數據外,Mysql還會為每個記錄默認添加一些列(隱藏列)
- row_id:當我們創建表的時候沒有指定主鍵,也沒有唯一約束的列,innodb 就會自動的為這些記錄添加row_id隱藏字段,占用6個字節。不是必須會有的。
- trx_id:事務ID,這個列是必須的,占用6個字節。表示數據是有哪個事務生產的。
- roll_pointer:回滾指針,表示當前記錄上一個版本的指針,這個列也是必需的,占用 7 個字節。
MVCC機制就是依賴 trx_id 和 roll_pointer 來實現的。
行溢出后,MySQL 是怎么處理的?
MySQL 中磁盤和內存交互的基本單位是頁,一個頁的大小一般是 16KB,也就是 16384字節,而一個 varchar(n) 類型的列最多可以存儲 65532字節,一些大對象如 TEXT、BLOB 可能存儲更多的數據,這時一個頁可能就存不了一條記錄。這個時候就會發生行溢出,多的數據就會存到另外的「溢出頁」中。
如果一個數據頁存不了一條記錄,InnoDB 存儲引擎會自動將溢出的數據存放到「溢出頁」中。在一般情況下,InnoDB 的數據都是存放在 「數據頁」中。但是當發生行溢出時,溢出的數據會存放到「溢出頁」中。
當發生行溢出時,在記錄的真實數據處只會保存該列的一部分數據,而把剩余的數據放在「溢出頁」中,然后真實數據處用 20 字節存儲指向溢出頁的地址,從而可以找到剩余數據所在的頁。大致如下圖所示。
總結
MySQL 的 NULL 值是怎么存放的?
MySQL 的 Compact 行格式中會用「NULL值列表」來標記值為 NULL 的列,NULL 值并不會存儲在行格式中的真實數據部分。
NULL值列表會占用 1 字節空間,當表中所有字段都定義成 NOT NULL,行格式中就不會有 NULL值列表,這樣可節省 1 字節的空間。
行溢出后,MySQL 是怎么處理的?
如果一個數據頁存不了一條記錄,InnoDB 存儲引擎會自動將溢出的數據存放到「溢出頁」中。
Compact 行格式針對行溢出的處理是這樣的:當發生行溢出時,在記錄的真實數據處只會保存該列的一部分數據,而把剩余的數據放在「溢出頁」中,然后真實數據處用 20 字節存儲指向溢出頁的地址,從而可以找到剩余數據所在的頁。