Mysql-4之InnoDB記錄存儲結構

到現(xiàn)在為止,MySQL對于我們來說還是一個黑盒,我們只負責使用客戶端發(fā)送請求并等待服務器返回結果,表中的數(shù)據(jù)到底存到了哪里?以什么格式存放的?MySQL是以什么方式來訪問的這些數(shù)據(jù)?這些 問題我們統(tǒng)統(tǒng)不知道。

我們前邊嘮叨請求處理過程的時候提到過,MySQL服務器上負責對表中數(shù)據(jù)的讀取和寫入工作的部分是存儲引擎,而服務器又支持不同類型的存儲引擎,比如InnoDB、MyISAM、Memory啥的,不同的存儲引 擎一般是由不同的人為實現(xiàn)不同的特性而開發(fā)的,真實數(shù)據(jù)在不同存儲引擎中存放的格式一般是不同的,甚至有的存儲引擎比如Memory都不用磁盤來存儲數(shù)據(jù),也就是說關閉服務器后表中的數(shù)據(jù)就消失 了。由于InnoDB是MySQL默認的存儲引擎,也是我們最常用到的存儲引擎,我們也沒有那么多時間去把各個存儲引擎的內部實現(xiàn)都看一遍,所以本集要嘮叨的是使用InnoDB作為存儲引擎的數(shù)據(jù)存儲結構, 了解了一個存儲引擎的數(shù)據(jù)存儲結構之后,其他的存儲引擎都是依葫蘆畫瓢,等我們用到了再說哈~

InnoDB頁簡介

InnoDB是一個將表中的數(shù)據(jù)存儲到磁盤上的存儲引擎,所以即使關機后重啟我們的數(shù)據(jù)還是存在的。而真正處理數(shù)據(jù)的過程是發(fā)生在內存中的,所以需要把磁盤中的數(shù)據(jù)加載到內存中,如果是處理寫入 或修改請求的話,還需要把內存中的內容刷新到磁盤上。而我們知道讀寫磁盤的速度非常慢,和內存讀寫差了幾個數(shù)量級,所以當我們想從表中獲取某些記錄時,InnoDB存儲引擎需要一條一條的把記錄 從磁盤上讀出來么?不,那樣會慢死,InnoDB采取的方式是:將數(shù)據(jù)劃分為若干個頁,以頁作為磁盤和內存之間交互的基本單位,InnoDB中頁的大小一般為 16 KB。也就是在一般情況下,一次最少從磁 盤中讀取16KB的內容到內存中,一次最少把內存中的16KB內容刷新到磁盤中。

InnoDB行格式

我們平時是以記錄為單位來向表中插入數(shù)據(jù)的,這些記錄在磁盤上的存放方式也被稱為行格式或者記錄格式。設計InnoDB存儲引擎的大叔們到現(xiàn)在為止設計了4種不同類型的行格式,分別
是Compact、Redundant、Dynamic和Compressed行格式,隨著時間的推移,他們可能會設計出更多的行格式,但是不管怎么變,在原理上大體都是相同的。 指定行格式的語法
指定行格式的語法
我們可以在創(chuàng)建或修改表的語句中指定行格式:

CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名稱
ALTER TABLE 表名 ROW_FORMAT=行格式名稱 

比如我們在xiaohaizi數(shù)據(jù)庫里創(chuàng)建一個演示用的表record_format_demo,可以這樣指定它的行格式:

mysql> USE xiaohaizi;
Database changed

mysql> CREATE TABLE record_format_demo (
    ->     c1 VARCHAR(10),
    ->     c2 VARCHAR(10) NOT NULL,
    ->     c3 CHAR(10),
    ->     c4 VARCHAR(10)
    -> ) CHARSET=ascii ROW_FORMAT=COMPACT;
Query OK, 0 rows affected (0.03 sec)

可以看到我們剛剛創(chuàng)建的這個表的行格式就是Compact,另外,我們還顯式指定了這個表的字符集為ascii,因為ascii字符集只包括空格、標點符號、數(shù)字、大小寫字母和一些不可見字符,所以我們的漢 字是不能存到這個表里的。我們現(xiàn)在向這個表中插入兩條記錄:

mysql> INSERT INTO record_format_demo(c1, c2, c3, c4) VALUES('aaaa', 'bbb', 'cc', 'd'), ('eeee', 'fff', NULL, NULL);
Query OK, 2 rows affected (0.02 sec)
Records: 2  Duplicates: 0  Warnings: 0

現(xiàn)在表中的記錄就是這個樣子的:

mysql> SELECT * FROM record_format_demo;
+------+-----+------+------+
| c1   | c2  | c3   | c4   |
+------+-----+------+------+
| aaaa | bbb | cc   | d    |
| eeee | fff | NULL | NULL |
+------+-----+------+------+
2 rows in set (0.00 sec)

mysql>

演示表的內容也填充好了,現(xiàn)在我們就來看看各個行格式下的存儲方式到底有啥不同吧~

COMPACT行格式
image.png

大家從圖中可以看出來,一條完整的記錄其實可以被分為記錄的額外信息和記錄的真實數(shù)據(jù)兩大部分,下邊我們詳細看一下這兩部分的組成。

記錄的額外信息
這部分信息是服務器為了描述這條記錄而不得不額外添加的一些信息,這些額外信息分為3類,分別是變長字段長度列表、NULL值列表和記錄頭信息,我們分別看一下。

變長字段長度列表
我們知道MySQL支持一些變長的數(shù)據(jù)類型,比如VARCHAR(M)、VARBINARY(M)、各種TEXT類型,各種BLOB類型,我們也可以把擁有這些數(shù)據(jù)類型的列稱為變長字段,變長字段中存儲多少字節(jié)的數(shù)據(jù)是不固定 的,所以我們在存儲真實數(shù)據(jù)的時候需要順便把這些數(shù)據(jù)占用的字節(jié)數(shù)也存起來,這樣才不至于把MySQL服務器搞懵,所以這些變長字段占用的存儲空間分為兩部分:

  1. 真正的數(shù)據(jù)內容
  2. 占用的字節(jié)數(shù)

在Compact行格式中,把所有變長字段的真實數(shù)據(jù)占用的字節(jié)長度都存放在記錄的開頭部位,從而形成一個變長字段長度列表,各變長字段數(shù)據(jù)占用的字節(jié)數(shù)按照列的順序逆序存放,我們再次強調一遍, 是逆序存放!

我們拿record_format_demo表中的第一條記錄來舉個例子。因為record_format_demo表的c1、c2、c4列都是VARCHAR(10)類型的,也就是變長的數(shù)據(jù)類型,所以這三個列的值的長度都需要保存在記錄開頭 處,因為record_format_demo表中的各個列都使用的是ascii字符集,所以每個字符只需要1個字節(jié)來進行編碼,來看一下第一條記錄各變長字段內容的長度:

列名 存儲內容 內容長度(十進制表示) 內容長度(十六進制表示)
c1 'aaaa' 4 0x04
c2 'bbb' 3 0x03
c4 'd' 1 0x01

又因為這些長度值需要按照列的逆序存放,所以最后變長字段長度列表的字節(jié)串用十六進制表示的效果就是(各個字節(jié)之間實際上沒有空格,用空格隔開只是方便理解):
01 03 04

把這個字節(jié)串組成的變長字段長度列表填入上邊的示意圖中的效果就是:


image.png

由于第一行記錄中c1、c2、c4列中的字符串都比較短,也就是說內容占用的字節(jié)數(shù)比較小,用1個字節(jié)就可以表示,但是如果變長列的內容占用的字節(jié)數(shù)比較多,可能就需要用2個字節(jié)來表示。具體用1個 還是2個字節(jié)來表示真實數(shù)據(jù)占用的字節(jié)數(shù),InnoDB有它的一套規(guī)則,我們首先聲明一下W、M和L的意思:

  1. 假設某個字符集中表示一個字符最多需要使用的字節(jié)數(shù)為W,也就是使用SHOW CHARSET語句的結果中的Maxlen列,比方說utf8字符集中的W就是3,gbk字符集中的W就是2,ascii字符集中的W就是1。 2. 對于變長類型VARCHAR(M)來說,這種類型表示能存儲最多M個字符(注意是字符不是字節(jié)),所以這個類型能表示的字符串最多占用的字節(jié)數(shù)就是M×W。
  2. 假設它實際存儲的字符串占用的字節(jié)數(shù)是L。

所以確定使用1個字節(jié)還是2個字節(jié)表示真正字符串占用的字節(jié)數(shù)的規(guī)則就是這樣:

  • 如果M×W <= 255,那么使用1個字節(jié)來表示真正字符串占用的字節(jié)數(shù)。
    也就是說InnoDB在讀記錄的變長字段長度列表時先查看表結構,如果某個變長字段允許存儲的最大字節(jié)數(shù)不大于255時,可以認為只使用1個字節(jié)來表示真正字符串占用的字節(jié)數(shù)。

  • 如果M×W > 255,則分為兩種情況:

  1. 如果L <= 127,則用1個字節(jié)來表示真正字符串占用的字節(jié)數(shù)。
  2. 如果L > 127,則用2個字節(jié)來表示真正字符串占用的字節(jié)數(shù)。

總結一下就是說:如果該可變字段允許存儲的最大字節(jié)數(shù)(M×W)超過255字節(jié)并且真實存儲的字節(jié)數(shù)(L)超過127字節(jié),則使用2個字節(jié),否則使用1個字節(jié)。

另外需要注意的一點是,變長字段長度列表中只存儲值為 非NULL 的列內容占用的長度,值為 NULL 的列的長度是不儲存的 。也就是說對于第二條記錄來說,因為c4列的值為NULL,所以第二條記錄的變 長字段長度列表只需要存儲c1和c2列的長度即可。其中c1列存儲的值為'eeee',占用的字節(jié)數(shù)為4,c2列存儲的值為'fff',占用的字節(jié)數(shù)為3。數(shù)字4可以用1個字節(jié)表示,3也可以用1個字節(jié)表示,所以整 個變長字段長度列表共需2個字節(jié)。填充完變長字段長度列表的兩條記錄的對比圖如下:

image.png

小貼士: 并不是所有記錄都有這個 變長字段長度列表 部分,比方說表中所有的列都不是變長的數(shù)據(jù)類型的話,這一部分就不需要有。

NULL值列表
我們知道表中的某些列可能存儲NULL值,如果把這些NULL值都放到記錄的真實數(shù)據(jù)中存儲會很占地方,所以Compact行格式把這些值為NULL的列統(tǒng)一管理起來,存儲到NULL值列表中,它的處理過程是這樣 的:

  1. 首先統(tǒng)計表中允許存儲NULL的列有哪些。
    我們前邊說過,主鍵列、被NOT NULL修飾的列都是不可以存儲NULL值的,所以在統(tǒng)計的時候不會把這些列算進去。比方說表record_format_demo的3個列c1、c3、c4都是允許存儲NULL值的,而c2列是被NOT NULL修飾,不允許存儲NULL值。

  2. 如果表中沒有允許存儲 NULL 的列,則 NULL值列表 也不存在了,否則將每個允許存儲NULL的列對應一個二進制位,二進制位按照列的順序逆序排列,二進制位表示的意義如下:

  • 二進制位的值為1時,代表該列的值為NULL。
  • 二進制位的值為0時,代表該列的值不為NULL。
    因為表record_format_demo有3個值允許為NULL的列,所以這3個列和二進制位的對應關系就是這樣:


    image.png

    再一次強調,二進制位按照列的順序逆序排列,所以第一個列c1和最后一個二進制位對應。

  1. MySQL規(guī)定NULL值列表必須用整數(shù)個字節(jié)的位表示,如果使用的二進制位個數(shù)不是整數(shù)個字節(jié),則在字節(jié)的高位補0。

表record_format_demo只有3個值允許為NULL的列,對應3個二進制位,不足一個字節(jié),所以在字節(jié)的高位補0,效果就是這樣:


image.png

以此類推,如果一個表中有9個允許為NULL,那這個記錄的NULL值列表部分就需要2個字節(jié)來表示了。 知道了規(guī)則之后,我們再返回頭看表record_format_demo中的兩條記錄中的NULL值列表應該怎么儲存。因為只有c1、c3、c4這3個列允許存儲NULL值,所以所有記錄的NULL值列表只需要一個字節(jié)。

  • 對于第一條記錄來說,c1、c3、c4這3個列的值都不為NULL,所以它們對應的二進制位都是0,畫個圖就是這樣:


    image.png

所以第一條記錄的NULL值列表用十六進制表示就是:0x00。

  • 對于第二條記錄來說,c1、c3、c4這3個列中c3和c4的值都為NULL,所以這3個列對應的二進制位的情況就是:


    image.png

    所以第二條記錄的NULL值列表用十六進制表示就是:0x06。

所以這兩條記錄在填充了NULL值列表后的示意圖就是這樣:


image.png

記錄頭信息
除了變長字段長度列表、NULL值列表之外,還有一個用于描述記錄的記錄頭信息,它是由固定的5個字節(jié)組成。5個字節(jié)也就是40個二進制位,不同的位代表不同的意思,如圖:

image.png

這些二進制位代表的詳細信息如下表:


image.png

大家不要被這么多的屬性和陌生的概念給嚇著,我這里只是為了內容的完整性把這些位代表的意思都寫了出來,現(xiàn)在沒必要把它們的意思都記住,記住也沒啥用,現(xiàn)在只需要看一遍混個臉熟,等之后用 到這些屬性的時候我們再回過頭來看。

記錄的真實數(shù)據(jù)
對于record_format_demo表來說,記錄的真實數(shù)據(jù)除了c1、c2、c3、c4這幾個我們自己定義的列的數(shù)據(jù)以外,MySQL會為每個記錄默認的添加一些列(也稱為隱藏列),具體的列如下:

列名 是否必須 占用空間 描述
row_id 6字節(jié) 行ID,唯一標識一條記錄
transaction_id 6字節(jié) 事務ID
roll_pointer 7字節(jié) 回滾指針

小貼士: 實際上這幾個列的真正名稱其實是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR,我們?yōu)榱嗣烙^才寫成了row_id、transaction_id和roll_pointer。

這里需要提一下InnoDB表對主鍵的生成策略:優(yōu)先使用用戶自定義主鍵作為主鍵,如果用戶沒有定義主鍵,則選取一個Unique鍵作為主鍵,如果表中連Unique鍵都沒有定義的話,則InnoDB會為表默認添 加一個名為row_id的隱藏列作為主鍵。所以我們從上表中可以看出:InnoDB存儲引擎會為每條記錄都添加 transaction_id 和 roll_pointer 這兩個列,但是 row_id 是可選的(在沒有自定義主鍵以及Unique鍵 的情況下才會添加該列)。這些隱藏列的值不用我們操心,InnoDB存儲引擎會自己幫我們生成的。

因為表record_format_demo并沒有定義主鍵,所以MySQL服務器會為每條記錄增加上述的3個列?,F(xiàn)在看一下加上記錄的真實數(shù)據(jù)的兩個記錄長什么樣吧:


image.png

看這個圖的時候我們需要注意幾點:

  1. 表record_format_demo使用的是ascii字符集,所以0x61616161就表示字符串'aaaa',0x626262就表示字符串'bbb',以此類推。
  2. 注意第1條記錄中c3列的值,它是CHAR(10)類型的,它實際存儲的字符串是:'cc',而ascii字符集中的字節(jié)表示是'0x6363',雖然表示這個字符串只占用了2個字節(jié),但整個c3列仍然占用了10個字 節(jié)的空間,除真實數(shù)據(jù)以外的8個字節(jié)的統(tǒng)統(tǒng)都用空格字符填充,空格字符在ascii字符集的表示就是0x20。
  3. 注意第2條記錄中c3和c4列的值都為NULL,它們被存儲在了前邊的NULL值列表處,在記錄的真實數(shù)據(jù)處就不再冗余存儲,從而節(jié)省存儲空間。
CHAR(M)列的存儲格式

record_format_demo表的c1、c2、c4列的類型是VARCHAR(10),而c3列的類型是CHAR(10),我們說在Compact行格式下只會把變長類型的列的長度逆序存到變長字段長度列表中,就像這樣:


image.png

但是這只是因為我們的record_format_demo表采用的是ascii字符集,這個字符集是一個定長字符集,也就是說表示一個字符采用固定的一個字節(jié),如果采用變長的字符集(也就是表示一個字符需要的字 節(jié)數(shù)不確定,比如gbk表示一個字符要12個字節(jié)、utf8表示一個字符要13個字節(jié)等)的話,c3列的長度也會被存儲到變長字段長度列表中,比如我們修改一下record_format_demo表的字符集:

mysql> ALTER TABLE record_format_demo MODIFY COLUMN c3 CHAR(10) CHARACTER SET utf8;
Query OK, 2 rows affected (0.02 sec)
Records: 2  Duplicates: 0  Warnings: 0

修改該列字符集后記錄的變長字段長度列表也發(fā)生了變化,如圖:


image.png

這就意味著:對于 CHAR(M) 類型的列來說,當列采用的是定長字符集時,該列占用的字節(jié)數(shù)不會被加到變長字段長度列表,而如果采用變長字符集時,該列占用的字節(jié)數(shù)也會被加到變長字段長度列表

另外有一點還需要注意,變長字符集的CHAR(M)類型的列要求至少占用M個字節(jié),而VARCHAR(M)卻沒有這個要求。比方說對于使用utf8字符集的CHAR(10)的列來說,該列存儲的數(shù)據(jù)字節(jié)長度的范圍是10~ 30個字節(jié)。即使我們向該列中存儲一個空字符串也會占用10個字節(jié),這是怕將來更新該列的值的字節(jié)長度大于原有值的字節(jié)長度而小于10個字節(jié)時,可以在該記錄處直接更新,而不是在存儲空間中重新 分配一個新的記錄空間,導致原有的記錄空間成為所謂的碎片。(這里你感受到設計Compact行格式的大叔既想節(jié)省存儲空間,又不想更新CHAR(M)類型的列產(chǎn)生碎片時的糾結心情了吧。)

行溢出數(shù)據(jù)

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

我們知道對于VARCHAR(M)類型的列最多可以占用65535個字節(jié)(長度用兩個字節(jié)表示)。其中的M代表該類型最多存儲的字符數(shù)量,如果我們使用ascii字符集的話,一個字符就代表一個字節(jié),我們看看VARCHAR(65535)是否可用:

mysql> CREATE TABLE varchar_size_demo(
    ->     c VARCHAR(65535)
    -> ) CHARSET=ascii ROW_FORMAT=Compact;
ERROR 1118 (42000): 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
mysql>

從報錯信息里可以看出,MySQL對一條記錄占用的最大存儲空間是有限制的,除了BLOB或者TEXT類型的列之外,其他所有的列(不包括隱藏列和記錄頭信息)占用的字節(jié)長度加起來不能超過65535個字節(jié)。所以MySQL服務器建議我們把存儲類型改為TEXT或者BLOB的類型。這個65535個字節(jié)除了列本身的數(shù)據(jù)之外,還包括一些其他的數(shù)據(jù)(storage overhead),比如說我們?yōu)榱舜鎯σ粋€VARCHAR(M)類型的列,其實需要占用3部分存儲空間:

  • 真實數(shù)據(jù)
  • 真實數(shù)據(jù)占用字節(jié)的長度
  • NULL值標識,如果該列有NOT NULL屬性則可以沒有這部分存儲空間

如果該VARCHAR類型的列沒有NOT NULL屬性,那最多只能存儲65532個字節(jié)的數(shù)據(jù),因為真實數(shù)據(jù)的長度可能占用2個字節(jié),NULL值標識需要占用1個字節(jié):

mysql> CREATE TABLE varchar_size_demo(
    ->      c VARCHAR(65532)
    -> ) CHARSET=ascii ROW_FORMAT=Compact;
Query OK, 0 rows affected (0.02 sec)

如果VARCHAR類型的列有NOT NULL屬性,那最多只能存儲65533個字節(jié)的數(shù)據(jù),因為真實數(shù)據(jù)的長度可能占用2個字節(jié),不需要NULL值標識:

mysql> DROP TABLE varchar_size_demo;
Query OK, 0 rows affected (0.01 sec)

mysql> CREATE TABLE varchar_size_demo(
    ->      c VARCHAR(65533) NOT NULL
    -> ) CHARSET=ascii ROW_FORMAT=Compact;
Query OK, 0 rows affected (0.02 sec)

如果VARCHAR(M)類型的列使用的不是ascii字符集,那會怎么樣呢?來看一下:

mysql> DROP TABLE varchar_size_demo;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE TABLE varchar_size_demo(
    ->       c VARCHAR(65532)
    -> ) CHARSET=gbk ROW_FORMAT=Compact;
ERROR 1074 (42000): Column length too big for column 'c' (max = 32767); use BLOB or TEXT instead

mysql> CREATE TABLE varchar_size_demo(
    ->       c VARCHAR(65532)
    -> ) CHARSET=utf8 ROW_FORMAT=Compact;
ERROR 1074 (42000): Column length too big for column 'c' (max = 21845); use BLOB or TEXT instead

從執(zhí)行結果中可以看出,如果VARCHAR(M)類型的列使用的不是ascii字符集,那M的最大取值取決于該字符集表示一個字符最多需要的字節(jié)數(shù)。在列的值允許為NULL的情況下,gbk字符集表示一個字符最多需要2個字節(jié),那在該字符集下,M的最大取值就是32766(也就是:65532/2),也就是說最多能存儲32766個字符;utf8字符集表示一個字符最多需要3個字節(jié),那在該字符集下,M的最大取值就是21844,就是說最多能存儲21844(也就是:65532/3)個字符。

上述所言在列的值允許為NULL的情況下,gbk字符集下M的最大取值就是32766,utf8字符集下M的最大取值就是21844,這都是在表中只有一個字段的情況下說的,一定要記住一個行中的所有列(不包括隱藏列和記錄頭信息)占用的字節(jié)長度加起來不能超過65535個字節(jié)!

記錄中的數(shù)據(jù)太多產(chǎn)生的溢出

我們以ascii字符集下的varchar_size_demo表為例,插入一條記錄:

mysql> CREATE TABLE varchar_size_demo(
    ->       c VARCHAR(65532)
    -> ) CHARSET=ascii ROW_FORMAT=Compact;
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO varchar_size_demo(c) VALUES(REPEAT('a', 65532));
Query OK, 1 row affected (0.00 sec)

其中的REPEAT('a', 65532)是一個函數(shù)調用,它表示生成一個把字符'a'重復65532次的字符串。前邊說過,MySQL中磁盤和內存交互的基本單位是,也就是說MySQL是以為基本單位來管理存儲空間的,我們的記錄都會被分配到某個中存儲。而一個頁的大小一般是16KB,也就是16384字節(jié),而一個VARCHAR(M)類型的列就最多可以存儲65532個字節(jié),這樣就可能造成一個頁存放不了一條記錄的尷尬情況。

CompactReduntant行格式中,對于占用存儲空間非常大的列,在記錄的真實數(shù)據(jù)處只會存儲該列的一部分數(shù)據(jù),把剩余的數(shù)據(jù)分散存儲在幾個其他的頁中,然后記錄的真實數(shù)據(jù)處用20個字節(jié)存儲指向這些頁的地址(當然這20個字節(jié)中還包括這些分散在其他頁面中的數(shù)據(jù)的占用的字節(jié)數(shù)),從而可以找到剩余數(shù)據(jù)所在的頁,如圖所示:

image.png

從圖中可以看出來,對于Compact和Reduntant行格式來說,如果某一列中的數(shù)據(jù)非常多的話,在本記錄的真實數(shù)據(jù)處只會存儲該列的前768個字節(jié)的數(shù)據(jù)和一個指向其他頁的地址,然后把剩下的數(shù)據(jù)存放 到其他頁中,這個過程也叫做行溢出,存儲超出768字節(jié)的那些頁面也被稱為溢出頁。畫一個簡圖就是這樣:


image.png

最后需要注意的是,不只是 VARCHAR(M) 類型的列,其他的 TEXT、BLOB 類型的列在存儲數(shù)據(jù)非常多的時候也會發(fā)生行溢出。

行溢出的臨界點

那發(fā)生行溢出的臨界點是什么呢?也就是說在列存儲多少字節(jié)的數(shù)據(jù)時就會發(fā)生行溢出?
MySQL中規(guī)定一個頁中至少存放兩行記錄,至于為什么這么規(guī)定我們之后再說,現(xiàn)在看一下這個規(guī)定造成的影響。以上邊的varchar_size_demo表為例,它只有一個列c,我們往這個表中插入兩條記錄,每 條記錄最少插入多少字節(jié)的數(shù)據(jù)才會行溢出的現(xiàn)象呢?這得分析一下頁中的空間都是如何利用的。

  • 每個頁除了存放我們的記錄以外,也需要存儲一些額外的信息,亂七八糟的額外信息加起來需要136個字節(jié)的空間(現(xiàn)在只要知道這個數(shù)字就好了),其他的空間都可以被用來存儲記錄。
  • 每個記錄需要的額外信息是27字節(jié)。

這27個字節(jié)包括下邊這些部分:
- 2個字節(jié)用于存儲真實數(shù)據(jù)的長度(總長度不大于65535原因
- 1個字節(jié)用于存儲列是否是NULL值
- 5個字節(jié)大小的頭信息
- 6個字節(jié)的row_id
- 6個字節(jié)的transaction_id
- 7個字節(jié)的roll_pointer

假設一個列中存儲的數(shù)據(jù)字節(jié)數(shù)為n,那么發(fā)生行溢出現(xiàn)象時需要滿足這個式子:

136 + 2×(27 + n) > 16384

求解這個式子得出的解是:n > 8098。也就是說如果一個列中存儲的數(shù)據(jù)不大于8098個字節(jié),那就不會發(fā)生行溢出,否則就會發(fā)生行溢出。不過這個8098個字節(jié)的結論只是針對只有一個列 的varchar_size_demo表來說的,如果表中有多個列,那上邊的式子和結論都需要改一改了,所以重點就是:你不用關注這個臨界點是什么,只要知道如果我們想一個行中存儲了很大的數(shù)據(jù)時,可能發(fā) 生行溢出的現(xiàn)象。

Dynamic和 Compressed行格式

下邊要介紹另外兩個行格式,Dynamic和Compressed行格式,我現(xiàn)在使用的MySQL版本是5.7,它的默認行格式就是Dynamic,這倆行格式和Compact行格式挺像,只不過在處理行溢出數(shù)據(jù)時有點兒分歧,它們不會在記錄的真實數(shù)據(jù)處存儲字段真實數(shù)據(jù)的前768個字節(jié),而是把所有的字節(jié)都存儲到其他頁面中,只在記錄的真實數(shù)據(jù)處存儲其他頁面的地址,就像這樣:


image.png

Compressed行格式和Dynamic不同的一點是,Compressed行格式會采用壓縮算法對頁面進行壓縮,以節(jié)省空間。

小結

  1. 頁是MySQL中磁盤和內存交互的基本單位,也是MySQL是管理存儲空間的基本單位。
  2. 指定和修改行格式的語法如下:
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名稱
ALTER TABLE 表名 ROW_FORMAT=行格式名稱
  1. InnoDB目前定義了4種行格式
  • COMPACT行格式


    image.png
  • Dynamic和Compressed行格式
    這兩種行格式類似于COMPACT行格式,只不過在處理行溢出數(shù)據(jù)時有點兒分歧,它們不會在記錄的真實數(shù)據(jù)處存儲字符串的前768個字節(jié),而是把所有的字節(jié)都存儲到其他頁面中,只在記錄的真 實數(shù)據(jù)處存儲其他頁面的地址。

  • 一個頁一般是16KB,當記錄中的數(shù)據(jù)太多,當前頁放不下的時候,會把多余的數(shù)據(jù)存儲到其他頁中,這種現(xiàn)象稱為行溢出。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容