拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MySQL VARCHAR能存多少汉字?字符集、行限制与主键设计全解析

今天在技术群里又看到有人在问varchar类型到底能存多少个汉字、多少个数字这个问题看起来基础可真要往深了说牵扯出的是字符集、字节数、行格式、索引长度这一连串知识点。我见过不少同事在建表时随便写个VARCHAR(255)结果遇到表情符号存不进去、主键选型犹豫半天、或者建索引报Row size too large的错。这篇文章就从这个高频问题入手把VARCHAR的存储原理、极限计算、主键设计这些事从头到尾捋一遍。无论是刚入门的新手还是写了好几年业务代码的老开发应该都能在这里找到能直接用的结论。1. 先搞清楚VARCHAR(M)里的M到底是什么意思1.1 M是字符数不是字节数很多人对VARCHAR(255)的理解是“最多能放255个字节”这是一个流传很广的误区。从MySQL 5.0.3开始VARCHAR(M)中的M统一表示字符数而不是字节数。换句话说VARCHAR(10)在utf8mb4字符集下既能存10个汉字也能存10个英文字母或者10个数字——因为M限制的是“字符个数”不是“字节个数”。这个改动其实坑过一批老项目。在MySQL 5.0.3之前VARCHAR(M)里的M确实表示字节数所以那个年代如果有人建了VARCHAR(255)打算在utf8下存中文实际只能存85个汉字255除以3。现在数据库版本早就升级到5.7、8.0了但很多老教程、老博客还在沿用旧的计算方式这就导致大家经常在各处看到互相矛盾的说法。我建议你在做表结构设计时先把“字符数”这个概念刻在脑子里。举个例子VARCHAR(64)表示最多64个字符无论你存的是纯数字、中文、英文还是混合内容字符数上限都是64。但不同内容最终占用的磁盘字节数不一样因为字符串在落盘时每个字符会根据字符集编码成不同长度的字节。1.2 变长存储到底怎么存长度前缀加数据VARCHAR是变长类型它不会像CHAR那样固定占用M个字符对应的字节数而是“用多少存多少”。但MySQL要能从行记录里正确切分出这个字段的边界就必须知道这个字段实际有多长所以在每个VARCHAR的值前面MySQL会额外存一段“长度前缀”。长度前缀的规则是这样的如果这个列定义后可能达到的最大字节数不超过255就用1个字节来记录实际长度如果超过255就用2个字节记录。举个例子utf8mb4下VARCHAR(63)最大字节数是63×4252不超过255所以长度前缀占1字节VARCHAR(64)最大字节数是64×4256超过255长度前缀就要占2字节。为什么要关心这个细节因为你在计算一行数据的字节开销时必须把长度前缀也算进去。尤其是后面要讲的65535行大小限制就差这几个字节就可能决定建表能不能成功。1.3 65535这个数字是怎么来的MySQL对数据行有一个硬性限制除TEXT、BLOB这类大对象列之外一行的总字节数不能超过65535字节。65535等于2的16次方减1这个限制来自MySQL底层行格式的历史设计当时用16位的偏移量来管理行内数据所以最大值就是65535。这个限制是整个“VARCHAR能存多少数据”问题的总闸门。VARCHAR(M)不是一个可以无限扩大的类型即使你把M写得很大最终一行里所有列加起来的最大可能字节数也不能超过65535否则建表直接报错。所以关于“能存多少汉字、多少数字”的计算本质上就是在65535这个预算里做减法。2. 字符集才是“能存多少汉字”的关键变量2.1 常用字符集每个字符占几个字节既然M是字符数那汉字和数字在VARCHAR里到底吃多少空间完全取决于表的字符集。数据库里常见的字符集有这么几种它们的编码逻辑差别挺大latin1单字节字符集每个字符固定1字节但不支持中文。gbk中文字符固定2字节英文字符和数字是1字节属于变长编码。utf8MySQL中的utf8mb3每个字符1到3字节绝大多数中文汉字占3字节英文和数字占1字节。utf8mb4这是现在MySQL 8.0的默认字符集每个字符1到4字节常规中文字符也是3字节但Emoji、部分冷僻汉字需要4字节。这里必须多说一句很多人以为utf8mb4下所有字符都占4字节这是不对的。“mb4”的意思是“maximum bytes 4”也就是单个字符最多4字节并不是每个字符固定4字节。你在utf8mb4的列里存一个“你”字实际占用3字节只有当遇到Emoji或扩展B区汉字时才会消耗4字节。下表把常见字符集的字节情况汇总了一下字符集一个中文汉字一个数字字符单字符最大字节数latin1不支持中文1字节1字节gbk2字节1字节2字节utf8mb33字节1字节3字节utf8mb43字节普通汉字/ 4字节Emoji、生僻字1字节4字节2.2 一张表算出不同字符集下VARCHAR能定义多长现在我们手上有两个关键数字行大小上限65535字节以及长度前缀的2字节开销。在单列、不允许为NULL的理想情况下VARCHAR(M)能定义到的最大M值约等于(65535-2)除以单字符最大字节数再取整数。我按这个公式算出了常用字符集下的极限值字符集每字符最大字节数VARCHAR定义上限M对应能存的最大汉字数latin1165533不支持gbk23276632766utf8mb332184421844utf8mb441638316383每个值我都验证过比如utf8mb4下VARCHAR(16383)最大字节数是16383×465532字节加上2字节长度前缀正好65534小于65535可以建表但VARCHAR(16384)直接就是16384×465536已经超过行大小限制建表必失败。这里要特别提醒一下能定义到这么极限的长度不代表实际业务里就该这么用。一旦表里还有其他列或者列允许NULL每一列都会从65535这个预算里扣掉自己的最大字节数、长度前缀和NULL标记位所以真实场景下的上限会明显低于上述理论值。2.3 常见误区utf8mb4下汉字不等于4字节接着辟一个流传很广的谣有人说utf8mb4下的VARCHAR(255)存不了255个汉字因为255×41020字节会被行大小限制卡住。这个说法完全是错误的。VARCHAR(255)限制的是字符数上限255个汉字确实能存进去。在utf8mb4下绝大多数汉字是3字节255个汉字实际占用765字节加上长度前缀也就767字节左右离65535远得很。只有当这255个字符全部是4字节的Emoji或者冷僻字时才会占用1020字节那同样也没超过65535。所以“utf8mb4下一个汉字占4字节”这种说法应该理解为“最多可能占4字节”而不是“见汉字就是4字节”。平时估算容量时我用的是“单字符最大字节数”来算VARCHAR定义上限用“业务实际内容”来估算真实占用这两件事分开看待就不容易出错。3. 实操建表测试VARCHAR的极限3.1 测试环境准备理论算完还是要动手验证一下心里才踏实。我用MySQL 8.0做了一组测试重点看不同字符集下VARCHAR定义长度的边界到底在哪里。先确认当前会话的字符集设置可以用这条命令SHOW VARIABLES LIKE character_set_server; SHOW VARIABLES LIKE collation_server;MySQL 8.0默认一般是utf8mb4和utf8mb4_0900_ai_ci所以下面的测试基于这个环境。如果想测其他字符集建表时用DEFAULT CHARSET指定即可。3.2 utf8mb4下的极限先在utf8mb4下建一个VARCHAR(16383)的单列表试试能不能建成功CREATE TABLE test_utf8mb4_ok ( col1 VARCHAR(16383) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这条语句执行成功说明utf8mb4下M16383还在65535的限制内。再试试M16384CREATE TABLE test_utf8mb4_bad ( col1 VARCHAR(16384) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;直接报错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...这个报错信息就是典型的“行大小过大”我把它的含义放到后文单独解读。3.3 gbk与utf8下的极限继续用同样方式验证gbkCREATE TABLE test_gbk_ok ( col1 VARCHAR(32766) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETgbk; -- 执行成功 CREATE TABLE test_gbk_bad ( col1 VARCHAR(32767) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETgbk; -- ERROR 1118 (42000): Row size too large...gbk下M32766可以通过M32767失败和公式计算的完全一致。再看utf8mb3即MySQL里俗称的“utf8”CREATE TABLE test_utf8_ok ( col1 VARCHAR(21844) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8; -- 执行成功 CREATE TABLE test_utf8_bad ( col1 VARCHAR(21845) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8; -- ERROR 1118 (42000): Row size too large...三组测试的结果和前面表格里的理论值完美对应。以后面试或者自己做表设计时可以直接背下这几个关键数utf8mb4最多16383utf8最多21844gbk最多32766。3.4 Row size too large报错解读Row size too large是很多人都遇到过的建表错误它本质上就是告诉你你这一行里所有可变长列的最大可能字节数加在一起超过了65535。这个报错最常见的触发场景是一张表里有好几个超长VARCHAR比如同时定义VARCHAR(1000)、VARCHAR(2000)、VARCHAR(3000)加在一起很容易冲破行上限。解决办法通常是把某几个字段改成长文本类型或者拆分表结构。另外要注意报错里的“This includes storage overhead”指的是MySQL在计算时不仅看数据字段本身还会把长度前缀、NULL标记位等元数据开销算进去。所以设计VARCHAR长度时一定要留出余量不建议真的把65535用满否则一加其他字段就崩。4. 那到底能存多少个数字要不要用VARCHAR存数字4.1 数字字符的字节占用以及VARCHAR(255)的实际容量回到大家最关心的问题数字字符在VARCHAR里占多少字节答案比汉字简单得多。数字字符‘0’到‘9’在latin1、gbk、utf8、utf8mb4这些常用字符集里都属于ASCII兼容区全部只占1字节。所以结论是在支持中文的字符集下VARCHAR(M)对汉字和数字的字符数上限一视同仁都是M个。比如VARCHAR(255)不管存汉字还是数字最多就是255个字符区别在于实际字节占用——255个汉字在utf8mb4下约765字节255个数字只有255字节。如果非要追求“最多能存多少个数字”答案取决于VARCHAR定义的长度。理论上限还是那一组数utf8mb4下是16383个数字gbk下是32766个数字utf8下是21844个数字latin1下最多65533个数字。当然实际表里很少会用到这种极端长度。4.2 为什么手机号、订单号要用VARCHAR存既然数字占1字节那直接用BIGINT、INT之类数值类型不是更省空间吗这里要分场景。开发中最典型的反例就是手机号我见过不少新人在设计表时把手机号设成BIGINT结果数据一导入就傻眼位数为0开头的手机号或者座机号全部丢掉了前导零。用VARCHAR存数字的核心场景有三个不需要做算术运算的编号类字段比如手机号、座机号、订单号、身份证号、银行卡号。可能包含前导零或者字母的字段比如订单号“A20240001”、身份证号里的“X”。长度超过数值类型表示范围的字段比如某些平台生成的超长流水号。反过来如果是年龄、数量、金额这种需要加减乘除、聚合运算的字段老老实实用INT、BIGINT、DECIMAL不要图省事全用VARCHAR。用错类型不仅浪费空间还会在查询时引入隐式类型转换拖慢性能。4.3 VARCHAR存数字的三个坑排序、隐式转换、空格先说排序。字符串排序和数值排序完全不一样VARCHAR列里存了“10”和“9”按字符串字典序排结果是“10”排在“9”前面因为“1”比“9”小。如果你拿订单号做范围排序很容易出现让人摸不着头脑的顺序。解决办法是尽量固定编号长度并补足前导零例如统一存成“000010”让字典序和数值序一致。第二个坑是隐式类型转换。当VARCHAR列和数值类型做比较时MySQL会尝试把字符串转成数字比如SELECT * FROM user WHERE phone 13800001111;这条语句会让phone列上建好的索引失效因为MySQL需要在每行上做类型转换再比较。写成下面这种字符串形式就能避免SELECT * FROM user WHERE phone 13800001111;第三个坑是尾部空格。在多数排序规则下VARCHAR比较时会忽略尾部空格也就是说“abc”和“abc ”会被认为是相等的。如果业务上对编号的精确性敏感宁可存之前先做一次trim也不要依赖数据库帮你区分。5. VARCHAR做主键ID能不能为空怎么用才不踩坑5.1 主键非空是硬性约束NULL会被直接拒绝“varchar为主键id能不能为空”这个问题很多人刚接触数据库时都问过。先说结论主键列不能为NULL这是MySQL的硬性约束和字段类型没关系。即使你建表时没写NOT NULLMySQL也会自动把主键列改成NOT NULL。例如执行CREATE TABLE t_user ( id VARCHAR(32) PRIMARY KEY, name VARCHAR(50) );再用SHOW CREATE TABLE查看表结构你会看到id列被隐式加上了NOT NULLSHOW CREATE TABLE t_user;输出里会是id varchar(32) NOT NULL, PRIMARY KEY (id)如果往主键列插NULL第一时间报错ERROR 1048 (23000): Column id cannot be null所以别再纠结“能不能为空”了约定上不允许数据库层面也强制不允许。5.2 “空字符串”和NULL是两码事注意主键不能为NULL但空字符串‘’是可以通过的。NULL代表“没有值”空字符串代表“这是一个值为空串的内容”两者在数据库语义上完全不同。INSERT INTO t_user (id, name) VALUES (, 张三);这条SQL能正常插入。但从业务角度看把空字符串当成主键值是相当危险的设计它最多只能存在一条而且会让后续数据关联、接口对接产生一堆脏数据。如果你需要一个“即使没有业务编号也能落库”的机制正确做法是增加一个自增整数主键或者雪花ID主键而不是给业务主键塞空字符串。5.3 VARCHAR主键的代价InnoDB索引结构引发的连锁反应聊完能不能为空再说说VARCHAR主键值不值得用。这里要先理解InnoDB的聚簇索引结构表数据本身按主键排序存储二级索引的叶子节点会额外携带一份主键值用来回表查询主键对应的行数据。这就意味着主键字段越大所有二级索引的存储开销就越大。如果你的主键是VARCHAR(64)的随机UUID字符串在utf8mb4下这个字段最多占256字节每个二级索引行都要附带完整的256字节主键值数据量一旦上千万光索引膨胀的空间就很可观。更麻烦的是随机字符串主键带来的写入性能问题。UUID没有顺序性新插入的行会落在聚簇索引的随机位置InnoDB为了维护B树平衡不得不频繁做页分裂和页合并产生大量随机写入和碎片插入性能明显比自增整数主键差。5.4 如果必须用VARCHAR做主键这样设计更稳妥虽然VARCHAR主键有代价但业务上有时确实避不开比如对接外部系统的订单号或者分布式环境下不方便用自增ID。这时我建议尽量做到下面几点控制主键长度。VARCHAR(32)够用就不要写成VARCHAR(64)能用32位UUID字符串就别上36位带横杠的完整版本。表字符集能选utf8mb3就尽量不选utf8mb4少了那1字节的字符最大宽度对长度和索引都有帮助。确保业务会传唯一值不要指望空字符串兜底ORM层也要加校验。如果表中还有其他索引要特别警惕索引键长度限制别让主键连带普通索引一起超出范围。如果你有自增能力我个人的倾向是另外加一个BIGINT自增列或雪花ID列做物理主键业务编号用UNIQUE KEY约束唯一性。这样业务上仍然可以按编号查数据索引结构也更健康。6. 避坑指南这些年我见过的高频VARCHAR事故6.1 建表失败Row size too large的排查思路遇到Row size too large时第一件事不是删表重来而是先列出所有列按“每列最大字符数×字符集单字符最大字节数长度前缀”估算每列最大字节加总后和65535对比。排查时重点看三方面一是有没有好几个超大的VARCHAR同时出现二是有没有列设置成允许NULL因为每8个可空列还要额外占1字节的NULL标记位三是字符集是否选得太“奢侈”如果业务只存中文和英文用utf8mb4存1万个字符需要4万字节用gbk只要2万字节。遇到超长文本就别硬用VARCHAR顶了改成TEXT、MEDIUMTEXT或LONGTEXT。它们的内容可以存在额外的页空间行内只保留指针或前缀能有效绕开65535的行大小限制。6.2 索引长度溢出767和3072的前世今生VARCHAR还有一个隐藏限制在索引上。早期MySQL InnoDB单列索引最大长度是767字节后来引入了innodb_large_prefix把上限提升到3072字节。MySQL 5.7之后默认就是3072字节8.0也延续这个配置。这个限制直接影响你能给多长的VARCHAR建索引。拿utf8mb4举例单字符最大4字节767除以4取整就是191所以老版本下很多人把VARCHAR字段的索引长度做成191。到了3072字节时代utf8mb4下单个索引列最好不要超过768个字符否则建索引同样会报错。如果你的VARCHAR确实很长又想提高查询速度可以用前缀索引只对前N个字符建索引ALTER TABLE t_doc ADD KEY idx_content(content(100));这样做的代价是查询时不能覆盖完整列值但很多场景下已经够用了。6.3 VARCHAR(255)与VARCHAR(256)的经典区别说到VARCHAR(255)和VARCHAR(256)很多人觉得不就差1嘛实际上在旧版MySQL里差别可大了。utf8字符集下VARCHAR(255)最大字节数255×3765字节加上2字节长度前缀是767字节恰好等于旧版InnoDB索引最大长度而VARCHAR(256)最大字节数是768字节直接超过767整列建索引就会失败。所以你在很多老项目里会看到大量VARCHAR(255)这个长度当年几乎是“万能长度”既能存255个字符又能安全建索引。现在虽然默认索引上限是3072字节VARCHAR(256)建索引没问题但255这个习惯已经深入一代开发者的骨髓。我的建议是新项目不用刻意回避255但也别无脑255按业务实际需要来字段值就那么几十个字符没必要给到255。6.4 VARCHAR和TEXT怎么选最后说说VARCHAR和TEXT的取舍。很多开发者在存一长段文本时纠结用哪个我的判断标准很简单文本长度相对可控比如几百到一两千字符优先VARCHAR长度不确定可能上万甚至更大的用TEXT系列。VARCHAR的优势在于可以走行内存储访问性能好可以有默认值可以建普通索引或前缀索引。TEXT虽然也能建前缀索引但做不了有默认值旧版本并且超长内容会产生额外IO读取性能相对弱一些。记住一个原则能用VARCHAR解决的就不轻易上TEXT但也不要为了“不用TEXT”而硬把VARCHAR定义得巨大。两者各有适用场景关键看你的数据长什么样。最后再分享几个实际项目中的体会VARCHAR这类基础类型平时不起眼可一旦设计错了后期改表的成本非常高。我印象最深的一次事故是给用户表主键用了36位带横杠的UUID字符串结果表数据一千万以后每次插入都在页分裂数据库CPU经常被打满后来花了两个大版本才把主键迁成BIGINT自增。从那以后我设计表的第一步就是先问自己三个问题这个字段最长会到多少字符表用什么字符集这一列要不要允许NULL把这三个问题想清楚VARCHAR的绝大多数坑都能提前避开。如果你还在纠结“VARCHAR到底能存多少个汉字、多少个数字”记住最核心的那句话就行VARCHAR(M)限制的是字符数M真正限制字节数的是行大小65535而字符集决定了每个字符落盘占用几个字节。具体到数值utf8mb4下极限是16383个字符utf8下是21844个gbk下是32766个。实际业务里留足余量别把数据库的硬限制当成设计目标就能少踩很多坑。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门