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