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

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

先給你們講一個故事,我剛參加工做,在一個小做坊裏面當【碼畜】(儘管如今也是),有一天老闆從我背後走過,說了一句舉世震驚的話:我看大家的數據庫和excel同樣,不就是一行行數據,人家excel還能夠對單元格進行美化,還有各類函數,生成各類報表,大家的數據庫有什麼複雜的?我竟無力反駁。bash

爲何要說這個故事呢,固然是爲了引出今天的話題——InnoDB行存儲數據結構。服務器

雖然作開發的各位,或多或少都接觸過數據庫,可是數據庫中的一行行數據究竟是怎麼存儲的,存儲的格式又是什麼,就不是每一個開發都知道的了,數據庫對咱們而言就是一個黑盒子,你想打開這個黑盒子一探究竟嗎?【不,我不想,我只想CURD】【不,這不是你的真實想法】。當咱們收了快遞,儘管咱們已經知道是什麼快遞了,可是咱們仍是會火燒眉毛的拆開快遞,更況且,咱們面對的是未知的事物,做爲人的天性,必定是很是但願能夠打開這個黑盒子,更別提充滿好奇心的程序猿了,今天我就帶着打開這神祕的黑盒子。數據結構

此次咱們打開的黑盒子即是InnoDB存儲數據結構,換而言之,MySql其餘的存儲引擎,如Memory,MyISAM不在本次的討論範圍。函數

InnoDB頁簡介

InnoDB是一個把數據存儲在硬盤的存儲引擎,即便服務器重啓,數據依然不會丟失,而真正的數據處理是發生在內存中的,因此InnoDB須要把硬盤上數據加載到內存中,而後在內存中進行各類數據處理,最終在某個時機把內存中的數據刷新到硬盤。而硬盤的處理速度是很慢很慢的,和內存差的太遠了,若是InnoDB每次只從硬盤中讀取一條數據,顯然是不行的,速度會慢死,因此InnoDB會把數據分紅若干頁,以頁做爲內存和硬盤之間交互的基本單位,說的再直白點:InnoDB讀取數據不是一行一行讀,而是以頁爲最小單位讀取數據。默認狀況下,一頁是16K,也就是InnoDB讀取數據的數據大小至少是16K。固然這個值是能夠被修改的,由於通常狀況下,也沒人會修改這個值,因此這裏我就不說明應該怎麼改了。工具

InnoDB行格式

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

InnoDB 提供了4種行格式供咱們選擇,分別是Compact、Redundant、Dynamic和Compressed行格式,之後可能會有新的行格式出現,可是區別並非很大。編碼

咱們建表的時候,能夠指定某種行格式:spa

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以前使用的,如今基本不會再使用,這裏就不介紹了。

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

相關文章
相關標籤/搜索