本文來自作者投稿,原作者:zyz1992
關於MySql數據庫,相信很多人都不陌生,這是當今最常用的一種關系型數據庫,關於MySql的知識也是很豐富的。
那么,不知道大家有沒有想過這樣的問題:MySql中的數據是存在哪的?又是如何存儲的呢?
本文就來深入分析一下這些問題。文章內容很長,建議收藏,建議大家靜下心來仔細閱讀,一定會有收獲!

Innodb的存儲格式
我們知道,關於Mysql這種關系型數據庫,里面保存的數據最終都是要持久化到磁盤文件上面的。磁盤文件里存放的物理格式就是數據頁(關於數據頁,如果不太理解先忽略,后續文章單獨介紹),數據頁中存放的是一行一行的記錄,但是對於數據頁中的每一行數據他又是怎么存儲的呢?
我們拿Mysql中最常用的Innodb引擎來重點說,介紹下存儲格式是怎樣的。
MySQL中存儲有3種:
server層格式:與存儲引擎無關,Binlog存儲常用的一種 (Bin Log 我們前面已經詳細介紹過了,這個是MySql主從復制的一個很重要的文件)
索引元組格式:InnoDB存取過程記錄的中間狀態,是InnoDB在內存中存儲的格式 (換句話說我們的增刪改的操作都是在內存中執行的,這個只是一種臨時狀態)
物理存儲格式:記錄在物理頁面中的存儲格式,即compact格式,與索引元組格式一一對應。(這個是數據在磁盤存儲的真正的格式)
MySql 的 InnoDB 存儲引擎和大多數數據庫一樣,都是以行的形式存儲數據的,我們可以通過SHOW TABLE STATUS查看到行的的存儲格式。
InnoDB 儲存引擎支持有四種行儲存格式:COMPACT、Redundant、Dynamic 和 COMPRESSED。默認為COMPACT。

其他的參數我們這里不關注,僅僅看 Row_format 這列,這里我們可以看到行的存儲格式是 Compact,Compact 存儲數據的格式大致如下這樣

對於我們看到的每一行數據,我們最先看到的好像並不是各個列,而是一些類似列的描述信息。沒錯,其實在存儲的時候都會有一些都字段來描述這一行的信息,這就好比緩存池中的描述緩存頁的描述數據類似。
上面的圖片大家可以這么簡化來對待,事務ID和回滾指針大家先不要關注,免得因為這個產生干擾而難於理解

1、變長字段 varchar 是如何存儲的
一般情況下,我們要存儲的數據是並不能確定他的長度的,大部分情況下都是一些變長的數據,以varchar為例,假設現在三個字段,字段類型分別為:varchar(10),char(1),char(1),char大家都是知道的,存儲的基本是一些已知的長度固定的數據,假設這三個類型的字段分別有如下的數據:
第一行:mysql a a;第二行:dog b c;畫個圖來幫助大家想象,現在你看到的是數據中為我們展現的樣子。

但是在磁盤中可不是這樣子的,前文已經提到過,表空間和行這些其實是邏輯上的概念,而數據頁是一種物理概念,也就是說我們看到的樣子在磁盤中的樣子本本是不一樣的。
在磁盤中這兩條記錄大致是這樣子的:mysql a a dog b c,他們在磁盤中都是挨在一起存儲的。
是不是瞬間感覺想要去查找一條數據非常麻煩,告訴你:是的,所以 MySql在設計的時候才會使用行格式存儲,才會有前面的哪些變長字段列表和標志位以及記錄信息,這些就是用來記錄一行的記錄的信息,換句話說,MySql是通過這些描述信息來定位到一行中的具體記錄的。
以第一行記錄為例,它在磁盤中的記錄情況大致是下面這樣子的,首先我們需要明確知道的是各個字段的類型MySql是很清楚的,在這個基礎上我們能看明白下面和想通后面的事情。首先我們看到 mysql是5個字符,使用十六進制表示是 0x05,所以他的存儲大概是這樣子的:

同理第二行數據類似這樣子的:

相信大家在看到這里已經大概能推測出MySql這個時候是怎么讀讀取數據的了,就是他會先根據變長字段長度列表中描述的變長字段的信息去查找變長字段,例如第一行,MySql解析到變長字段是5,所以他會在mysql a a dog b c 這些里面取出5個字符,也就是 mysql,緊接着后面是兩個 char(1) 也就是兩個 a 在依次取出來。
中間設備。由淺入深,我們慢慢來,剛剛上面說到的僅僅是一種非常簡單的情況,這個首先是幫助大家理解,讓大家先明白有這么個回事,是這么回事,然后在慢慢的挖掘,我們一定要一個蘿卜一個坑的去踏實學習
現在如果是多個varchar類型的字段怎么辦?例如:varchar(3),varchar(10),varchar(4),char(1),他有一條記錄是這樣子的:aaa,bb,cccc,d,你根據上面的能推測出磁盤中的行記錄是怎么樣子的嗎?
你是不是這么想的:磁盤中肯定是這樣的:0x03,0x02,0x04 null標志位 記錄頭信息aaa bb cccc d;這么想的同學請鼻子靠牆:);實際上並不是這樣子的。
當有多個變長字段的時候,MySql在 compact 行格式中,把所有變長類型的長度存放在行記錄的開頭部位形成一個列表(這個列表就是剛剛上面說的變長字段列表),按照列的逆序存放,也就是大致是這樣子的:

這里我必須要給大家解釋下變長字段列表會逆序存放,因為每行記錄的都有一個next_record指針 指向下一行 記錄頭信息和 真實數據 之間的位置。因為這個位置剛剛好,向左讀取就是行描述相關信息,向右讀取就是真實數據。正好對應變長字段長度列表。畫個圖來幫助大家理解下:

說到這里我們來稍微小結一下
MySql中數據在磁盤的存儲小結
數據在磁盤中的存儲在物理空間上面是連續的
數據是被存放在MySql設計出來的數據頁上面的,數據頁上面存儲的才是最終的一行一行的記錄
行的存儲格式默認是Compact
每一行數據都會有相應的行描述部分,描述部分有【變長字段列表】【NULL標志位】【記錄頭信息】
每一行都會有next_record指針,指向記錄頭和變長字段列表的中間某個位置,方便尋址
變長列表中的varchar列的描述是逆序的(和字段的順序相反)這樣做的目的在上圖中描述的很清楚了

2、NULL字段是如何存儲的
上面說到了情況都是比較正常的情況,也就說上面提到的字段是沒有空值的,不管是變長字段還是char字段,都是有值的,那如果某個字段允許為空,且值確實為空,MySql又是怎么處理的呢?是不是直接存儲NULL呢。
假設MySql針對與Null直接存儲,他實際上是按照“NULL”這樣字符串的形式存儲的,這樣顯然不行啊,因為字符串要占用空間的啊(一個 NULL 字符串要占用四個字符呢),你都沒有值,還占這么多空間,所以MySql肯定不是這樣存儲的。其實MySql在處理NULL值的時候是會將它通二進制來存儲的,且也是逆序的
MySql是如何通過二進制來存儲NULL值的?
上面的 Compact 格式數據中的【NULL標志位(也可以叫NULL列表)】就是用來存儲NULL值的。若有某個字段值為 null,將將其 bit 位置為 1 說明值為 NULL,bit為 0 說明該字段值不為空
是不是聽了解釋還是稀里糊塗的,別急,我畫個圖再來詳細介紹下,先假設我們有一張 sutdents 表
CREATETABLE`students`(
`name`varchar(10)NOTNULL,
`address`varchar(255)DEFAULTNULL,
`gender`char(1)DEFAULTNULL,
`class`varchar(10)DEFAULTNULL,
`hobbies`varchar(255)DEFAULTNULL,
PRIMARYKEY(`name`)
)
他有這樣一行記錄

我們先看變長字段列表部分(記住是逆序存放的):
roles是長度為5記作:0x05;address 為null,不放在變長列表中、gender 是 char 類型,不放在變長列表中、class為空,不放在變長列表中、hobby_xx長度為8記作:0x08;所以變長列表的記錄為:0x08 0x05
現在到了NULL標志位了:依舊是從右往左記錄字段:name 在設計的時候就是 not null,所示是不會出現在NULL標志為中(Null標志為是用來記錄字段可為NULL的字段,字段不可以為NULL的不是會被記錄到NULL標志位的),address為NULL記作1,gender不為null記作0,class 為null記作1,hobbies不為null記作0;所以按照字段的順序結果就是:0101,但是NULL標志位是逆序的,所以NULL標志位存放的結果大概是這樣子的:0101,高位補0即可

我們來模擬讀取下這條記錄:MySql 對於字段的類型一定是已知的(這個是在創建數據表的時候就已經定下來了),所以對於 name 這種 not nul l的字段是不會去存放在null標志位的,下面是詳細的讀取步驟:
name字段是主鍵,不可能在NULL 標志位中的,又因為 name 是varchar 字段,所以就會去變長字段列中查找,找到值為 0x05 接着就會去字段列表中讀取5個字符的長度,也就是 roles ,第一個字段讀取成功;
接着是 address 字段,因為類型是 MySql 已知的,又因為字段值為 null 所以就不需要去讀取了,第二個字段讀取結束;
接着是gender字段,是char類型的,直接拿到 f 就可以了;
下一個是class 字段,因為是null 所以根本不會去變長字段中查找;
最后一個是 hobbies 字段,因為不為null ,又是第二個變長字段,這個時候就會去 變長字段列表中查找,結果定位到是 0x08 那就讀取 8 個字符的長度出來,拿出來是hobby_xx;
說到這里,關於一行記錄的中的變長字段列表和 NULL 標志位具體是如何讀取字段值的就給大家介紹完了,不知道大家看到以上內容腦子是不是會展現一條條行記錄的描述信息。目前我們只需要了解 varchar 和 NULL 存儲的基本就足夠了,因為這兩個表特殊,也是最經常使用的,其他的字段類型本篇暫且不展開討論了。
上面的記錄頭的信息我們還沒有討論過,下面我們再詳細介紹下記錄頭信息是什么。
記錄頭信息
記錄頭信息由40位的bit位組成,其各個位的划分和含義如下:

記錄頭的各個位的作用其實就已經說的很清楚了,一些概念現在還沒法講解,很多東西需要到索引的時候才能展開講,這里大家需要明確的就是各個標志位的含義。
我認為對於記錄頭的了解到這里就足夠了,各個標志位的含義明確了到這個程度就行了,至於更多的可能我們根本接觸不到。這一小節就當是科普。

3、數據在磁盤上到底是怎么存儲的
上面畫過這樣一張圖:

之前說的是數據大致是這樣子在磁盤中存儲的:0x03 NULL標志位 記錄頭信息 dog b c,但是實際上后面的列的數據並是不是我們看到的這個樣子,磁盤在存儲的時候是根據數據庫指定的字符集編碼存儲起來的你以為可能是上面那樣子存儲的。
實際上可能是在樣子的:0x03 NULL 標志位 記錄頭信息 1233 323 223,也就是說實際的數據在磁盤上存儲根本不是我們人能認識的,后面的 1233 323 223 這幾個是我亂寫的,沒什么含義,主要是想表明是計算在實際存儲的時候是以特定的字符編碼來存儲的。
另外每一行數據在被存儲的時候實際上還會有隱藏的字段,相信大家對這個應該不會陌生的,row_id 大家應該是知道的,哪怕自己沒用過可能也是聽過的,這個是數據庫自己為我們的每一行記錄生成的一個唯一的表示,如果我們沒有為數據表指定主鍵字段,也沒有指定 Unique key,那么這個時候數據庫內部會幫我們維護一個自增長的 ROW_ID 字段作為主鍵。
還有一個隱藏字段就是 事務ID 上面的第二張圖上層畫出來過,這個顧名思義了,就是和事務相關的一個字段屬性字段名為DB_TRX_ID,這個再詳解到事務的時候再詳細介紹;最后一個也是在上面的第二張圖上畫出來了,就是回滾指針 DB_ROLL_PTR,回滾也是事務使用到的概念,也是放在事務那邊跟大家介紹
現在再來整體回顧下一行記錄在磁盤中的存儲的結構大概是什么樣子的:
0x08 0x05 00000101 000001010000000000000000000000000000001021134 44 232343
說到了存儲,我們順便聊聊和存儲相關的一個概念,行溢出。
行溢出
說到這里,不知道大家有沒有想過一個問題,就是我們一直在說 MySql 存儲是以數據頁的形式來存儲的,然后數據頁中記錄的是一行行的記錄,但是往往常規情況下不會有什么問題。
但是如果現在有一行記錄非常大,因為數據頁大小默認也就是16KB,假設某張表里面有text字段也有BLOB字段,且這一行的記錄的大小遠遠超過了一個數據頁的大小16KB,這種情況稱之為行溢出。
MySql 是怎么來處理這種行溢出的情況的呢?實際上很簡單,一個數據頁不夠就使用多個數據頁,數據頁和數據頁之間使用鏈表連起來,之所以能夠使用鏈表連接因為數據頁里面是包含了存放指針的 bit 位。對於行溢出的概念了解到這個程度就足夠了。我們學習是有的放矢,不是什么都要去刨根問底的。

結束語
本片文章詳細的介紹了 MySql 存儲數據的格式和數據具體在磁盤中是怎么存儲的,被存儲的數據又是怎么查找的,說白了很多事情都是已經是既定的規則,所謂既定的規則就是很對東西已經被更早的設計出來。
所以你在使用和了解的使用只需要按照被人的規則來執行,然后在此基礎上深入了解下別人為什么這么設計?這樣會更有助於我們掌握和理解某個知識點。
文章來源:https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650152173&idx=1&sn=649e69f288d3d529d3af5282584b97dc&chksm=f36801ccc41f88dae42bf2914ae341aca27ee1284d06b50e8801e261bcb20e0c8cc380194edc&scene=21#wechat_redirect