MySQL系列(3)— InnoDB行記錄格式

系列文章:MySQL系列專欄mysql

InnoDB 行記錄格式

行記錄格式

目前,InnoDB支持4中行記錄格式,分別是 Compact、Redundant、Dynamic和Compressed 行格式。web

四種行格式的特性對好比下:算法

image.png

InnoDB 表的默認行格式由參數 innodb_default_row_format 定義,默認值爲 DYNAMICsql

mysql> show variables like 'innodb_default_row_format';
+---------------------------+---------+
| Variable_name             | Value   |
+---------------------------+---------+
| innodb_default_row_format | dynamic |
+---------------------------+---------+
複製代碼

咱們能夠經過以下語法來指定表的行格式:服務器

CREATE TABLE <table_name(column_name)> ROW_FORMAT=行格式名稱
    
ALTER TABLE <table_name> ROW_FORMAT=行格式名稱
複製代碼

COMPACT 行記錄格式

Compact 設計目標是高效地存儲數據,一個頁中存放的行數據越多,其性能就越高。markdown

下圖顯示了 Compact 行記錄格式的存儲方式:性能

image.png

變長字段長度列表

MySQL中有一些變長字段類型,如 VARCHAR(M)、TEXT、BLOB 等,變長字段的長度是不固定的,因此在存儲數據的時候要把這些數據佔用的字節數也存起來,讀取數據的時候才能根據這個長度列表去讀取對應長度的數據。測試

變長字段長度列表 就是用來記錄一行中全部變長字段的真實數據所佔用的字節長度,而且各變長字段數據佔用的字節數是按照列的順序逆序存放url

變長字段長度列表中只存儲值爲非NULL的列內容佔用的長度,值爲 NULL 的列的長度是不儲存的。若是表中全部的列都不是變長的數據類型的話,就不須要變長字段長度列表了。spa

若變長字段的長度小於 255字節,就用1字節表示;若大於 255字節,用2字節表示,最大不會不超過2字節,由於MySQL中VARCHAR類型的最大字節長度限制爲65535

對於一些佔用字節數很是多的字段,比方說某個字段長度大於了16KB,那麼若是該記錄在單個頁面中沒法存儲時,InnoDB會把一部分數據存放到所謂的溢出頁中,在變長字段長度列表處只存儲留在本頁面中的長度,因此使用兩個字節也能夠存放下來。

NULL值列表

表中的某些列可能會存儲NULL值,若是把這些NULL值都放到記錄的真實數據中會比較浪費空間,因此Compact行格式把這些值爲NULL的列存儲到NULL值列表中。

若是表中全部列都不容許爲 NULL,就不存在NULL值列表了。若是存在容許NULL值的列,則每一個列對應一個二進制位,二進制位按照列的順序逆序排列。

  • 二進制位的值爲1時,表明該列的值爲NULL。
  • 二進制位的值爲0時,表明該列的值不爲NULL。

另外,NULL值列表必須用整數個字節的位表示(1字節8位),若是使用的二進制位個數不足整數個字節,則在字節的高位補0

記錄頭信息

記錄頭信息是由固定的5個字節組成,5個字節也就是40個二進制位,不一樣的位表明不一樣的意思,這些頭信息會在後面的一些功能中看到。

每一個位的含義以下表:

image.png

記錄真實數據

最後的部分就是實際存儲每一個列的數據。注意 NULL 不佔該部分任何空間,即 NULL 除了佔有NULL值列表的標誌位,實際存儲不佔有任何空間。

每行數據除了用戶定義的列外,在開頭還有兩個隱藏列,事務ID列(DB_TRX_ID)回滾指針列(DB_ROLL_PTR),分別爲6字節7字節的大小。若InnoDB表沒有定義主鍵,每行還會增長一個6字節行ID列(DB_ROW_ID)

隱藏主鍵列

若是咱們沒有爲某個表顯式的定義主鍵,而且表中也沒有定義惟一索引,那麼InnoDB會自動爲表添加一個row_id的隱藏列做爲主鍵。

爲這個row_id隱藏列賦值的方式以下:

  • 服務器會在內存中維護一個全局變量,每當向某個包含隱藏的row_id列的表中插入一條記錄時,就會把該變量的值看成新記錄的row_id列的值,而且把該變量自增1

  • 每當這個變量的值爲256的倍數時,就會將該變量的值刷新到系統表空間的頁號爲7的頁面中一個Max Row ID的屬性處。

  • 當系統啓動時,會將頁中的Max Row ID屬性加載到內存中,並將該值加上256以後賦值給全局變量,由於在上次關機時該全局變量的值可能大於頁中Max Row ID屬性值。

數據存儲演示

咱們建立下面一張表:其中 username 非空,nickname、address、email 均可爲空。

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(60) NOT NULL COMMENT '用戶名',
  `nickname` varchar(240) DEFAULT NULL COMMENT '暱稱',
  `address` varchar(240) DEFAULT NULL COMMENT '地址',
  `email` varchar(60) DEFAULT NULL COMMENT '郵箱',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_uk_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;
複製代碼

再插入兩條數據,查詢結果以下:

image.png

第一條數據的行存儲格式:

  • 變成字段列表中,按列逆序存儲變長字段的長度,address 列的值爲 NULL,不存儲長度。
  • NULL值列表中,按列逆序存儲可爲空的字段,address 列的值爲NULL,二進制標誌位爲1。username 列非NULL就沒有標誌位。一個字節沒用完,高位補0。
  • 數據段中先記錄了事務ID和回滾指針兩個隱藏列,而後存儲了值不爲NULL的列,NULL值不佔該部分任何空間。

image.png

接着看第二條數據的行存儲格式:

  • 變成字段列表中,按列逆序存儲變長字段的長度,email 列的值爲 NULL,不存儲長度。
  • NULL值列表中,按列逆序存儲可爲空的字段,email 列的值爲NULL,二進制標誌位爲1。
  • 數據段中先記錄了事務ID和回滾指針兩個隱藏列,而後存儲了值不爲NULL的列,NULL值不佔該部分任何空間。

image.png

Redundant行記錄格式

Redundant 是 MySQL5.0 版本以前 InnoDB 的行記錄存儲方式,已經比較老了,如今基本也再也不使用這種格式,下面簡單瞭解下就好了。

Redundant 的記錄格式大體以下圖所示:

image.png

字段長度偏移列表

Redundant 行記錄格式的首部是一個字段長度偏移列表,一樣是按照列的順序逆序放置的。該條記錄中全部列(包括隱藏列、NULL值列)的長度信息都按照逆序存儲到字段長度偏移列表。

多了個偏移兩個字,也就是列表存儲的是每一個字段的偏移量,那他就是採用兩個相鄰數值的差值來計算各個列值的長度。

Redundant 並無NULL值列表,它是將字段長度偏移列表中的各個列對應的偏移量的第一個比特位做爲是否爲NULL的依據,該比特位也能夠被稱之爲NULL比特位。也就是說在解析一條記錄的某個列時,首先看一下該列對應的偏移量的NULL比特位是否是爲1,若是爲1,那麼該列的值就是NULL,不然不是NULL。

記錄頭信息

Redundant 行格式的記錄頭信息佔用6字節,48個二進制位。

每一個位的含義以下表所示:

image.png

與 Compact 格式相比,多了 n_fields、1byte_offs_flag 兩個屬性,少了 record_type 屬性。n_fields 值表明一行中列的數量,佔用10位,這也說明了 Redundant 行格式一行最多支持1023列。1byte_offs_flags 值表示偏移列表佔用1字節仍是2字節。

VARCHAR 數據類型

字符集

在介紹後面的內容前,先了解下字符集,咱們在建表時每每都會設置表的字符集。計算機中只能存儲二進制數據,字符集就是字符與二進制數據的映射關係。

能夠經過 SHOW CHARSET; 命令查看 MySQL 支持的字符集。能夠看到這個MySQL版本一共支持41種字符集,其中的Default collation 列表示這種字符集默認的比較規則。最後一列 Maxlen 表明該種字符集表示一個字符最多須要幾個字節。

mysql> SHOW CHARSET;
+----------+---------------------------------+---------------------+--------+
| Charset  | Description                     | Default collation   | Maxlen |
+----------+---------------------------------+---------------------+--------+
| big5     | Big5 Traditional Chinese        | big5_chinese_ci     |      2 |
| dec8     | DEC West European               | dec8_swedish_ci     |      1 |
| cp850    | DOS West European               | cp850_general_ci    |      1 |
| hp8      | HP West European                | hp8_english_ci      |      1 |
| koi8r    | KOI8-R Relcom Russian           | koi8r_general_ci    |      1 |
| latin1   | cp1252 West European            | latin1_swedish_ci   |      1 |
| latin2   | ISO 8859-2 Central European     | latin2_general_ci   |      1 |
| swe7     | 7bit Swedish                    | swe7_swedish_ci     |      1 |
| ascii    | US ASCII                        | ascii_general_ci    |      1 |
| ujis     | EUC-JP Japanese                 | ujis_japanese_ci    |      3 |
| sjis     | Shift-JIS Japanese              | sjis_japanese_ci    |      2 |
| hebrew   | ISO 8859-8 Hebrew               | hebrew_general_ci   |      1 |
| tis620   | TIS620 Thai                     | tis620_thai_ci      |      1 |
| euckr    | EUC-KR Korean                   | euckr_korean_ci     |      2 |
| koi8u    | KOI8-U Ukrainian                | koi8u_general_ci    |      1 |
| gb2312   | GB2312 Simplified Chinese       | gb2312_chinese_ci   |      2 |
| greek    | ISO 8859-7 Greek                | greek_general_ci    |      1 |
| cp1250   | Windows Central European        | cp1250_general_ci   |      1 |
| gbk      | GBK Simplified Chinese          | gbk_chinese_ci      |      2 |
| latin5   | ISO 8859-9 Turkish              | latin5_turkish_ci   |      1 |
| armscii8 | ARMSCII-8 Armenian              | armscii8_general_ci |      1 |
| utf8     | UTF-8 Unicode                   | utf8_general_ci     |      3 |
| ucs2     | UCS-2 Unicode                   | ucs2_general_ci     |      2 |
| cp866    | DOS Russian                     | cp866_general_ci    |      1 |
| keybcs2  | DOS Kamenicky Czech-Slovak      | keybcs2_general_ci  |      1 |
| macce    | Mac Central European            | macce_general_ci    |      1 |
| macroman | Mac West European               | macroman_general_ci |      1 |
| cp852    | DOS Central European            | cp852_general_ci    |      1 |
| latin7   | ISO 8859-13 Baltic              | latin7_general_ci   |      1 |
| utf8mb4  | UTF-8 Unicode                   | utf8mb4_general_ci  |      4 |
| cp1251   | Windows Cyrillic                | cp1251_general_ci   |      1 |
| utf16    | UTF-16 Unicode                  | utf16_general_ci    |      4 |
| utf16le  | UTF-16LE Unicode                | utf16le_general_ci  |      4 |
| cp1256   | Windows Arabic                  | cp1256_general_ci   |      1 |
| cp1257   | Windows Baltic                  | cp1257_general_ci   |      1 |
| utf32    | UTF-32 Unicode                  | utf32_general_ci    |      4 |
| binary   | Binary pseudo charset           | binary              |      1 |
| geostd8  | GEOSTD8 Georgian                | geostd8_general_ci  |      1 |
| cp932    | SJIS for Windows Japanese       | cp932_japanese_ci   |      2 |
| eucjpms  | UJIS for Windows Japanese       | eucjpms_japanese_ci |      3 |
| gb18030  | China National Standard GB18030 | gb18030_chinese_ci  |      4 |
+----------+---------------------------------+---------------------+--------+
41 rows in set (0.14 sec)
複製代碼

幾個經常使用的字符集以下:例如 latin1 一個字符最大佔用 1字節,utf8mb4 一個字符最大佔用 4字節。

+----------+---------------------------------+---------------------+--------+
| Charset  | Description                     | Default collation   | Maxlen |
+----------+---------------------------------+---------------------+--------+
| latin1   | cp1252 West European            | latin1_swedish_ci   |      1 |
| ascii    | US ASCII                        | ascii_general_ci    |      1 |
| gb2312   | GB2312 Simplified Chinese       | gb2312_chinese_ci   |      2 |
| gbk      | GBK Simplified Chinese          | gbk_chinese_ci      |      2 |
| utf8     | UTF-8 Unicode                   | utf8_general_ci     |      3 |
| utf8mb4  | UTF-8 Unicode                   | utf8mb4_general_ci  |      4 |
+----------+---------------------------------+---------------------+--------+
複製代碼

單字節 VARCHAR 長度限制

咱們常常會用到變成類型 VARCHAR(M),其中的 M 表明該類型最多存儲的字符數量,咱們可能還知道 VARCHAR 最大可存放 65535 字節的長度,那其實是這樣嗎,下面咱們來驗證下。

咱們建立下面的一張表,指定 C1 列爲 VARCHAR(65535),注意字符集是 latin1,也就是1個字符佔用1字節

mysql> CREATE TABLE `test` (
  `ID` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `C1` VARCHAR(65535) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=COMPACT;
1118 - 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
複製代碼

從建立報錯的信息能夠了解到,一行數據除了 TEXT、BLOBs 這種大對象類型以外,其餘全部的列(不包括隱藏列和記錄頭信息)佔用的字節長度加起來不能超過65535個字節,不然須要將一些過長的列轉爲 TEXT 或 BLOBs 類型。

也就是說一行數據除了 TEXT、BLOBs 類型的列,限制最大爲 65535字節,注意是一行的總長度,不是一列。

咱們預測一下,C1 這個 VARCHAR 最大能設置多大?從 Compact 行格式能夠知道,主要有以下幾部分的數據:

  • 變長字段長度列表:C1 列超過 255字節,須要 2字節 表示長度
  • NULL值列表:C1 列可爲空,因此須要 1字節 標識 C1 列的值是否爲空
  • ID列:ID列爲 BIGINT 類型,佔 8字節

因此 C1 列最多還剩:65535 - 1 - 2 - 8 = 65524。

先設置爲 65525 長度試試:能夠看到仍是報一樣的錯誤。

mysql> CREATE TABLE `test` (
  `ID` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `C1` VARCHAR(65525) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=COMPACT;
1118 - 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
複製代碼

再設置爲 65524:建立成功,驗證了上面的預測。

mysql> CREATE TABLE `test` (
  `ID` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `C1` VARCHAR(65524) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=COMPACT;
Query OK, 0 rows affected (0.01 sec)
複製代碼

若是將 C1 列設置爲非空,那 NULL 值列表應該就不存在了,因此 VARCHAR 又能夠增長 1字節 就是 65525。

下面將 C1 設置爲非NULL,長度爲 65525:建立成功。

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

mysql> CREATE TABLE `test` (
  `ID` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `C1` VARCHAR(65525) NOT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=COMPACT;
Query OK, 0 rows affected (0.01 sec)
複製代碼

多字節 VARCHAR 長度限制

接着將字符集換成 utf8mb4,一個字符最多佔用 4字節。這個時候 C1 列 VARCHAR(M) 這個 M 設置多大呢?

要知道 M 指的是字符長度,而不是字節長度,而前面在 latin1 字符集且C1可爲空的狀況下算出的 65524 表示的既是字符長度又是字節長度。因此這時 C1 的長度實際應該是 M = 65524 / 4 = 16381。

下面驗證下,先將長度設置爲 16382,注意設置的字符集爲 utf8mb4,能夠看到建立報了一樣的行太大的錯誤。

mysql> CREATE TABLE `test` (
  `ID` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `C1` VARCHAR(16382) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;
1118 - 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
複製代碼

接着設置爲 16381,建立成功,驗證了咱們的計算結果。

mysql> CREATE TABLE `test` (
  `ID` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `C1` VARCHAR(16381) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;
Query OK, 0 rows affected (0.01 sec)
複製代碼

最後這裏總結一下VARCHAR類型:

  • 一行數據,除了 TEXT、BLOB 等大對象類型,總長度最大 65535字節。並且這個 65535 最大長度是包含 變長字段長度列表NULL值列表 的。
  • VARCHAR(M) 中的 M 指的是字符長度,而不是字節長度,計算時,要用總長度除以字符集最大長度,例如 utf8mb4 字符集每一個字符的最大長度爲 4字節。
  • VARCHAR 類型若是小於255字節,要在變長字段長度列表佔 1字節,不然佔 2字節;若是可爲NULL,還要在NULL值列表佔 1字節,不過這一個字節能夠存8個可爲NULL的列的狀態。因此一個 VARCHAR(M) 的字節長度最大爲 65532字節

CHAR 數據類型

咱們通常會認爲 CHAR(M) 是定長類型,M 與 VARCHAR(M) 中的 M 是同樣的,指的是字符的長度。類型爲CHAR(M)時,對於長度不足的值會用空格來補足,就算存的是空值,也會用空格補足,查詢的時候會去除首尾的空格,而VARCHAR就不會。

從下面的列表能夠看出,存儲 CHAR(4) 只須要4字節,VARCHAR(4)則至少須要1字節用於存儲長度。並且 CHAR(4) 會用空格補足長度,這樣應該就不須要記錄這個字段的長度了。

image.png

那 CHAR(M) 的長度會存到變長字段長度列表嗎?

在我參考的書籍中,有這樣的結論:

  • 若是是定長字符類型,例如 latin1,一個字符就是1字節,CHAR(M) 會用空格補足,不須要在變長字段長度列表記錄長度。
  • 若是是變長字符類型,例如 utf8mb4,一個字符佔用1~4 字節,CHAR(M) 就會佔用 M~4M 字節,會被當成變長字符類型,會將實際長度存儲到變長字段長列表中。

但我對這個結論有點迷,咱們看下面的測試。

下面新增了一個 C2 列,類型爲 VARCHAR(1),本來的 C1 長度減1。能夠看到會建立失敗,這個能夠明確知道緣由,由於C2列是變長類型,要在變長字段長度列表佔用1字節,因此總長度就就超過了 65535字節。

8(ID字節) + 16380 * 4(C1字節) + 1 * 4(C2字節) + 1(NULL列表) + 2(C1長度) + 1(C2長度) = 65536

CREATE TABLE `test` (
  `ID` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `C1` VARCHAR(16380) DEFAULT NULL,
  `C2` VARCHAR(1) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;
1118 - 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
複製代碼

若是將C2改成 CHAR(1),這時就建立成功了。從這裏能夠看出,是否是能夠說明 CHAR(M) 是定長類型,不會在變長字段長度列表佔用空間,或者就算佔用了也不會計算到總長度列表中?這裏先留個疑問。

mysql> CREATE TABLE `test` (
  `ID` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `C1` VARCHAR(16380) DEFAULT NULL,
  `C2` CHAR(1) DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;
Query OK, 0 rows affected (0.01 sec)
複製代碼

行溢出數據

MySQL中磁盤和內存交互的基本單位是,一個頁的大小通常是16KB,也就是16384字節,而一個VARCHAR(M)類型的列最多能夠存儲65532字節,一些大對象如 TEXT、BLOB 可能存儲更多的數據,這時一個頁可能就存不了一條記錄。這個時候就會發生行溢出,多的數據就會存到另外的溢出頁中。

InnoDB 規定一頁至少存儲兩條記錄,若是頁中只能存放下一條記錄,InnoDB存儲引擎會自動將行數據存放到溢出頁中。在通常狀況下,InnoDB 的數據都是存放在 FIL_PAGE_INDEX 類型的數據頁中的。可是當發生行溢出時,溢出的數據會存放到 FIL_PAGE_TYPE_BLOB 類型的溢出頁中。

當發生行溢出時,數據頁只保存了前768字節的前綴數據,接着是20個字節的偏移量,指向行溢出頁,大體以下圖所示。

image.png

COMPRESSED 和 DYNAMIC 行記錄格式

Compressed 和 Dynamic 行記錄格式與 Compact 行記錄格式是相似的,只不過在處理行溢出數據時有些區別。

這兩種格式採用徹底的行溢出方式,數據頁不會存儲真實數據的前768字節,只存儲20個字節的指針來指向溢出頁。而實際的數據都存儲在溢出頁中,看起來就像下面這樣:

image.png

Compressed 與 Dynamic 相比,Compressed 存儲的行數據會以zlib的算法進行壓縮以節省空間,所以對於 BLOB、TEXT、VARCHAR 這類大長度類型的數據可以進行很是有效的存儲。

MySQL5.7 默認的行記錄格式是 Dynamic

相關文章
相關標籤/搜索