MySQL VARCHAR 最佳長度評估實踐

你的 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 審覈和管理。支持主流的開源、商業、國產數據庫,爲開發和運維提供流程自動化能力,提升上線效率,提高數據質量。

SQLE 獲取

類型 地址
版本庫 https://github.com/actiontech/sqle
文檔 https://actiontech.github.io/sqle-docs/
發佈信息 https://github.com/actiontech/sqle/releases
數據審覈插件開發文檔 https://actiontech.github.io/sqle-docs/docs/dev-manual/plugins/howtouse
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章