從一條數據說起——InnoDB行存儲數據結構


本篇博客參考掘金小冊——MySQL 是怎樣運行的:從根兒上理解 MySQL

先給大家講一個故事,我剛參加工作,在一個小作坊里面當【碼畜】(盡管現在也是),有一天老板從我背后走過,說了一句舉世震驚的話:我看你們的數據庫和excel一樣,不就是一行行數據,人家excel還可以對單元格進行美化,還有各種函數,生成各種報表,你們的數據庫有什么復雜的?我竟無力反駁。

為什么要說這個故事呢,當然是為了引出今天的話題——InnoDB行存儲數據結構。

雖然做開發的各位,或多或少都接觸過數據庫,但是數據庫中的一行行數據到底是怎么存儲的,存儲的格式又是什么,就不是每個開發都知道的了,數據庫對我們而言就是一個黑盒子,你想打開這個黑盒子一探究竟嗎?【不,我不想,我只想CURD】【不,這不是你的真實想法】。當我們收了快遞,盡管我們已經知道是什么快遞了,但是我們還是會迫不及待的拆開快遞,更何況,我們面對的是未知的事物,作為人的天性,一定是非常希望可以打開這個黑盒子,更別提充滿好奇心的程序猿了,今天我就帶着打開這神秘的黑盒子。

這次我們打開的黑盒子便是InnoDB存儲數據結構,換而言之,MySql其他的存儲引擎,如Memory,MyISAM不在本次的討論范圍。

InnoDB頁簡介

InnoDB是一個把數據存儲在硬盤的存儲引擎,即使服務器重啟,數據依然不會丟失,而真正的數據處理是發生在內存中的,所以InnoDB需要把硬盤上數據加載到內存中,然后在內存中進行各種數據處理,最終在某個時機把內存中的數據刷新到硬盤。而硬盤的處理速度是很慢很慢的,和內存差的太遠了,如果InnoDB每次只從硬盤中讀取一條數據,顯然是不行的,速度會慢死,所以InnoDB會把數據分成若干頁,以頁作為內存和硬盤之間交互的基本單位,說的再直白點:InnoDB讀取數據不是一行一行讀,而是以頁為最小單位讀取數據。默認情況下,一頁是16K,也就是InnoDB讀取數據的數據大小至少是16K。當然這個值是可以被修改的,因為一般情況下,也沒人會修改這個值,所以這里我就不說明應該怎么改了。

InnoDB行格式

之所以,文章開頭的老板會認為數據庫和excel是一樣的,就是因為我們平時基本都是用可視化工具去管理表,去查數據,一個不懂的人乍一看,確實和excel有點像,就是一行一行數據,這些數據在硬盤上存儲格式是需要我們去探究的。

InnoDB提供了4種行格式供我們選擇,分別是Compact、Redundant、Dynamic和Compressed行格式,以后可能會有新的行格式出現,但是區別並不是很大。

我們建表的時候,可以指定某種行格式:

CREATE TABLE table_name (列信息) ROW_FORMAT=行格式名稱

也可以修改已經存在的表的行格式:

ALTER TABLE  table_name ROW_FORMAT=行格式名稱

准備工作

為了后面的故事可以順利展開,我們先來建一張表:

CREATE TABLE  hero(
`x` VARCHAR(10),
`y` VARCHAR(10) NOT NULL,
`z` CHAR(10),
`t` VARCHAR(10)
)CHARSET=ASCII, ROW_FORMAT=COMPACT;

我建了一張表,指定的行格式是COMPACT,采用的字符集是ASCII,也就是我們的中文是無法存進去的,現在我要向這張表添加兩行數據:

INSERT INTO hero(x, y, z, t) VALUES('a', 'bb', 'cccc', 'ddddd'), ('a', 'b', NULL, NULL);

現在表中的數據是這樣的:
image.png

表建好了,數據填充好了,下面我們就來分析下在COMPACT行格式下,數據是如何存儲的吧。

COMPACT行格式

image.png

從上圖可以看到,一行數據被分為了兩個部分,一部分是記錄的額外信息,一部分是記錄的真實數據。

記錄額外信息

變長字段字節數列表

varchar(X)和char(X)的區別是什么,相信大家都非常清楚,char是定長的,varchar是變長的,變長字段中存儲多少字節的數據不是固定的,所以InnoDB在存儲數據的時候,會把這些數據占用的真實字節數也保存下來,也就是變長字段是占用了兩部分空間來存儲的:

  1. 真實的數據內容
  2. 占用的字節數

在COMPACT行格式中,把所有的變長字段所占用的字節數逆序排放在變長字段字節數列表中。

我們先前創建了一張表,還准備了兩條數據,現在我們來看下第一條數據中的變長字段字節數列表是什么醬紫的。

表中有四個字段,其中x,y,t三個字段都是變長字段,所以這三個字段的字節數需要保存在變長字段字節數列表,數據表采用的字符集是ascii,所以每一個字符占用的字節數是1,下面我們來看下第一條數據各個變長字段所占用的字節數:

字段名稱 內容 占用字節數 (十進制) 占用字節數 (十六進制)
x a 1 0x01
y bb 2 0x02
t ddddd 5 0x05

所以,第一行數據x,y,t三個字段所占用的字節數分別是1 2 5,但是InnoDB會把所占用的字節數逆序排放,如果用16進制來表示變長字段所占用的字節數就是這樣的效果了:
image.png

為了更容易理解、清晰,所以我用了空格來分割,其實是沒有的。

由於數據的長度都比較小,用一個字節就可以表示,但是如果變長字段占用的字節數比較多,就要用兩個字節來表示了,到底使用一個字節來表示,還是用兩個字節來表示,InnoDB有着自己的一套規則。在說這個規則之前,要先說明下規則中用到的三個變量:

  1. W:指定字符集下,一個字符最多需要占用的字節數。比如,ascii字符集的W是1,GBK字符集的W是2,utf-8字符集的W是3。
  2. M:最多可以存儲多少個字符,varchar(50)的M就是50。
  3. L:實際存儲字符占用了多少字節。

W*M:指定字段類型、字符集下,存儲的字符串最多占用的字節數。

下面就是規則了:

  1. 如果M*W<=255,那么用一個字節表示字符串所占用的字節數。
  2. 如果M*W>255,則分為兩種情況:
    2.1 如果L<=127,則用一個字節來表示字符串所占用的字節數。
    2.2 如果L>127,則用兩個字節來表示字符串所占用的字節數。

光看規則是不是覺得很繞,總結一下,該可變字段允許存儲的最大字節數(W*M)>255,且真實存儲的字節數(L)超過127,就用兩個字節來表示字符串所占用的字節數,否則用一個字節來表示字符串所占用的字節數。

我們再來看看第二條數據,字段t的值是NULL,變長字段字節數列表只存儲非NULL列內容占用的字節數,所以對於第二條數據,變長字段字節數列表只要存儲x和y所占用的字節數即可,填充在變長字段字節數列表的效果是醬紫的:
image.png

變長字段字節數列表不是必須的,如果一個表中所有的字段都不是變長的,那么就沒有變長字段字節數列表了。

我們建的表采用的字符集是ascii編碼的,一個字符所占用的字節固定是1,如果我們采用utf-8字符集,一個字段所占用的字節就不是固定的了,而是一個范圍:1-3,所以如果我們采用這樣的字符集,char(m)雖然是定長字段,但是也會被加入到變長字段字節數列表中。

NULL值列表

我待過一家公司,對表設計有非常明確的規定,其中有一條是任何字段都不允許為NULL,問原因,DBA只是淡淡的說了句,允許為NULL會額外占用一些空間。我也沒有繼續追究下去,就按照規定來唄。下面我就來揭秘為什么會有這個蛋疼的規定。

如果表中有字段允許為NULL,InnoDB就會開辟一塊空間來標識每個字段實際存儲的數據是不是為NULL,如果表中的字段都不允許為NULL,那么這塊空間就不復存在了。

那么InnoDB開辟出來的那塊空間具體是怎么回事呢,接下去往下看。

每個允許存儲為NULL的字段對應一個二進制位:

  • 如果字段實際存儲的數據不為NULL,二進制是0。
  • 如果字段實際存儲的數據是NULL,二進制是1。

這里和變長字段字節數列表是一樣的,是逆序排放的。

我們新建的hero表有三個字段都允許為NULL,所以存在NULL值列表。

我們先來看第一條數據,三個字段存儲的實際數據都不為NULL,所以用二進制來表示是醬紫的:
image.png

但是InnoDB是用整數字節的二進制位來表示NULL值列表的,現在不足8位,所以要在高位補0,最終用二進制來表示是醬紫的:
image.png

所以,對於第一條數據,NULL值列表用十六進制表示是0x00。

我們再來看看第二條數據,其中z和t兩個字段存儲的實際數據都是NULL,我們來看看用二進制如何來表示:
image.png

同樣的,需要高位補0:
image.png

所以,對於第二條數據,NULL值列表用十六進制表示是0x06。

我們把兩條數據的NULL值列表都填充完畢是醬紫的效果:
image.png

記錄頭信息

記錄頭信息中包含的內容很多,我先隨便列舉幾條:

  1. delete_mask :標識此條數據是否被刪除。
  2. next_record:下一條數據的位置。
  3. record_type:表示當前記錄的類型,0表示普通記錄,1表示B+樹非葉子節點記錄,2表示最小記錄,3表示最大記錄
    ...
    還有其他的,或者更具體的解釋等以后用到了再說吧。

記錄真實數據

對於hero表來說,記錄真實數據部分除了我們定義的四個字段,還有三個隱藏字段,分別為:row_id、trx_id、roll_pointer,我們來看下這三個字段是什么。

row_id

如果我們建表的時候指定了主鍵或者唯一約束列,那么就沒有row_id隱藏字段了。如果既沒有指定主鍵,又沒有唯一約束,那么InnoDB就會為記錄添加row_id隱藏字段。row_id不是必需的,占用6個字節。

trx_id

事務Id,表示這個數據是由哪個事務生成的。 trx_id是必需的,占用6個字節。

roll_pointer

這條數據上一個版本的指針。roll_pointer是必需的,占用7個字節。

關於 trx_id、roll_pointer的具體解釋,在我上一篇關於事務的博客有詳細描述過,感興趣的小伙伴可以找來看看。

VARCHAR(M)最多能存儲的數據

在講可變字段字節數列表的時候,講到InnoDB會有一套規則,計算是用一個字節來表示實際存儲的字節數,還是用兩個字節來表示實際存儲的字節數,但是如果存儲的字符串很長很長,用兩個字節都無法表示,該怎么辦呢?

我們先來看看用兩個字節最多可以表示的字節數是多少:
image.png
用兩個字節最多可以表示的字節數是65535。

我們用這個最大字節數來試下,能不能成功創建一張表:

CREATE TABLE test_max ( test VARCHAR ( 65535 ) ) charset = ascii,
row_format = Compact
Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs

看到了木有,兩個字節最多可以表示的字節數是65535,我們用這個數字創建表竟然失敗了,更別提65536了。

為什么失敗呢?

從報錯信息就可以知道一行數據的最大字節數是65535,其中包含了storage overhead。問題來了,這個storage overhead是什么呢?就是可變字段字節數列表、NULL值列表。

我們存儲VARCHAR(M)類型的字段,其實可能分成了三個部分來存儲:

  • 真實數據
  • 真實數據占用的字節數
  • NULL標識,如果不允許為NUL,這部分不需要

剛剛我們嘗試創建的表,字段是允許為NULL的,所以會占用一個字節來存儲NULL標識,真實的數據所占的字節數用兩個字節來表示,所以最多可以存儲65535-2-1=65532個字節。

CREATE TABLE test_max ( test VARCHAR ( 65532 ) ) charset = ascii,
row_format = Compact
> OK
> 時間: 0.229s

我們新建的表采用的字符集是ascii,如果采用的是GBK或者UTF-8,VARCHAR(M)最多能存儲的數據計算方式就不一樣了:

  • 在GBK字符集下,一個字符最多需要兩個字節,VARCHAR(M)的最大取值就是 65532/2=32766。
  • 在UTF-8字符集下,一個字符串最多需要三個字節,VARCHAR(M)的最大取值就是 65532/3=21844。

我們上面所說的只是針對於一個列的計算方式,如果有多個列的話,要保證多個列所允許占用的最大字節數+變長字段字節數列表所占用的字節數+NULL值列表所占用的字節數<=65535。

行溢出

文章開頭的時候,給大家簡單的介紹了下頁的概念,我們知道硬盤和內存之間交互的基本單位是頁,而頁的大小默認情況下16K,也就是16384字節,而VARCHAR(M)最多可以存儲的遠遠不止16384字節,這樣就出現了一個頁存放不了一條記錄的局面。

在Compact和Redundant行格式中,對於占用字節數非常大的列,在記錄的真實數據中只會存儲一小部分數據(768個字節),剩余的數據分散存儲在其他的頁,為了可以找到它們,在記錄的真實數據中會記錄這些頁的地址,就像下面醬紫:
image.png

Dynamic和Compressed行格式

Dynamic和Compressed行格式和COMPACT行格式很相近,只是在行溢出的處理方式上有所不同,溢出后,Dynamic和Compressed行格式不會在記錄的真實數據中存儲一小部分數據,而是直接記錄其他頁的地址。Dynamic和Compressed行格式的區別是Compressed格式會對頁進行壓縮以節省空間。

Redundant行格式是MySql5.0之前使用的,現在基本不會再使用,這里就不介紹了。

本章內容到這里就結束了,下次會介紹關於頁的詳細內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM