ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Oracle VARCHAR2(200) 和 MySQL VARCHAR(200) 到底差在哪?字符集与字节陷阱详解

Oracle VARCHAR2(200) 和 MySQL VARCHAR(200) 到底差在哪?字符集与字节陷阱详解 做数据库开发和迁移这些年被问得最多的一个小问题就是 Oracle 的 VARCHAR2(200) 和 MySQL 的 VARCHAR(200) 到底有什么区别。很多人从 MySQL 转过去写 Oracle或者从 Oracle 迁回 MySQL看到两边定义都是 200心想“200 就是 200能有什么不同”结果一插入中文就报错或者迁移之后字段长度不够数据被硬生生截断。这个问题看似是最基础的知识点但实际踩坑的人真不少。今天我就把它彻底拆开讲明白Oracle 的 VARCHAR2(200) 和 MySQL 的 VARCHAR(200)最大支持的字节数和字符数到底一不一样哪些情况下一样哪些情况下差得远。1. 字符和字节的关系——问题的根源1.1 一个汉字等于几个字节要搞清长度定义第一件事是把字符和字节分开。字符是人眼看到的“字”字节是计算机存储的基本单位。一个字符占多少字节取决于数据库用的字符集也就是“用什么编码把这个字存下来”。以最常见的 UTF-8 编码为例英文字母和数字是 1 字节大部分常用汉字是 3 字节少数生僻字和表情符号是 4 字节。如果是 GBK 编码一个汉字是 2 字节。如果是 latin1所有字符都是 1 字节。所以“200 个字符能存多少数据”这个问题在没确定字符集之前压根没有答案。拿一个最直观的例子说同样是“中国”这两个字在 UTF-8 下占 6 字节在 GBK 下占 4 字节在 latin1 下干脆存不了。同一个字符串在不同字符集里物理大小完全不同。很多业务系统现在都用 UTF-8 家族所以“一个汉字 3 字节”是最常看到的数字。但要注意几个命名上的坑MySQL 的 utf8 是个历史遗留命名实际是 utf8mb3最多 3 字节一个字符utf8mb4 才是真正的 UTF-8最多 4 字节一个字符。Oracle 那边常见的是 AL32UTF8也是最多 4 字节一个字符只是大部分常用汉字落在 3 字节区间。不要一看到“UTF-8”就想当然认为所有字符都是 3 字节。1.2 为什么“200”在两个数据库里含义完全不同同一个“200”Oracle 和 MySQL 表达的单位不一样这才是问题的根源Oracle 的 VARCHAR2(200)在默认情况下这个 200 是字节数。MySQL 的 VARCHAR(200)这个 200 是字符数。注意我说的不是“Oracle 一定按字节、MySQL 一定按字符”这么绝对。Oracle 完全可以通过 CHAR 语义让 200 变成字符数但默认值是字节MySQL 则无论怎么设VARCHAR(n) 里的 n 都是字符数。正是这种默认规则和语义的错位导致了大量认知混淆。打个比方同样是“一口锅能煮多少米”Oracle 默认告诉你的是“锅容量是 200 克米”重量MySQL 告诉你的是“锅能煮 200 粒米”粒数。你要是以为两边都是 200 粒或者都是 200 克那煮出来的饭量完全对不上。下面两部分我分别把两个数据库的规则解剖清楚。2. Oracle VARCHAR2(200) 里那个 200 到底是什么2.1 默认是字节NLS_LENGTH_SEMANTICS 参数在背后起作用在 Oracle 里VARCHAR2(n) 的 n 到底是什么单位由一个初始化参数决定NLS_LENGTH_SEMANTICS默认值是 BYTE。也就是说你写 VARCHAR2(200)建出来的是一个“最多容纳 200 字节”的列。这意味着什么取决于数据库字符集数据库字符集是 AL32UTF8最常见的 Oracle UTF-8 字符集时英文字母和数字每字符 1 字节所以能存 200 个英文字母汉字每字符 3 字节所以 200 除以 3 等于 66 个汉字余 2 字节第 67 个汉字放不下。数据库字符集是 ZHS16GBK国内老系统很常见时汉字每字符 2 字节所以 200 除以 2 等于 100 个汉字英文字母每字符 1 字节能存 200 个。这就是为什么很多人遇到“Oracle 只能存 66 个汉字”的诡异现象而且整个团队都说不清原因。你在 SQL*Plus 里敲 VARCHAR2(200)如果字符集是 AL32UTF8实测超过 66 个汉字就报 ORA-12899一点都不客气。ORA-12899 的全称是 value too large for column中文意思是“值太大放不进该列”错误信息里会带上列名、实际值和最大长度排查时看后面那串数字非常有帮助。这里有个历史原因值得说一下Oracle 早期版本主要面向单字节字符集为主的场景字节数直接对应磁盘占用好估算容量所以默认按字节。但随着业务国际化这个默认值就成了大坑所以 Oracle 后来才提供 CHAR 语义作为补救。2.2 用 CHAR 语义绕开“数不清汉字”的尴尬既然默认是字节Oracle 也给了开关。最简单的方式是在建表时显式写CREATE TABLE demo_table ( col1 VARCHAR2(200 CHAR) );上面的 col1 就被定义为“最多 200 个字符”这个字符数和字符集无关。200 个汉字就是 200 个汉字即使它们在 AL32UTF8 下实际占 600 字节也不会超限前提是总字节数不超过 Oracle 的列长度上限这个下面讲。也可以在系统层面改默认值ALTER SYSTEM SET NLS_LENGTH_SEMANTICS CHAR SCOPE BOTH;这样后续创建的 VARCHAR2 列默认就按字符数算了。注意这个参数不会改变已经存在的列因为每一列的长度语义在建表那一刻就固定了。判断一个现有列到底是字节语义还是字符语义可以查数据字典SELECT table_name, column_name, char_used, char_length FROM user_tab_columns WHERE table_name DEMO_TABLE AND column_name COL1;char_used 字段是关键B 表示 BYTE默认C 表示 CHAR字符语义。char_length 就是你 DDL 里写的那个数字比如 200。Oracle 官方文档和大部分 DBA 的建议都是如果业务以中文存储为主或者表要跟 MySQL 等按字符定义长度的系统对齐建议显式使用 CHAR 语义。否则你的字段到底能存多少个中文开发人员得天天按计算器。2.3 4000 和 32767Oracle 的长度天花板除了“200 是字节”这个坑Oracle 的 VARCHAR2 还有一层限制总长度上限。在 11g 以及更早的版本VARCHAR2 的长度上限是 4000 字节。注意是字节跟你是 BYTE 语义还是 CHAR 语义没关系物理占用的最大字节数不能超过 4000。举个例子如果字符集是 AL32UTF8你想定义 VARCHAR2(2000 CHAR)2000 个汉字理论上占 6000 字节超过 4000Oracle 会直接报 ORA-00910specified length too long for its datatype根本建不出这个列。12c 开始Oracle 引入了扩展数据类型的概念通过设置 MAX_STRING_SIZE EXTENDED可以把 VARCHAR2 的上限提到 32767 字节。但这个操作需要迁移系统字典不是随手就能改的绝大多数生产环境并不会启用。所以你日常遇到的 OracleVARCHAR2 基本就是 4000 字节封顶。这个天花板对后面聊跨库迁移很重要——很多人就是因为没意识到“4000”是字节才在迁移时踩了连环坑。3. MySQL VARCHAR(200) 里那个 200 又是什么3.1 n 是字符数物理存储按字符集算MySQL 这边简单很多VARCHAR(n) 的 n 定义的就是字符数。建 VARCHAR(200)就表示这个列最多容纳 200 个字符不管字符集是 latin1、gbk、utf8 还是 utf8mb4200 个汉字就是 200 个汉字200 个英文字母就是 200 个英文字母。但“最多放 200 个字符”绝不等于“最多占用 200 字节”。物理存储字节数等于实际字符数乘以字符集单字符最大字节数再加上长度前缀。比如一张 utf8mb4 的表VARCHAR(200) 列存满 200 个汉字存储需要 200 乘以 4 加 2大概 802 字节。这里要澄清一个误区这个 802 不是预分配的空间。InnoDB 是变长存储你实际只存了 20 个字符就只占 20 个字符对应的字节数。但 802 这个数字在 DDL 规划和行大小计算时是躲不开的MySQL 会按“最坏情况”来校验你的表结构是否合法。MySQL 中 n 始终是字符数这是和 Oracle 默认行为最核心的分水岭。也正因为这样很多从 MySQL 转去学 Oracle 的开发第一次写建表语句都栽在“我把 200 当字符数了”上面。3.2 65535 字节的行上限真正的紧箍咒MySQL 的 VARCHAR 还有一个隐藏约束所有列共享一条 65535 字节的行大小上限。注意是“行”的上限不是“列”的上限。就是说一行里所有列的最大可能存储字节数加在一起不能超过 65535 字节超了就报错。为什么是 65535因为 MySQL 行格式里用来记录行大小的字段是 2 字节2 的 16 次方是 65536再减去 1 就是 65535。这意味着一个 utf8mb4 字符集下的单列 VARCHAR理论上最多能定义到多少字符我们算一下65535 字节中要先减去长度前缀。当最大可能字节数超过 255 字节时VARCHAR 需要 2 字节的长度前缀来记录长度不超过 255 字节时只需要 1 字节。按 utf8mb4 每字符最多 4 字节算近似公式很好记n_max ≈ (65535 - 2) / 4 16383字符也就是说一张只有这一个 VARCHAR 列的表utf8mb4 下最大能定义到 VARCHAR(16383)。如果一行里还有其他列或者列允许 NULL实际能定义的上限还会再小一点。很多 DBA 口头禅是“varchar 最大 65535”这句话严格说是不准确的。它真正的意思是所有列加起来的总字节数不能超过 65535单列长度因此被间接压在天花板之下。理解这一点你就知道为什么不能张口就说“那我把所有字段都设成 VARCHAR(60000)”——因为两三个字段就顶爆行上限了。3.3 不同字符集下的实际容量测算既然 n 是字符数我们需要按字符集算物理存储上限。下面这张表是假设一张表只有这一个 VARCHAR 列、且列不允许 NULL 时的最大字符数实际建表时若有其他列需要重新汇总字符集每字符最大字节数单列 VARCHAR 最大字符数VARCHAR(200) 的最大占用latin1165533202 字节gbk232766402 字节utf8321844602 字节utf8mb4416383802 字节这张表的单列最大字符数是理论最大值实际业务表里会因为其他列存在、可空标记、InnoDB 行格式开销等因素再降一点。但至少能直观看到VARCHAR(200) 的物理开销并不夸张离 65535 远得很日常表里用它是个很安全的定义。不过别把 200 划等号成“最多 200 字节”。在 utf8mb4 下VARCHAR(200) 存满 200 个汉字需要 800 多字节这个量级容易被忽略。哪些场景会突然暴露问题最常见的是跨库迁移、导出导入、排序缓冲和 JSON 字段混合计算时你会惊讶“怎么 200 个字符这么占空间”。4. 一张表看穿 Oracle 和 MySQL 的差异4.1 同字符集下两边容量对照现在把两边放到同一坐标系里对比。看两个最常见场景场景 AOracle 字符集 AL32UTF8MySQL 字符集 utf8mb4两者都接近完整 UTF-8 场景 BOracle 字符集 ZHS16GBKMySQL 字符集 gbk国内很多老系统还是这套定义Oracle 实际语义Oracle 最大容量MySQL 实际语义MySQL 最大容量VARCHAR2(200)Oracle 默认 BYTE200 字节AL32UTF8 下 66 个汉字GBK 下 100 个汉字MySQL 无此类型不适用VARCHAR(200)MySQL 默认Oracle 也可写但官方不用取决于 Oracle 端定义200 字符utf8mb4 下 200 个汉字最多 802 字节VARCHAR2(200 CHAR)显式字符语义200 字符200 个汉字约 600 字节不适用不适用VARCHAR(200) 作为迁移目标原列如果是 200 字符迁移到 MySQL 需按字符重估200 字符存储上限 802 字节这张表要表达的核心是当 Oracle 用默认 BYTE 语义、MySQL 用默认 utf8mb4 时“VARCHAR2(200)”和“VARCHAR(200)”的实际含义差得很远。Oracle 那个只能放 66 个汉字MySQL 这个能放 200 个汉字。方向不同结论完全相反Oracle 迁到 MySQL如果原 Oracle 列是 VARCHAR2(200) BYTE且存的是中文迁到 MySQL 后把它扩成 VARCHAR(200)容量反而变大了。这是好消息但也容易掩盖问题——如果你按“兼容原长度”的思路只设 VARCHAR(66)后面业务加长就尴尬了。MySQL 迁到 Oracle如果原 MySQL 列是 VARCHAR(200)且实际存了 100 个以上的汉字迁移到 Oracle 时照抄 VARCHAR2(200)一定报错。必须写成 VARCHAR2(200 CHAR)或者先估算字节数再扩容。这是最危险、最容易忽视的跨库坑。4.2 四种常见“想当然”错在哪我把带团队时经常遇到的错误直觉归纳成四条每条都是真实踩过的错误直觉一“VARCHAR2(200) 和 VARCHAR(200) 都是 200 个字符差不多。”不对。Oracle 默认是字节200 个字符在 AL32UTF8 下最多 66 个汉字。只有显式写成 CHAR 语义才跟 MySQL 的字符数一致。错误直觉二“VARCHAR2(200) 能存 200 个英文所以也能存 200 个中文。”不对。英文 1 字节一个中文 3 字节一个200 字节塞不下 200 个中文。这就像 200 个箱子只能装下 66 个大家具箱子是按体积算的不是按件数算的。错误直觉三“MySQL 的 VARCHAR(200) 就是最多 200 字节。”不对。它是 200 字符在 utf8mb4 下最多占 802 字节。同理MySQL 里 VARCHAR(200) 存 200 个中文完全合法。错误直觉四“两边数据库的 VARCHAR 最大长度不都是很大的数吗按大的设就行。”不对。Oracle 单列上限是 4000 字节12c 扩展后是 32767 字节MySQL 是行大小 65535 字节约束下的字符数具体能设多少要看字符集。两边根本不是同一套衡量标尺。到这里基础概念已经说透。但光讲概念不行下面看实际工作里怎么判断、迁移时怎么换算。5. 跨库场景下怎么判断和换算5.1 动手前先查底细在迁移或对接前第一件事是查目标数据库的底细。Oracle 需要查三个信息数据库字符集SELECT value FROM nls_database_parameters WHERE parameter NLS_CHARACTERSET;长度语义SHOW PARAMETER nls_length_semantics;具体列的定义是字节还是字符语义SELECT char_used, char_length FROM user_tab_columns WHERE table_name DEMO_TABLE AND column_name COL1;char_used B 表示字节语义char_used C 表示字符语义。char_length 是建表时写的那个数字比如 200。注意只看 char_length 还不够因为这个数字本身不告诉你单位必须结合 char_used 一起看。MySQL 这边可以用SELECT table_name, column_name, character_maximum_length, character_octet_length FROM information_schema.columns WHERE table_name DEMO_TABLE AND column_name COL1;character_maximum_length字符数也就是 VARCHAR(200) 里的 200。character_octet_length按当前字符集计算的最大字节数。在 utf8mb4 下它等于 200 乘以 4也就是 800。这个查询在排查跨库迁移时非常实用一眼就能看出目标字段的真实字节开销。除了查字典还可以用函数直接测实际数据。Oracle 里LENGTH(col) 返回字符数。LENGTHB(col) 返回字节数。MySQL 里CHAR_LENGTH(col) 返回字符数。OCTET_LENGTH(col) 返回字节数。用这两个函数查一下现有数据的最大字符数和最大字节数比任何理论估算都准确。5.2 Oracle 迁 MySQL 的换算实例分享一个实际处理过的场景。某系统从 Oracle 迁到 MySQLOracle 里字段定义是 VARCHAR2(4000)字符集 AL32UTF8字节语义。因为 AL32UTF8 一个汉字最多占 3 字节常用汉字所以这个列实际最多只能存 1333 个汉字。迁到 MySQL字符集 utf8mb4如果直接写成 VARCHAR(4000)按定义是 4000 字符物理最大占用是 4000 乘以 4 加 2等于 16002 字节没超过单列 16383 的极限建表能建出来。但原 Oracle 列最多装 1333 汉字新 MySQL 列能装 4000 汉字容量是变大了。容量变大不算坏事可如果这个列要建索引utf8mb4 下一个索引键能容纳的字符数会明显变少可能出现“索引过长”的新报错。如果 Oracle 端用的是 CHAR 语义VARCHAR2(4000 CHAR)那就更麻烦了。AL32UTF8 下 4000 个汉字占 12000 字节迁到 MySQL utf8mb4 后如果表里还有其他字段行大小很容易超标需要把 VARCHAR(4000) 拆分成多个更小的 VARCHAR或者干脆转成 TEXT。更极端的坑是 Oracle 12c 启用扩展后 VARCHAR2(32767)。这种字段迁到 MySQL 几乎没法直接写成 VARCHAR(32767)因为 32767 乘以 4 加 2 远超 65535通常只能降级成 TEXT 类型。TEXT 在 MySQL 里不受 65535 行大小限制但它不能有默认值、索引处理更麻烦、排序和临时表也相对吃亏。所以跨库迁移时“Oracle 长 VARCHAR2”换“MySQL 的 TEXT”是最常见的方案但一定要仔细评估 TEXT 带来的副作用。5.3 开发规范建议基于这些实战经验建议把下面几条写进开发规范Oracle 建表涉及中文字段时一律显式写 CHAR 语义比如 VARCHAR2(200 CHAR)不要依赖系统默认的 BYTE 语义。MySQL 建表统一用 utf8mb4尽量避免使用 utf8 别名和 gbk否则后期字符集升级又是一轮连环坑。跨库迁移前先做一次字段语义清单把两边每个字段的字符数上限、字节上限、字符集逐项对齐不要只在建表脚本上做文本替换。设计评审里明确“200 到底是字节还是字符”这个口径让开发、测试、DBA 都形成统一认识。很多事故不是技术难度高而是团队里一半人以为是字节、一半人以为是字符。6. 常见问题速查与避坑记录6.1 一插 200 个汉字就报 ORA-12899怎么破现场描述某业务表字段是 VARCHAR2(200)字符集 AL32UTF8开发往里面插 200 个汉字报了 ORA-12899: value too large for column。原因VARCHAR2(200) 默认是 200 字节在 AL32UTF8 下200 个汉字需要 600 字节远超 200 字节上限。Oracle 最多只能塞下 66 个汉字第 67 个汉字开始就越界。解决办法最直接的方式是改列定义ALTER TABLE demo_table MODIFY (col1 VARCHAR2(200 CHAR));这里要提醒一句生产环境执行 ALTER TABLE 之前注意表锁和回滚段开销尽量选低峰操作。而且如果原列里已经存了超过 200 字符的数据修改会失败得先处理存量数据。如果不想改列也可以把插入数据先截断但那只是治标不治本业务迟早还会踩。顺带强调VARCHAR2(200 CHAR) 不是“把 200 个字符硬塞进 200 字节”它允许这段数据在 AL32UTF8 下实际占 600 字节。你的目标应该是让 DDL 表达“最多 200 字符”这个业务语义而不是把物理字节数强行压小。6.2 VARCHAR 和 VARCHAR2 在各自数据库里是什么关系这个问题经常被混在一起讨论分开说就很清楚Oracle 端VARCHAR 和 VARCHAR2 目前行为基本一致但 Oracle 官方强烈建议使用 VARCHAR2。原因很简单VARCHAR 是 ANSI 标准类型Oracle 不保证未来版本的语义不会变生产脚本一律用 VARCHAR2 最稳妥。MySQL 端没有 VARCHAR2 这个类型。如果直接把 Oracle 的建表脚本扔到 MySQL 里跑VARCHAR2 会直接语法报错必须改成 VARCHAR。还有一个常见误区VARCHAR(200) 里的 200 不是“显示宽度”。MySQL 8.0 中显示宽度特性已经废弃VARCHAR 的 n 就是字段容量上限别跟旧版整型那种 INT(11) 的显示宽度混为一谈。6.3 5 分钟实测你的环境理论说再多不如动手验证一次。我建议接触新库时做一个小实验成本极低但能根治认知偏差。Oracle 端CREATE TABLE char_byte_test (c1 VARCHAR2(200)); SELECT char_used FROM user_tab_columns WHERE table_name CHAR_BYTE_TEST AND column_name C1; -- 如果返回 B说明默认是字节语义 INSERT INTO char_byte_test (c1) VALUES (LPAD(啊, 67, 啊)); -- 大概率报 ORA-12899 INSERT INTO char_byte_test (c1) VALUES (LPAD(啊, 66, 啊)); -- 66 个汉字能插入MySQL 端CREATE TABLE char_byte_test (c1 VARCHAR(200)) CHARACTER SET utf8mb4; SELECT character_maximum_length, character_octet_length FROM information_schema.columns WHERE table_name char_byte_test AND column_name c1; -- 结果是 200 和 800 INSERT INTO char_byte_test (c1) VALUES (REPEAT(啊, 200)); -- 正常插入200 个汉字没压力这个小实验 5 分钟就能做完但对团队统一认知特别有效。经历过一次 ORA-12899或者一次成功插入 200 汉字之后你对这个知识点的记忆会牢固很多。我带队时用这招比贴十页文档管用。最后分享一个个人习惯每次做数据库设计评审我都会把 CREATE TABLE 语句里的每个 VARCHAR/VARCHAR2 拉出来过一遍先问三个问题——这个字段在 Oracle 里是字节还是字符语义MySQL 里定义的 n 在目标字符集下会占多少字节如果将来跨库迁移这个字段应该换成什么类型这三个问题过完绝大多数长度踩坑都能提前干掉。你也可以试试慢下来别只在报错时才回头看定义。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进