你的 VARCHAR 長度合適麼?
作者:官永強,愛可生 DBA 團隊成員,擅長 MySQL 運維方面的技能。熱愛學習新知識,亦是個愛打遊戲的宅男。
作者:李富強,愛可生 DBA 團隊成員,熟悉 MySQL,TiDB,OceanBase 等數據庫。相信持續把對的事情做好一點,會有不一樣的收穫。
愛可生開源社區出品,原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。
本文約 2200 字,預計閱讀需要 8 分鐘。
背景描述
有客戶反饋,他們對一個 VARCHAR 類型的字段進行長度擴容。第一次可以很快就可以修改好,但是第二次卻需要執行很久。比較疑惑明明表中的數據量是差不多的,爲什麼從 VARCHAR(20)
調整爲 VARCHAR(50)
就比較快,但是從 VARCHAR(50)
調整爲 VARCHAR(100)
就需要執行很久呢? 於是我們對該情況進行場景復現並進行問題分析。
環境信息
本次驗證涉及到的產品及版本信息如下:
產品 | 版本 |
---|---|
MySQL | 5.7.25-log MySQL Community Server (GPL) |
Sysbench | sysbench 1.0.17 |
場景復現
3.1 數據準備
mysql> show create table test.sbtest1;
+---------+----------------------------------------+
| Table | Create Table |
+---------+----------------------------------------+
| sbtest1 | CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` varchar(20) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
`pad` varchar(20) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
+---------+----------------------------------------+
1 row in set (0.00 sec)
mysql> select count(*) from test.sbtest1;
+----------+
| count(*) |
+----------+
| 1000000 |
+----------+
1 row in set (0.10 sec)
3.2 問題驗證
模擬客戶的描述,我們對字段 c
進行修改,將 VARCHAR(20)
修改爲 VARCHAR(50)
後再修改爲 VARCHAR(100)
,並觀察其執行所需時間,以下是相關的操作命令以及執行結果:
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(50);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> show create table test.sbtest1;
+---------+-------------------------------+
| Table | Create Table |
+---------+-------------------------------+
| sbtest1 | CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` varchar(50) COLLATE utf8mb4_bin DEFAULT NULL,
`pad` varchar(20) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
+---------+--------------------------------------------------------+
1 row in set (0.00 sec)
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(100);
Query OK, 1000000 rows affected (4.80 sec)
Records: 1000000 Duplicates: 0 Warnings: 0
mysql> show create table test.sbtest1;
+---------+---------------------------+
| Table | Create Table |
+---------+---------------------------+
| sbtest1 | CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` varchar(100) COLLATE utf8mb4_bin DEFAULT NULL,
`pad` varchar(20) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
+---------+------------------------------------------------------------------------+
1 row in set (0.00 sec)
通過驗證發現,該問題會穩定復現,故繼續嘗試去修改,最終發現在修改 VARCHAR(63)
爲 VARCHAR(64)
時需要執行很久,但在 64 之後繼續進行長度擴容發現可以很快完成。
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(63);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(64);
Query OK, 1000000 rows affected (4.87 sec)
Records: 1000000 Duplicates: 0 Warnings: 0
mysql> show create table test.sbtest1;
+---------+---------------+
| Table | Create Table |
+---------+---------------+
| sbtest1 | CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
`pad` varchar(20) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
+---------+------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(65);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(66);
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
3.3 問題分析
對於 VARCHAR(63)
修改爲 VARCHAR(64)
需要執行很久的這個情況進行分析。通過查閱官方文檔 發現,由於 VARCHAR
字符類型在字節長度爲 1 時可存儲的字符爲 0~255。當前字符集類型爲 UTF8MB4,由於 UTF8MB4 爲四字節編碼字符集,即一個字節長度可存儲 63.75(255/4)個字符,所以當我們將 VARCHAR(63)
修改爲 VARCHAR(64)
時,需要增加一個字節去進行數據的存儲,就要通過建立臨時表的方式去完成本次長度擴容,故需要花費大量時間。
拓展驗證
4.1 數據準備
mysql> show create table test_utf8.sbtest1;
+---------+----------------------------------------+
| Table | Create Table |
+---------+----------------------------------------+
| sbtest1 | CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` varchar(20) NOT NULL DEFAULT '',
`pad` varchar(20) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8 |
+---------+------------------+
1 row in set (0.00 sec)
mysql> select count(*) from test_utf8.sbtest1;
+----------+
| count(*) |
+----------+
| 1000000 |
+----------+
1 row in set (0.10 sec)
4.2 UTF8 場景驗證
由於 UTF8 爲三字節編碼字符集,即一個字節可存儲 85(255/3=85)個字符。
本次修改順序:VARCHAR(20)→VARCHAR(50)→VARCHAR(85),並觀察其執行所需時間,以下是相關的操作命令以及執行結果:
mysql> ALTER TABLE test_utf8.sbtest1 MODIFY c VARCHAR(50) ,algorithm=inplace,lock=none;
Query OK, 0 rows affected (0.01 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE test_utf8.sbtest1 MODIFY c VARCHAR(85) ,algorithm=inplace,lock=none;
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> show create table test_utf8.sbtest1;
+---------+-------------------------------+
| Table | Create Table |
+---------+-------------------------------+
| sbtest1 | CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` varchar(85) DEFAULT NULL,
`pad` varchar(20) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8 |
+---------+--------------------------------------------------+
1 row in set (0.00 sec)
修改順序:VARCHAR(85)→VARCHAR(86)→VARCHAR(100),此時我們會觀察到執行的 SQL 語句直接返回報錯。於是我們刪除 algorithm=inplace ,lock=none
這兩個參數,即允許本次 SQL 創建臨時表以及給目標表上鎖,然後重新執行 SQL,以下是相關的操作命令以及執行結果:
mysql> ALTER TABLE test_utf8.sbtest1 MODIFY c VARCHAR(86) ,algorithm=inplace,lock=none;
ERROR 1846 (0A000): ALGORITHM=INPLACE is not supported. Reason: Cannot change column type INPLACE. Try ALGORITHM=COPY.
mysql> ALTER TABLE test_utf8.sbtest1 MODIFY c VARCHAR(86);
Query OK, 1000000 rows affected (4.94 sec)
Records: 1000000 Duplicates: 0 Warnings: 0
mysql> show create table test_utf8.sbtest1;
+---------+-------------------------------+
| Table | Create Table |
+---------+-------------------------------+
| sbtest1 | CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` varchar(86) DEFAULT NULL,
`pad` varchar(20) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8 |
+---------+--------------------------------------------------+
1 row in set (0.00 sec)
mysql> ALTER TABLE test_utf8.sbtest1 MODIFY c VARCHAR(100) ,algorithm=inplace,lock=none;
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
4.3 UTF8MB4 場景驗證
由於 UTF8MB4 爲四字節編碼字符集,即一個字節長度可存儲 63(255/4=63.75)個字符。
本次修改順序:VARCHAR(20)→VARCHAR(50)→VARCHAR(63),並觀察其執行所需時間,以下是相關的操作命令以及執行結果:
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(50) ,algorithm=inplace,lock=none;
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(63) ,algorithm=inplace,lock=none;
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> show create table test.sbtest1;
+---------+-------------------------+
| Table | Create Table |
+---------+-------------------------+
| sbtest1 | CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` varchar(63) COLLATE utf8mb4_bin DEFAULT NULL,
`pad` varchar(20) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
+---------+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
本次修改順序:VARCHAR(63)→VARCHAR(64)→VARCHAR(100),此時我們會觀察到執行的 SQL 語句直接返回報錯。於是我們刪除 algorithm=inplace, lock=none
這兩個參數,即允許本次 SQL 創建臨時表以及給目標表上鎖,然後重新執行 SQL,以下是相關的操作命令以及執行結果:
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(64) ,algorithm=inplace,lock=none;
ERROR 1846 (0A000): ALGORITHM=INPLACE is not supported. Reason: Cannot change column type INPLACE. Try ALGORITHM=COPY.
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(64) ;
Query OK, 1000000 rows affected (4.93 sec)
Records: 1000000 Duplicates: 0 Warnings: 0
mysql> show create table test.sbtest1;
+---------+--------------------------+
| Table | Create Table |
+---------+--------------------------+
| sbtest1 | CREATE TABLE `sbtest1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`k` int(11) NOT NULL DEFAULT '0',
`c` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL,
`pad` varchar(20) COLLATE utf8mb4_bin NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `k_1` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin |
+---------+-------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> ALTER TABLE test.sbtest1 MODIFY c VARCHAR(100) ,algorithm=inplace,lock=none;
Query OK, 0 rows affected (0.00 sec)
Records: 0 Duplicates: 0 Warnings: 0
4.4 對比分析
字符長度修改 | UTF8(MB3) | UTF8MB4 |
---|---|---|
20->50 | online ddl (inplace) | online ddl (inplace) |
50->100 | online ddl (copy) | online ddl (copy) |
X->Y | 當Y*3<256 時,inplace <br> 當X*3>=256,inplace | 當 Y*4<256 時,inplace <br> 當 X*4>=256,inplace |
備註 | 一個字符最大佔用 3 個字節 | 一個字符最大佔用 4 個字節 |
結論
當一個字段的最大字節長度 >=256 字符時,需要 2 個字節來表示字段長度。
使用 UTF8MB4 舉例:
- 對於字段的最大字節長度在 256 字符內變化 (即 x*4<256 且 Y*4<256),online ddl 走 inplace 模式,效率高。
- 對於字段的最大字節長度在 256 字符外變化 (即 x*4>=256 且 Y*4>=256) ,online ddl 走 inplace 模式,效率高。
- 否則,online ddl 走 copy 模式,效率低.
- UTF8(MB3) 同理。
建議
爲避免由於後期字段長度擴容,online ddl 走效率低的 copy 模式,建議:
- 對於 UTF8(MB3) 字符類型:
- 字符個數小於 50 個,建議設置爲 VARCHAR(50) 或更小的字符長度。
- 字符個數接近 84(256/3=83.33)個,建議設置爲varchar(84)或更大的字符長度。
- 對於 UTF8MB4 字符類型:
- 字符個數小於 50 個,建議設置爲 VARCHAR(50),或更小的字符長度。
- 字符個數接近 64(256/4=64)個,建議設置爲 VARCHAR(64) 或更大的字符長度。
本次驗證結果僅供參考,若您需要在生產環境中進行操作,請結合實際情況合理定義 VARCHAR 的長度,避免造成經濟損失。
更多技術文章,請訪問:https://opensource.actionsky.com/
關於 SQLE
SQLE 是一款全方位的 SQL 質量管理平臺,覆蓋開發至生產環境的 SQL 審覈和管理。支持主流的開源、商業、國產數據庫,爲開發和運維提供流程自動化能力,提升上線效率,提高數據質量。