如果大家對我的 【大白話系列】MySQL 學習總結系列 感興趣的話,可以點擊關注一波。
一、回顧
MySQL 學習總結系列至此已經第七節了。
從大方向:我們已經學習了 MySQL 的架構設計、InnoDB 的架構設計。
從較爲深入的:我們已經學習了 rodo log 和 binlog 配合的兩階段提交協議,瞭解 緩衝池的設計原理和支持高併發、動態調整的管理機制。
下面,我們將介紹數據行格式:數據是以什麼格式存儲在數據頁中的。
二、行存儲格式
InnoDB 儲存引擎支持有四種行儲存格式:COMPACT、Redundant、Dynamic 和 COMPRESSED。
下面我們將重點介紹 COMPACT 行格式:
COMPACT 行存儲格式大概類似這樣:
變長字段的長度列表,null值列表,數據頭,column01的值,column02的值,column0n的值......
ps:爲了讓磁盤空間得到最大的利用率,每個數據行都是緊緊地挨在一起的。
下面我們將詳細介紹 COMPACT 行格式的各個知識點,當你學習完之後,你就曉得即使每行數據緊緊地挨在一起,MySQL 也能精準地將每行數據找出來~
三、變長字段如何存儲?
1、變長字段的存儲問題
我們都知道,varchar 類型是變長的,例如 varchar(50),那麼這個字段值的長度範圍:0 ~ 50 個字符。但是,不是每個字段值都剛好50個字符,肯定會有的長有的短。
那麼,數據存儲時,會按照字段定義時的最大長度來存儲值嗎?
必須不會的,如果都按照最大長度存儲,當出現值不滿 50個字符長度時,會浪費磁盤空間和內存空間。
爲什麼也浪費內存空間,數據不是存放在磁盤麼?大家不會忘了緩衝池的作用了吧?哈哈,要記得緩衝池和磁盤數據交換的單位就是數據頁而數據行是存放在數據頁中的
2、變長字段長度列表
InnoDB 中,利用 變長字段長度列表 來解決上面的問題:
- 變長字段長度列表記錄每一個變長字段值的長度,存儲的長度是十六進制的。
- 如果有多個變長字段,那麼變長字段長度列表是按逆序存儲的。
下面用一個例子來描述一下變長字段長度列表的使用原理:
-- 表結構
create table test(
c1 varchar(10) comment '字段1-變長',
c2 varchar(5) comment '字段2-變長',
c3 varchar(20) comment '字段3-變長',
c4 char(1) comment '字段4-定長',
c5 char(1) comment '字段5-定長'
) ENGINE=InnoDB;
-- 一行數據
insert into test values('hello','ni','hao','a','a');
我們來算一下他們的長度(十六進制):
- hello 的長度爲5,十六進制爲 0x05
- ni 的長度爲2,十六進制爲 0x02
- hao 的長度爲3,十六進制爲 0x03
那麼,實際的存儲格式是這樣的:
0x03 0x02 0x05 null值列表 數據頭 hello hi hao a a
四、NULL 值字段如何存儲?
1、可爲 NULL 字段的存儲問題
定義爲 default NULL 的字段,值可空可不空。那如果字段值爲 NULL,數據行裏是怎樣存儲的呢?是直接存儲“NULL”字段嗎?
我們分析一下:
- 如果是,那將會浪費磁盤空間,本來值就是 NULL 的,你現在給我搞了個四個字符大小的字符串。
- 如果不是,那怎麼識別這個字段是否是 NULL 呢?
2、NULL值列表
InnoDB 中,利用 NULL值列表 來解決上面的問題:
- NULL 值列表記錄可爲 NULL 的字段的情況。
- 用二進制bit位來標識字段值是否爲 NULL。1爲 NULL,0 不爲 NULL。
- 如果有多個可爲 NULL 的字段,那麼 NULL 值列表也是按照逆序存儲的。
- 而且 NULL 值列表的位數必須是 8bit 的N倍。例如:列表僅僅只有4個bit,則往高位補0,補到 8個bit。
下面用一個例子來描述一下 NULL 值列表 的使用原理:
-- 表結構
create table test(
c1 varchar(10) not null comment '字段1-變長',
c2 varchar(5) comment '字段2-變長',
c3 char(1) comment '字段3-變長',
c4 varchar(30) comment '字段4-定長',
c5 varchar(50) comment '字段5-定長'
) ENGINE=InnoDB;
-- 一行數據
insert into test values('howinfun',null,'m',null,'foshan');
算一下變長字段的長度:
- howinfun 的長度爲8,十六進制爲 0x08
- foshan 的長度爲6,十六進制爲 0x06
統計一下值爲 NULL 的字段:
- c2 字段爲 NULL
- c4 字段爲 NULL
那麼,實際的存儲格式是這樣的:
0x06 0x08 00000101 數據頭 howinfun m foshan
3、採用 NULL值列表 和 直接存儲“NULL”字符串相比,有多大的存儲差距?
到此,我們就可以算一下這兩種方案的存儲差距有多大了。
- 一個字節 8個bit,NULL 值列表用二進制 bit 位來標識字段值是否爲 NULL;那麼就是說,標識8個字段才佔用一個字節。
- 而如果用字符串的方式來存儲,而一個"NULL"字符串足足用了四個字節(英文一個字符等於一個字節,中文一個字符等於兩個字節),那麼同樣的8個字段就需要36個字節了。
這差距是非常明顯的!
五、數據頭
COMPACT 行格式中,除了 變長字段長度列表 和 NULL 值列表,就到數據頭了。
數據頭的大小爲 40 個bit位。
下面介紹 40個 bit 分別都有什麼信息。
名稱 | 大小 (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 | 表示下一條記錄的相對位置 |
那麼,我們看一下,加上數據頭的實際存儲:
0x06 0x08 00000101 0000000000000000000010000000000000011001 howinfun m foshan
六、一行數據在磁盤是如何存儲的
1、字符集編碼
上面,我們已經介紹了 COMPACT 行格式了,那麼一行數據真正是如何存儲的?
我們都知道,在建庫和建表時,都可以指定字符集編碼。所以,數據都會經過數據庫指定的字符集編碼後,再進行存儲的。
下面用一個例子來描述一下使用原理:
-- 表結構
create table test(
c1 varchar(10) not null comment '字段1',
c2 varchar(5) comment '字段2',
c3 char(1) comment '字段3',
c4 varchar(30) comment '字段4',
c5 varchar(50) comment '字段5'
)
-- 一行數據
insert into test values('howinfun',null,'m',null,'foshan');
假設編碼後:
- howinfun 編碼後:61616161
- m 編碼後:62
- foshan編碼後:636363
那麼,實際的存儲格式是這樣的:
0x06 0x08 00000101 0000000000000000000010000000000000011001 61616161 62 636363
2、隱藏字段
除了變長字段長度列表、NULL值列表、40個bit位的數據頭和真實數據,其實還包含了一些隱藏字段:
- DB_ROW_ID 字段:如果我們沒有指定主鍵和unique key唯一索引的時候,他就內部自動加一個ROW_ID作爲主鍵。
- DB_TRX_ID 字段:事務 ID,標識這是哪個事務更新的數據
- DB_ROLL_PTR 字段:回滾指針,用來進行事務回滾的
加上隱藏字段後,上面的例子的實際存儲可能就是:
0x06 0x08 00000101 0000000000000000000010000000000000011001 00000000094C(DB_ROW_ID)00000000032D(DB_TRX_ID) EA000010078E(DB_ROL_PTR) 616161 636320 6262626262
ps:括號裏只是做說明用的,事實是不存在的。
3、行溢出問題
數據頁的默認大小是 16kb,但是某些字段的值可以遠遠大於 16kb。
例如變長字段類型 varchar(N):N 最大可爲 65532(65kb),這就遠遠大於 16kb。
當然了,還有 text 和 blog 字段,這些都是大字段,都可以超過 16kb。
如果一行數據的大小超過了 16kb,就會出現行溢出的現象。
怎麼解決?
當一行數據超了 16kb,會在超了大小的那個字段中,可能僅僅包含他的一部分數據,然後同時包含一個20個字節的指針,指向存儲了這行數據超了的部分的其他數據頁。