說說 VARCHAR 背後的那些事

在使用MySQL的過程當中,在存儲字符串時,你們或許都有過這樣或那樣的困惑,譬如:html

1.  對於固定長度的字符串,爲何推薦使用 CHAR 來存儲?mysql

2.  VARCHAR 可設置的最大長度是多少?sql

3.  給定一個字符串,怎麼知道它的空間使用狀況?編輯器

4.  建立索引時,提示「Index column size too large. The maximum column size is 767 bytes」,該如何解決?測試

5.  VARCHAR 爲什麼要按需設置?VARCHAR(50) 和 VARCHAR(500) 有什麼區別?優化

下面就這些問題作一個系統的分析。ui

 

1. CHAR與VARCHAR的區別

二者均可用來存儲字符串。只不過 CHAR 經常使用來存儲固定長度的字符串,VARCHAR 經常使用來存儲可變長度的字符串。爲何要這樣區分呢?spa

首先看下面這個表格。CHAR(4) 和 VARCHAR(4) 的存儲對比。指針

CHAR(4) 存儲需求(字節) VARCHAR(4) 存儲需求(字節)
'' '    ' 4 '' 1
'ab' 'ab   ' 4 'ab' 3
'abcd' 'abcd' 4 'abcd' 5
'abcdefgh' 'abcd' 4 'abcd' 5

基於表格的內容,咱們能夠得出如下結論:code

對於 CHAR(4) ,

1.  不管插入什麼值,存儲需求都是不變的,固定4個字節。

2.  在實際存儲的時候,對於不足4字節的值,右邊會以空格填充。

對於 VARCHAR(4) ,

1.  存儲的需求與插入的值有關。

2.  存儲的需求 = 字符串所佔的字節數 + 1。爲何要加1呢?這個與 VARCHAR 的實現有關,爲了實現「按需分配」的目的,它須要額外的字節來表示字符串的長度。

 

因此,對於固定長度的字符串推薦使用 CHAR 來存儲,相對於 VARCHAR ,前者會少用一個字節。

另外,'abcdefgh'被截斷爲'abcd'進行存儲,是在 SQL_MODE 非嚴格模式下。具體什麼是 SQL_MODE 的非嚴格模式,可參考:使用MySQL,SQL_MODE有哪些坑,你知道麼?

 

2. VARCHAR(M) 能設置的最大長度

M 限制了 VARCHAR 能存儲的字符串的最大長度,注意,是字符,不是字節,其有效值範圍爲 0 ~ 65535。雖然可設置的範圍是 0 ~ 65535,但 M 真的就能設置爲65535 嗎?

看下面這個測試。

mysql> create table t (c1 varchar(65535)) charset latin1;
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> create table t (c1 varchar(65534)) charset latin1;
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> create table t (c1 varchar(65533)) charset latin1;
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> create table t (c1 varchar(65532)) charset latin1;
Query OK, 0 rows affected (0.06 sec)

由此來看,在 latin1 字符集下,M 最大就只能設置爲 65532。

 

其實否則,再看下面這個示例。

mysql> create table t (c1 varchar(65533) not null) charset latin1;
Query OK, 0 rows affected (0.06 sec)

mysql> create table t (c1 varchar(65534) not null) charset latin1;
Query OK, 0 rows affected (0.06 sec)

若是將列定義爲NOT NULL,M 最大可設置爲 65533。

 

上面演示的是latin1下的使用場景,若是是其它字符集呢?

mysql> create table t (c1 varchar(65533) not null) charset utf8mb4;
ERROR 1074 (42000): Column length too big for column 'c1' (max = 16383); use BLOB or TEXT instead

mysql> create table t (c1 varchar(65533) not null) charset utf8;
ERROR 1074 (42000): Column length too big for column 'c1' (max = 21845); use BLOB or TEXT instead

基於報錯信息,能夠看出,對於utf8mb4字符集,M最大隻能設置爲16383。對於utf8字符集,M最大隻能設置爲21845。這兩個數值是怎麼計算出來的呢?

在utf8mb4字符集中,最多須要4個字節來表示一個字符,因此 65535 / 4 = 16383 。而在utf8字符集中,最多須要3個字節來表示一個字符,因此 65535 / 3 = 21845。

由此來看,在設置 M 的大小時,起決定做用的並非 M 的有效值範圍(0 ~ 65535),而是 M * 字符集的最大字節數不能超過65535個字節。

 

爲何不能超過 65535 字節呢?由於MySQL限制了一條記錄的最大長度就是 65535 字節。

 

除此以外,對於VARCHAR,在實際設置時,還需考慮如下兩個因素:

1.  MySQL須要1 ~  2個字節來表示字符串的長度。具體來講,若是字符串佔用的字節數在 0 ~255 之間,需1個字節來表示,若是大於 255 個字節,則需2個字節來表示。

2.  若是列定義爲NULL,額外還須要1個字節。

既然 65535 是記錄的最大長度,則這個限制不只僅是針對一列,而是全部列。即全部列的長度加起來不能超過65535。

看下面這個示例,指定了2個列,分別定義爲VARCHAR和INT。

mysql> create table t (c1 varchar(65530) not null,c2 int not null) charset latin1;
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> create table t (c1 varchar(65529) not null,c2 int not null) charset latin1;
Query OK, 0 rows affected (0.10 sec)

由於INT會佔4個字節,因此 c1 最大就只能設置爲65535 - 4 - 2 = 65529。這裏之因此要減去2,是由於varchar(65529)超過了255個字節,須要2個字節來表示其長度。

 

報錯信息中,提到「The maximum row size for the used table type, not counting BLOBs, is 65535」,即記錄的最大長度限制不包括BLOB等字段。

之因此將BLOB和TEXT排除在外,是由於它的內容會單獨存儲在其它頁中。但即使如此,存儲BLOB和TEXT的指針信息也須要9 ~ 12個字節,具體來講:

  • TINYTEXT(TINYBLOB): 9 字節
  • TEXT(BLOB): 10 字節
  • MEDIUMTEXT(MEDIUMBLOB): 11字節
  • LONGTEXT(LONGBLOB): 12字節。

看下面這個示例,指定了2個列,分別定義爲VARCHAR和TEXT。

mysql> create table t (c1 varchar(65524) not null,c2 text not null) charset latin1;
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 somecolumns to TEXT or BLOBs

mysql> create table t (c1 varchar(65523) not null,c2 text not null) charset latin1;
Query OK, 0 rows affected (0.13 sec)

由於TEXT佔了10個字節,因此 c1 最大可設置爲65535 - 10 - 2 = 65523。

 

注意,上面提到的一條記錄的最大長度不能超過65535字節是MySQL的限制,與存儲引擎無關。實際上,存儲引擎對行長也有限制。在InnoDB存儲引擎中,就規定一條記錄的最大長度不能超過數據頁大小的1/2。

InnoDB中數據頁的大小由innodb_page_size參數決定,默認爲16K。因此,在頁長爲16K的狀況下,InnoDB中一條記錄的大小不能超過8K。若是超過了8K,InnoDB會將部分變長字段存儲在外部頁中。

 

看下面這個示例,由於定義的都是CHAR,定長字段,因此不會存儲在外部頁中。

mysql> create table t (
            c1 char(255),c2 char(255),c3 char(255),
            c4 char(255),c5 char(255),c6 char(255),
            c7 char(255),c8 char(255),c9 char(255),
            c10 char(255),c11 char(255),c12 char(255),
            c13 char(255),c14 char(255),c15 char(255),
            c16 char(255),c17 char(255),c18 char(255),
            c19 char(255),c20 char(255),c21 char(255),
            c22 char(255),c23 char(255),c24 char(255),
            c25 char(255),c26 char(255),c27 char(255),
            c28 char(255),c29 char(255),c30 char(255),
            c31 char(255),c32 char(255)
            ) charset latin1;
ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help. In current row format, BLOB prefix of 0 bytes is stored inline.

 

3. VARCHAR的空間使用狀況

給定一個VARCHAR類型的字符串,怎麼知道它的空間使用狀況呢?

首先,看看官方文檔的說法。

類型 存儲需求
VARCHAR(M) L + 1 bytes if column values require 0 − 255 bytes, L + 2 bytes if values may require more than 255 bytes

這裏的 L 指的是字符串所佔的字節數,與字符集有關。

以「中國」爲例。

在utf8mb4中,一箇中文漢字須要 3 個字節來表示,因此,「中國」經過utf8mb4來存儲,會佔用6個字節。

而在GBK中,一箇中文漢字只需2個字節來表示,因此,「中國」經過GBK來存儲,會佔用4個字節。

除此以外,還須要額外的1~2個字節來表示字符串的長度。

 

4. VARCHAR(50) 和 VARCHAR(500) 的區別

不少人可能會好奇,VARCHAR 不是按需分配的麼?在知足業務的需求狀況下,設置 VARCHAR(50) 和 VARCHAR(500) 有什麼區別呢?最後的空間佔用還不是以實際寫入的字符串爲主,從這個角度來看,確實沒錯。但考慮如下兩點:

1. 索引有最大長度的限制

對於行格式爲 REDUNDANT 或 COMPACT 的InnoDB表,索引的最大長度被限制爲767字節。因此,在MySQL 5.6 中,在建立索引時,咱們一般會碰到「Index column size too large. The maximum column size is 767 bytes」錯誤:

mysql> create table t(c1 varchar(200)) charset=utf8mb4;   
Query OK, 0 rows affected (0.02 sec)

mysql> alter table t add index(c1);
ERROR 1709 (HY000): Index column size too large. The maximum column size is 767 bytes.

上面這個限制不只僅適用於單個索引,一樣也適用於複合索引。

要解決這個問題,可選的方案有:

1.  減小字段的長度,確保字段的長度 * 字符集的最大字節數不超過767。

2.  設置前綴索引,確保前綴索引的長度 * 字符集的最大字節數不超過767。

mysql> alter table t add index(c1(191));
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

3.  將表的行格式設置爲 DYNAMIC 或 COMPRESSED,同時開啓innodb_large_prefix,索引的最大長度可支持3072字節。

在 MySQL 5.6 中,若是要將表的行格式設置爲DYNAMIC 或 COMPRESSED,必須將參數innodb_file_format設置爲Barracuda。

innodb_file_format用來設置InnoDB文件的格式,支持的文件格式有Antelope和Barracuda。Antelope是最先的文件格式,支持REDUNDANT和COMPACT這兩種行格式,而Barracuda則是最新的文件格式,支持DYNAMIC和COMPRESSED這兩種行格式。

在 MySQL 5.6 中,innodb_file_format默認爲Antelope,建立表時,若是沒有顯式指定row_format,則默認爲Compact。

在 MySQL 5.7 中,innodb_file_format默認爲Barracuda,建立表時,若是沒有顯式指定row_format,則默認爲Dynamic。

在 MySQL 8.0 中,innodb_file_format被移除了,取而代之的是innodb_default_row_format。該參數用來設置默認的row_format,默認值爲dynamic。

而innodb_large_prefix呢?在 MySQL 5.6 中,默認爲OFF。在 MySQL 5.7 中,默認爲ON,在 MySQL 8.0中,也移除了。

這也是爲何在 MySQL 5.6 中,更容易出現「Index column size too large. The maximum column size is 767 bytes」錯誤。

 

2. MEMORY引擎的限制

在 MySQL 8.0 以前,內存臨時表只支持 MEMORY 引擎,而 MEMORY 引擎會將 VARCHAR 等變長類型做爲定長來分配內存。

When in-memory internal temporary tables are managed by the MEMORY storage engine, fixed-length row format is used. VARCHAR and VARBINARY column values are padded to the maximum column length, in effect storing them as CHAR and BINARY columns.

可喜的是,從 MySQL 8.0 開始,內存臨時表支持 TempTable 引擎。TempTable 引擎,從 MySQL 8.0.13 開始,優化了VARCHAR等變長類型的存儲。

 

5. 總結

1.  對於固定長度的字符串推薦使用 CHAR 來存儲。

2.  VARCHAR能設置的最大長度與使用的字符集、自身佔用的字節數、是否認義爲NULL有關。

3.  MySQL限制了一條記錄的最大長度是 65535 字節。

4.  InnoDB存儲引擎限制了一條記錄的最大長度不能超過數據頁大小的1/2。但在實際處理的時候,InnoDB會將部分變長字段存儲在外部頁中,因此咱們實際能存儲的比限制的要大。

5.  字符串所佔的字節數,與字符集有關。除此以外,還須要額外的1~2個字節來表示字符串的長度。

6.  在知足業務需求的狀況下,VARCHAR應越短越好。

7.  對於行格式爲 REDUNDANT 或 COMPACT 的InnoDB表,索引的最大長度爲767字節。

8.  對於行格式爲 DYNAMIC 或 COMPRESSED 的InnoDB表,索引的最大長度爲3072字節。

9.  MEMORY 引擎會將 VARCHAR 等變長類型做爲定長來分配內存。

相關文章
相關標籤/搜索