1. 先搞清楚定义:char和varchar到底在存什么
很多人在面试被问到char和varchar的区别,脱口而出“char是定长,varchar是变长”。这句话没错,但考试能过,落到生产环境就未必够用了。我在排查线上问题的时候,见过太多因为对这两个类型理解不透而埋下的坑:索引空间膨胀、隐式转换导致慢查询、唯一键莫名失效、主键排序错乱……这篇文章不打算只背结论,我想把底层存储逻辑、选型依据和实操避坑一次讲透。
先说最基本的定义。
char(M)是固定长度字符串,M表示字符数,范围为0到255。你声明char(10),无论存入“abc”还是“abcdef”,存储空间上都是按10个字符的容量来规划的。如果存入的内容不足10个字符,MySQL会补上空格帮你撑满长度,取出时再把尾部空格去掉。你可以把char想象成一个“固定规格的抽屉”,不管你放多少东西,抽屉本身占的物理位置是固定的。
varchar(M)是可变长度字符串,M同样表示最大字符数,范围为0到65535,但实际能存多少由行大小和字符集等因素决定。你声明varchar(50),存入“abc”时就在行里放3个字符的内容,存入“abcdef”就放6个字符的内容,存多少占多少,不会补空格。它的“变长”体现在两个地方:一是数据本身按实际内容存储;二是行里需要额外记录这个字符串到底有多长,这就是我们常说的“长度字节”。
char和varchar能解决的是同一类问题——存字符串,但背后是完全不同的存储思路。理解差异的核心,不是背定义,而是要明白数据库在设计存储格式时到底在“空间”和“时间”之间做了什么权衡。
1.1 char的“定长”到底怎么理解
我用建表语法来演示会更直观。
sql复制CREATE TABLE t_char_test (
code char(10),
name varchar(10)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO t_char_test VALUES ('abc', 'abc');
对于code字段,char(10),存入“abc”,存储结构仍然按10个字符处理。InnoDB在物理记录里不会真的把9个空格全部写进数据页,而是会做一定的处理,但从语义和长度计算逻辑上,它始终按10个字符来对待。这里的“按10个字符处理”会影响三个关键环节:比较长度、索引长度、排序时的长度。
char的真正优势在于:当你存储的内容长度相对稳定时,页内记录大小均匀,InnoDB扫描和计算偏移量会非常快。如果你把性别、状态码、MD5、交易流水号这类长度固定的字段设计成char,几乎不会浪费空间,还能换来更好的性能。但如果不看业务场景,把地址、备注这种长度差异极大的字段也设计成char(255),那每一行都会白白占用大量存储空间,数据量一上来,IO开销会非常难看。
1.2 varchar的“变长”究竟变在哪
varchar额外的长度字节是它和char最大的结构性区别。MySQL需要知道这一行里varchar字段实际占了多少字节,才能正确解析下一列的位置,所以必须在变长字段前记录长度。具体规则是:如果这个字段最大可能的字节数不超过255字节,用1个字节存长度;如果超过255字节,则用2个字节存长度。
这个规则在很多资料里被一笔带过,但实际操作中非常关键。举例来说:varchar(100)在utf8mb4字符集下,最大占用100乘以4等于400字节,超过了255,所以长度字节用2个字节;varchar(10)在utf8mb4下最大占用40字节,1个长度字节就够。存储“abc”时,utf8mb4下3个字符占3字节,加上1字节长度,总共4字节。
这里有个非常容易忽略的细节:varchar的长度字节数量由“最大可能占用字节数”决定,而不是由“实际存入内容的字节数”决定。一个varchar(255)和varchar(256),在utf8mb4字符集下长度字节数量就不同,前者最大字节数为1020,早就超过255,用2字节;后者也是2字节,但如果字符集是gbk,varchar(100)最多200字节,就只需要1个长度字节。所以同样是“存了3个字符”,不同字符集、不同长度声明,物理存储大小可能完全不一样。
1.3 一个冷知识:varchar的上限是怎么算出来的
varchar的M上限是65535,但这不代表你真的能创建成功一个varchar(65535)。限制来自两个方面:一是MySQL行最大大小为65535字节(不包括TEXT/BLOB等外置存储的类型),二是字符集编码会影响最大字符数。
在utf8mb4字符集下,1个字符最多占4字节。你创建varchar(20000),光这一列最大就能占80000字节,直接超出行大小限制,建表报错。所以要估算一个varchar字段最大能设置成多少,公式是:
text复制(65535 - 其他字段占用字节 - 变长字段长度字节 - NULL标志位等开销) / 字符集单字符最大字节数
举个实际例子,假设一张表只有一列,字符集utf8mb4,允许为NULL:
sql复制CREATE TABLE t_varchar_max (
content varchar(16383)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
16383乘以4等于65532,加上2字节长度,总共65534,再加上NULL标志位的开销,可以建成功。你再试试varchar(16384),16384乘以4等于65536,直接超了,MySQL会给出“Row size too large”的报错。
这个知识点在工作里用得不多,但定位问题很有用。偶尔会碰到有人问“为什么我varchar设置那么大报错”,本质就是没搞清楚行大小限制和字符集编码的关系。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 存储机制对比:从物理记录看两者差异
理解了定义之后,我建议你再往前迈一步:站在InnoDB的物理存储角度,看char和varchar在数据页里是怎么记录的。这一步能解释很多性能类和问题排查类的困惑。
InnoDB把数据存放在一个个16KB的数据页中。一张表的每一行,都被编码成一条记录放在数据页里。记录格式非常讲究,因为MySQL要解析出每一列的值,就必须知道每列从哪里开始、到哪里结束。对于char列,由于长度固定,解析器可以“按坐标直接定位”;对于varchar列,则必须先去读取前面记录的长度字节,再按长度取数据。
2.1 行格式中的可变字段头和长度字节
InnoDB目前的默认行格式是dynamic。在dynamic行格式下,每一条记录都有一个“变长字段长度列表”,专门用来记录该行中所有变长字段(varchar、varbinary、TEXT、BLOB等)的实际字节长度。注意,这个列表是“逆序记录”的——先记录最后一个变长字段的长度,再往前倒着记录。这么设计是为了方便解析和偏移量计算。
理论上char列长度固定,不需要出现在变长字段长度列表里。但有个例外:如果表的字符集是gbk或utf8mb4这种变长字符集,char列也可能被“当作变长字段”处理。举个例子,char(10)在utf8mb4下,如果存了2个字符,物理上可能只占2个字符对应的字节数,而不是10个字符的空间。在这种情况下,char列的长度也会被记录在变长字段长度列表里。
这个细节导致了一个很多人不知道的结论:在gbk、utf8mb4等多字节字符集下,char的实际存储优势没有想象中那么大,它更像是一个“语义上定长、物理上可能变长”的类型。真正的定长效果,只有在latin1这样的单字节字符集下才能完全体现。
2.2 字符集对长度的影响:不是字符数,是字节数
对于char和varchar,M单位都是“字符”,但存储占用和索引长度计算,全部基于“字节”。同一张表,哪怕字段长度声明一样,只要字符集不同,物理表现就完全不同。
常用字符集的单字符最大字节数:
| 字符集 | 一个字符最大字节数 | 举例 |
|---|---|---|
| latin1 | 1字节 | 适合纯英文/数字 |
| gbk | 2字节 | 中文占2字节 |
| utf8mb3 | 3字节 | 老版本中文占3字节 |
| utf8mb4 | 4字节 | 支持emoji等扩展字符,中文占3-4字节 |
我实际建表时,新项目基本默认utf8mb4,因为要兼容emoji和更多Unicode字符。但代价是同样的M,索引能覆盖的“字符数”变少了。比如一个普通索引,InnoDB默认单列索引最大767字节(使用BYTE的旧限制),在utf8mb4下,767除以4约等于191,所以varchar(255)作为索引前缀时会超过限制,实际只能取前191个字符做索引。虽然MySQL 8.0以后有动态变长页大小等新特性,这个“字符数和字节数”的关系依然是排查问题的核心。
我见过一个真实案例:业务表在utf8mb4下给一个varchar(255)字段建立了普通索引,结果索引列前缀自动被截断到191字符,导致查询时部分SQL走不了索引,开发排查很久没找到原因。后来把字段改成varchar(100),索引长度变为100乘以4等于400字节,在限制内,索引才真正生效。
2.3 大字段的溢出存储:varchar太大会被“挪出去”
InnoDB的dynamic行格式有一个重要行为:当一行的总长度接近或超过数据页的可用空间时,变长字段会采用“溢出存储”,把部分数据放到溢出页(off-page)中,而原记录里只保留一个20字节的指针。这个机制在TEXT/BLOB类型上最常见,但varchar也可以触发,只要长度足够大。
溢出存储带来的影响是:查询如果只读取行内数据,速度很快;一旦需要读取这个溢出字段的完整内容,就要额外访问另一个数据页,IO次数增加,性能下降。这个“大字段”问题不只是varchar和char的区别,更影响表结构设计。我的建议是:一篇正文、一大段JSON、一个完整HTML源码,都不该直接用varchar硬扛,应该考虑TEXT/MEDIUMTEXT或把数据拆到副表。
这里顺手说明白:char因为最大只有255字符,不会触发溢出页。varchar设置超过一定长度后,实际存储几乎等同于TEXT的机制,这点在评估表结构时要有预期。
3. 尾部空格处理:最容易被忽略的坑
如果说存储机制是理解char和varchar的地基,那尾部空格处理就是决定实战成败的分水岭。这个点非常小,却特别容易踩坑。我写过不少SQL调优和故障排查的文章,几乎每隔一段时间就能遇到一个和尾部空格有关的问题。
先说结论,然后逐条展开:
- char列在存储时,如果长度不足,会补空格。查询返回时,MySQL会把尾部的空格去掉。也就是说,你在char(10)里存入“abc”,取出时得到的是“abc”,而不是“abc ”。
- varchar列存储时不会补空格,也不会在查询时主动去掉尾部空格。你存入“abc ”,取出时依然带着两个空格。
这里已经出现第一个容易混淆的地方:varchar存入时保留尾部空格,那比较时怎么处理?答案和MySQL的排序规则有关。默认的collation是pad space,也就是比较时忽略字符串末尾的空格。所以“abc”和“abc ”在等值比较、排序时会被认为是相同的。MySQL 8.0开始支持NO PAD规则,这种情况下比较不再忽略尾部空格。
3.1 实际会踩到的坑:唯一索引误判重复
这是最典型的线上事故。假设用户表里有一个手机号或者邮箱字段,你设了唯一索引,字段类型是varchar(50)。用户第一次注册填的是“test@example.com”,第二次注册填的是“test@example.com ”——看起来带了两个空格,但数据库比较时按pad space规则,认为两个值完全相同,因此第二次插入直接触发唯一约束冲突。
但反过来,如果这个字段是char类型,存储时会自动去掉尾部空格再补空格,间接把数据“规范化”了,反而不会出现这个坑。所以不能说varchar一定比char好,关键看用途。
另一个场景是查询。用户在页面输入“abc”检索varchar列,库里有“abc ”的数据,用等值查询“where name = 'abc'”能查出来,因为比较规则会忽略尾部空格。但如果你用like,情况又变了:like一般不使用pad space规则去忽略尾部空格,它会精确匹配尾部空格。所以“where name like 'abc'”和“where name like 'abc '”结果可能完全不同。很多人在这个问题上踩过和踩坑中。
3.2 排序和去重也会受尾部空格影响
在默认的pad space规则下,排序时“abc”和“abc ”被视为相同,不会因为多一个空格而排在后面。这通常是好事,但如果你要在应用层做去重或比较,就必须知道数据库层已经帮你“忽略”了这些差异。
还有一个隐藏点:union、distinct、group by,都是基于“比较结果”判断是否相同的。也就是说,“abc”和“abc ”在group by时会被合并到同一组。如果你的业务逻辑依赖“空格也是内容”的精确匹配,就需要在表设计时想清楚:要么用varchar并且改用NO PAD排序规则,要么在应用层处理干净再入库。
3.3 我的建议:应用层统一清理,别指望数据库
我个人的习惯是:凡是字符串字段,在写入前先做trim处理,去掉首尾空格。这样既不依赖于char的自动去空格,也不担心varchar把空格保留下来,从源头消灭这一类问题。后端程序里可以统一处理:“用户输入 → trim → 规范化 → 入库”。如果情况特殊,业务上真的需要保留尾部空格,那一定不能用默认的pad space规则,必须在建表时明确指定NO PAD排序规则,并且提醒所有开发同学。
4. 性能与索引:别听人一句“char快”就无脑用char
“char比varchar快”这句话流传很广,但它只在特定场景下成立。从性能角度,把char和varchar的差异拆成几个维度来看,会更清楚。
4.1 CPU和存储开销的差异
char在单字节字符集(latin1)下是真正的定长,读取第N行、解析字段时可以直接用偏移量计算,不需要读长度字节,CPU开销小,记录长度也稳定。这在旧版本MySQL的MyISAM引擎时代,性能优势更明显。但到了InnoDB时代,默认行格式有额外的记录头、变长字段长度列表等结构,char与varchar在CPU层面的差距被明显缩小。特别是多字节字符集下,char也可能按变长方式存储,性能优势进一步减弱。
varchar的额外开销只有一个长度字节或两个长度字节,加上解析时要读取并计算长度。这个开销对单行来说微乎其微,但表数据量大、查询频繁时,会积累成可见的性能差异。不过这个差异,通常比“索引失效”或“行溢出”带来的性能损耗小得多。
4.2 索引存储和空间效率
索引对空间非常敏感。InnoDB普通索引默认限制是767字节(旧版),在utf8mb4下对应约191个字符。如果你用一个varchar(255)字段做索引,实际只有前191个字符能进索引,后面的字符不会参与索引定位。这会导致两个问题:一是索引无法覆盖所有可能值,区分度下降;二是回表次数增加。解决方式是使用前缀索引或缩短字段长度。
从索引存储空间来看,一个varchar字段,值长的行占用索引空间多,值短的行占用少;而char则是固定占用。如果某字段的内容长度差异巨大,并且要建立索引,varchar更有优势,因为短数据不会浪费空间。如果内容长度本身就固定,比如身份证号、手机号、MD5摘要,char反而更合适,既不会有额外长度字节(在单字节字符集下),也不会因为长度不齐导致索引页碎片化。
4.3 排序和join的性能
在order by和join场景里,varchar字段长度影响排序缓冲区和临时表大小。假设你设置varchar(1000),但是实际每行只存了几十个字符,排序时MySQL使用的sort buffer和可能产生的临时表,依然按声明的最大长度预分配空间。大量行排序时,varchar声明过长会导致临时表膨胀,甚至触发磁盘临时表,性能急剧下降。
所以我在设计表时有两条硬性习惯:
- varchar长度绝不“拍脑袋”定义,要按业务真实最大值加一点余量。比如手机号最长15位,就定义varchar(20),不要定义varchar(255)。
- 参与排序、分组、join的字段,尽量控制长度。长度越短,排序缓冲和临时表压力越小。
4.4 一个翻车案例:性别字段用了char(10)
我之前接手过一个项目,某个表的gender字段被设计成char(10),里面存“男”或“女”。按理说2个字符足够,char(10)在utf8mb4下最多占40字节,但这个表有2000万行,光这个字段就白白占了大量空间。这个字段还建了索引,索引空间随之膨胀,查询性能明显变差。后来改成tinyint或char(1)枚举,索引从约100MB缩到约10MB,排序和过滤速度提升非常明显。
这类案例的核心不是char本身不好,而是长度设置和业务不匹配。char适合短且长度稳定的内容,你非拿它存长文本,当然会出问题。
5. 选型建议:这几种场景我推荐char / varchar
说了这么多原理,落到实操上,到底怎么选?我把常见业务场景整理成了一套相对稳妥的选择逻辑,给读者一个可以直接套用的参考。
5.1 适合用char的场景
固定长度的业务编码和状态码
订单号、流水号、状态码这类长度固定的内容,非常适合char。比如物流单号固定12位,用char(12)是最优解。如果业务编码长度不完全固定,但基本都固定为某几个长度,依然建议先统一格式化成固定长度。
散列值
MD5摘要固定32个十六进制字符,SHA1固定40个字符,这类值长度绝对恒定,用char是标准做法。就算将来算法升级到SHA256,长度变成64个字符,改字段长度也很简单。
枚举值 / 建议值
性别、等级、类型标识等短枚举,如果不打算用tinyint,用char(1)或char(2)就足够。注意不要用char(10)或更长的char去存枚举,纯属浪费。
5.2 适合用varchar的场景
用户输入型内容
用户名、昵称、邮箱、地址、备注、文章标题,这类内容的长度天然可变,必须用varchar。核心诉求是“不浪费空间,同时保证足够的容量”。
可能超过255字符的字符串
char最大255字符,一旦超过必须用varchar或TEXT。varchar(500)、varchar(1000)在功能上可以当短文本用,但要注意长度对排序和索引的影响。
长度会动态变化的业务字段
比如URL、路径、JSON配置片段,虽然一般情况下不算长,但存在增长可能,varchar比char更稳妥。
5.3 一张表直接说结论
| 场景 | 建议类型 | 说明 |
|---|---|---|
| 性别、是否、状态等短枚举 | char(1)或tinyint | 空间小,性能好 |
| MD5、SHA、手机号、证件号等固定长度 | 对应长度的char | 长度固定,避免长度字节开销 |
| 用户名、昵称、地址、备注等变长内容 | varchar(适当长度) | 不要过长 |
| 文章正文、长JSON、日志内容 | text / mediumtext | 避免行溢出和行大小超限 |
| 排序频繁的短字符串 | 尽量缩短字段长度 | 降低临时表和排序缓冲压力 |
| 唯一约束字段(邮箱、手机号) | varchar + 应用层trim | 防止空格导致误判重复 |
这个表不是银弹,但覆盖了绝大多数情况。核心原则就一句话:长度稳定用char,长度可变用varchar,长度范围大且不参与排序索引的,直接用text。
6. 高频问题排查与面试考点
最后这部分,整理几个我实际工作里经常遇到的问题,以及面试时容易被追问的点。这些问题如果只看官方文档,容易记混;结合场景来理解,会清楚很多。
6.1 varchar能为主键吗?主键能不能为空?
varchar完全可以作为主键,但要注意两个问题:一是InnoDB的聚簇索引会按照主键排序,如果主键是长度较长的varchar,会导致二级索引体积变大,写入时也可能因为页分裂产生更多随机IO;二是主键本身不允许为NULL,但如果你问的是“唯一索引列能不能为空”,答案是可以,而且MySQL允许在唯一索引列上存在多个NULL值,因为NULL被视为“未知”,不是“相同值”。
在实际生产里,我建议优先使用自增整数或雪花ID作为主键,把varchar字段放到二级索引里作为唯一键使用。如果业务上非得用varchar当主键,尽量选择长度短且有序的值,比如“业务编码+日期”这种前缀相对有规律的字符串。
6.2 字符串与数字比较的隐式转换
虽然不算char和varchar的直接对比,但在使用字符串字段时特别容易踩坑。MySQL在比较字符串和数字时,会把字符串转换成数字比较。如果varchar字段存的是“123abc”这类内容,转换结果可能是123,也可能直接是0,很容易导致查询结果和数据预期不一致,还会引起索引失效。
我见过最典型的例子:某张表的id字段是varchar类型,存了带有前缀的编码,业务上偶尔用“where id = 123”查询,结果MySQL把每一行的id都转成数字再比较,不仅结果错误,而且走不了索引,全表扫描。解决办法很简单:查询时把数字写成字符串,比如“where id = '123'”,或者干脆把字段改成数值类型。
6.3 常见故障速查表
| 现象 | 可能原因 | 建议处理 |
|---|---|---|
| 唯一索引插入报重复,但数据看着不一样 | 尾部空格被忽略 | 入库前trim,或者改用NO PAD排序规则 |
| varchar(255)建索引报长度超限 | utf8mb4下字符数乘以4字节超过索引限制 | 缩短字段或改用前缀索引 |
| 大字段查询很慢 | 行溢出,字段被存放在溢出页 | 拆分字段,改用text或存放于副表 |
| 排序慢,临时表很大 | varchar声明过长 | 缩短长度声明,优化排序字段 |
| 数字和字符串比较结果异常 | 隐式类型转换 | 统一类型,避免混合比较 |
6.4 面试时怎么回答char和varchar比较的问题
如果面试官问“char和varchar的区别”,我建议不要只背那一句定长变长,而是按“存储机制 → 尾部空格 → 性能 → 实战选择”的逻辑来回答。比如先说明char是定长,varchar是变长且额外有长度字节;再说明char在检索时会去掉尾部空格,varchar保留,但默认排序规则比较时会忽略尾部空格;然后提到多字节字符集下char可能变成类似varchar的存储方式;最后补充一句:具体怎么选还要看字符集和业务长度特征。
能说到这一步,面试官基本能判断你是真用过MySQL的,而不是背了八股文。
我个人在实际操作中的一个体会是:很多所谓“char比varchar快”的说法,在业务体量没到一定规模时几乎感觉不出来。真正影响线上体验的,往往不是char和varchar那点存储差异,而是字段长度乱定义、字符集没想清楚、隐式转换、索引失效这些更底层的问题。所以与其纠结一个字段到底用char还是varchar,不如先把表结构设计规范和数据类型选择的完整思路建立起来。把本节提到的场景过一遍,再结合自己业务的真实数据特征,大概率就不会选错。
最后再分享一个小技巧:如果你分不清该用char还是varchar,建表前先问自己三个问题——这个字段长度是否固定?这个字段会不会参与排序和索引?这个字段的最大长度是多少?三个问题想清楚,答案基本就出来了。
