我上周就在一个线上项目里遇到这个报错,正在跑一个数据导入任务,往user_info表插一条用户自我介绍,结果一句几百字的文本刚塞进去,客户端直接甩出来一串红字:
ERROR 1406 (22001): Data truncation: Data too long for column 'name' at row 1
当时第一反应是"name不是姓名吗,怎么会太长",后来查了一下表结构才发现,这个name字段在设计时被定义成了VARCHAR(50),而业务却拿它存个人简介。这个报错本质特别简单,就是"你给的数据长度,超过了字段定义的上限",但真要把这个坑填干净,背后牵扯出来的东西不少:字段类型怎么选、字符集对长度的影响、sql_mode为什么会导致行为不同、ALTER TABLE会不会锁表、应用层要不要跟着改,等等。这篇文章就从这条报错开始,把整条排查链路完整走一遍。
1. 报错自述:Data truncation到底在说什么
1.1 先看懂错误信息里的三个关键点
MySQL的这个报错,措辞其实非常直白,拆开来看就三块信息:
Data truncation:数据被截断了,意思是写入的内容在某个环节被"砍掉"了一部分。Data too long for column 'name':明确告诉你是哪一列出问题,这里是name列。at row 1:出错的是这条INSERT语句里的第1行数据(如果是批量插入,行号会跟着递增)。
我之前见过不少同学一看到Data truncation就慌,以为是编码问题、乱码问题或者客户端连接问题,其实大部分情况下就是字段长度不够。先把错误信息里那三块信息定位清楚,再去查表结构,方向就不会跑偏。
1.2 复现现场:一封超长文本把插入SQL拦在门外
为了把问题说清楚,我复现一下当时的场景。表结构很简单:
CREATE TABLE user_info ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '姓名/简介' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;然后执行一条插入语句,往name里塞一段几百字的自我介绍:
INSERT INTO user_info (name) VALUES ('你好,我是一名拥有超过十年经验的全栈工程师,平时主要的工作内容包括……(后面还有几百字)');执行完直接报错:
ERROR 1406 (22001): Data too long for column 'name' at row 1这里的name是VARCHAR(50),也就是最多存50个字符。而我插入的内容已经远超50个字符,MySQL自然不肯放行。
有同学会问:为什么MySQL不自动把多余的部分截断掉,而是直接报错?这里就要牵扯到sql_mode了,后面第4节会单独展开。
1.3 最基础的排查:表结构现状确认
遇到这种报错,不管报错信息看着多复杂,第一步永远是确认字段的真实定义。我用两个命令交叉验证:
SHOW FULL COLUMNS FROM user_info;输出里会看到name字段的类型、是否允许NULL、默认值、字符集等。也可以用DESC user_info快速看个大概,但SHOW FULL COLUMNS能看到字段的Character Set和Collation,这对本问题的判断更关键:
+-------+-------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------+-------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(50) | NO | | NULL | | +-------+-------------+------+-----+---------+----------------+到了这一步,问题的直接原因就浮出水面了:name列被定义成VARCHAR(50),而插入的文本长度远超50个字符。下一步要解决的,就是为什么VARCHAR(50)这个看起来很常规的定义,会装不下几百字的文本,以及正确的修法是什么。
2. 字段长度的底层逻辑:VARCHAR为什么装不下
2.1 VARCHAR和TEXT到底差在哪
很多新手对MySQL字段类型的理解停留在"VARCHAR可以存字符串,TEXT也可以存字符串,好像差不多",其实两者差别非常大。我把最常用的字符串类型整理了一张表:
| 类型 | 最大长度(字符) | 存储方式 | 适合场景 |
|---|---|---|---|
| VARCHAR(n) | n是根据表定义指定的,受行大小限制 | 变长,存储在行内,超过一定长度放到溢出页 | 姓名、手机号、标题、短备注 |
| TINYTEXT | 255字节 | 变长,存储在行外 | 超短说明 |
| TEXT | 65535字节(约64KB) | 变长,存储在行外 | 文章正文、JSON、长描述 |
| MEDIUMTEXT | 16777215字节(约16MB) | 变长,存储在行外 | 日志、大量文本 |
| LONGTEXT | 4294967295字节(约4GB) | 变长,存储在行外 | 大数据文本 |
VARCHAR和TEXT最核心的区别有两个:
一是VARCHAR支持指定最大字符数,TEXT只按最大字节数分档,不能像VARCHAR那样定义VARCHAR(500)这种精确字符数。
二是VARCHAR的容量会被InnoDB表的行大小限制约束,而TEXT/BLOB在InnoDB中默认采用off-page存储,实际存储的数据不会完全占用行内空间。这正是"超长文本优先考虑TEXT"的根本原因。
2.2 字符、字节、字符集三者之间的关系
把VARCHAR(50)里的50理解成"50个字母",这是最常见的一个误区。MySQL 4.1之后,VARCHAR和CHAR定义里的长度单位是"字符数",不是"字节数"。一个字符存进去,实际占用的字节数取决于表的字符集。
以最常用的utf8mb4为例:
- 英文字母、数字:1个字符 = 1字节
- 中文汉字(基本平面):1个字符 = 3字节
- Emoji等扩展字符:1个字符 = 4字节
也就是说,VARCHAR(50)在utf8mb4下,最多能存50个汉字、或者50个Emoji,换算成字节分别是最多150字节或200字节。
那如果直接往里塞500个字符,无论怎么算都超了,报Data too long是必然的。
2.3 一个反直觉的算术题:255到底能存多少汉字
这里再补充一个很多人在生产环境踩过的坑,就是"VARCHAR最大长度到底能设成多少"。
InnoDB表有一条硬性限制:一行的总长度不能超过65535字节(这个限制不包括TEXT/BLOB类型,它们的数据会溢出存储,但行内仍保留一部分指针)。在utf8mb4字符集下,如果某个表只有一个VARCHAR字段,理论上可以定义成VARCHAR(16383),因为16383个字符乘以4字节约等于65532字节,再加上变长字段记录长度的2字节,刚好卡在限制内。一旦超过,MySQL会直接提示Row size too large。
所以当业务确实需要存很长文本时,你可能会遇到一个尴尬场面:想把name从VARCHAR(50)改成VARCHAR(5000),结果MySQL告诉你Row size too large。原因就是你一个字段就把整行的字节预算吃光了,表里还有其他字段呢。这种情况下,正确的选择就是TEXT系列。
2.4 最大行长度的隐形天花板
除了行大小限制,索引长度也是另一个隐形天花板。VARCHAR(50)上面的普通索引和唯一索引,索引键长度有限制:早期COMPACT行格式下InnoDB索引键最长767字节,新版DYNAMIC/COMPRESSED行格式下最多3072字节。在utf8mb4下,3072字节除以4等于768个字符,所以一个VARCHAR(768)以上的字段做全文索引,基本都会触顶。
这跟我们的报错有什么关系?如果你打算把name改成一个超长VARCHAR并且上面有索引,就要考虑索引键长度是否超标。如果改的是TEXT,那更麻烦,TEXT建索引必须指定前缀长度,而且没法做唯一约束。因此,字段类型的选择不只是一个长度问题,还会牵动索引设计。
3. 修表完整链路:从备份、ALTER TABLE到验证
3.1 预留备份:动手前的第一件事
排查清楚原因之后,动手改表结构之前,先做备份。这话我说过很多遍,但在生产环境真正执行的人真不多。尤其是ALTER TABLE这种DDL,一旦中途出问题,想回滚没那么容易。
如果只是想快速留一个可恢复的副本,可以用逻辑备份单表:
mysqldump -u root -p --single-transaction --quick --skip-lock-tables test_db user_info > /backup/user_info_$(date +%F).sql如果是本地开发环境或者表很小,也可以快速建一张备份表,修改后想对比或者反悔都有退路:
CREATE TABLE user_info_bak LIKE user_info; INSERT INTO user_info_bak SELECT * FROM user_info;第一条命令复制表结构,第二条命令复制数据。这种方式临时用可以,但别把它当成完整备份方案,因为外键、触发器这些不会一起带过来。
3.2 新类型选型:直接上TEXT还是继续用VARCHAR
备份做完后,回到核心选择题:name到底改成什么类型?
我的判断标准很简单:如果业务语义上是"姓名、手机号、短标签"这类有明确长度上限的字段,就用VARCHAR并留足余量;如果语义上是"描述、备注、正文"这类长度不可控的字段,优先TEXT系列。
比如我们当时的name实际存的是个人简介,几百字很正常,未来可能到几千字。这种情况下我不建议继续抠VARCHAR长度,直接改成TEXT最省心,而且不用担心行大小限制。
ALTER TABLE user_info MODIFY COLUMN name TEXT NOT NULL COMMENT '姓名/简介';如果业务确实要求保留VARCHAR,只是当前长度不够,比如从50扩到500,那可以:
ALTER TABLE user_info MODIFY COLUMN name VARCHAR(500) NOT NULL COMMENT '姓名/简介';但改之前建议先确认表的字符集和整行长度,否则可能被Row size too large拦回来。
3.3 ALTER TABLE实操:DDL细节与锁表风险
在真正执行DDL前,先看清当前表的数据量:
SELECT COUNT(*) FROM user_info;如果表里数据量不大,直接ALTER TABLE问题不大。如果这是一个几千万行的大表,就得掂量掂量锁表风险了。
这里有个非常经典的细节:VARCHAR(255)改成VARCHAR(256),看着只多了1个字符,但MySQL底层记录变长字段长度的字节数要从1字节变成2字节,因此会触发全表重建,也就是COPY算法。从VARCHAR(50)改成VARCHAR(500)也一样,长度前缀从1字节变成2字节,需要重建表。而改成TEXT,本质上是行格式调整,通常也会走全表复制。
在MySQL 5.6及之后,ALTER TABLE支持在线DDL,但能不能在线也看具体操作。对于这种需要重建表的MODIFY,虽然5.7/8.0下一般不会把原表锁死,但执行期间I/O压力、空间占用都会明显上升。我的建议是:小表随便改,大表在低峰期改,并且先用EXPLAIN或者实际备份验证过再说。
如果你用的MySQL版本支持INSTANT算法,部分VARCHAR长度变化可以秒级完成,但限制条件比较苛刻,比如长度变化不能导致底层长度字节数变化、不能改动表其他字段等。具体能不能走INSTANT,取决于版本和具体DDL,生产环境别盲目乐观,先看看执行计划对应的算法。
对于超大表的字段类型变更,如果担心锁表时间太长,业界常用pt-online-schema-change这类工具来平滑变更。它的原理是通过触发器把增量同步到新表,最后切换表名。这个工具我建议在真正因为ALTER TABLE锁表造成过事故之后再去研究部署,小项目暂时用不到。
3.4 索引与唯一约束的连带影响
修改字段类型之前,一定要先看看这个字段上有没有索引:
SHOW INDEX FROM user_info;如果name上建了普通索引,从VARCHAR(50)改成VARCHAR(500)一般问题不大,长度增加但还在索引键长度范围内就行。但改成TEXT就有讲究了——TEXT列不能直接建普通索引,必须指定前缀长度:
ALTER TABLE user_info ADD INDEX idx_name (name(191));这里的191是有原因的:utf8mb4下191个字符最多占764字节,加上一些开销后,在旧的行格式下能卡进767字节的索引键限制。新版本虽然支持3072字节,但用191做前缀索引依然是很稳妥的默认选择。
如果name上有UNIQUE唯一约束,情况就更麻烦了:TEXT无法直接作为唯一约束列,因为唯一性要求比较整个字段内容,而TEXT的索引只能做到前缀。如果业务上要求这个字段值不能重复,那就老老实实继续用VARCHAR,或者把name改成VARCHAR(500)并对业务场景做长度评估。实在不行,只能另建一个唯一标识字段来做约束。
3.5 修复后的真实测试
改完表结构后,不能光看DDL执行成功就完事,必须把之前报错的那条SQL重新执行一遍,确认能插进去,再查一遍数据完整性:
INSERT INTO user_info (name) VALUES ('你好,我是一名拥有超过十年经验的全栈工程师,平时主要的工作内容包括……(后面还有几百字)');执行成功之后:
SELECT id, CHAR_LENGTH(name) AS name_char_len, LENGTH(name) AS name_byte_len FROM user_info;CHAR_LENGTH是字符数,LENGTH是字节数。在utf8mb4下,几百个字符的文本返回的字节数大概率是字符数的3倍左右,这是正常现象。到这里,报错问题算是在数据库层彻底解决了。
4. 链路里的隐藏坑:sql_mode、ORM映射与静默截断
4.1 STRICT_TRANS_TABLES:报错模式的开关
为什么同样的INSERT,在你自己电脑的MySQL上不报错,到了公司测试库就报Data too long?这就牵出sql_mode了。
MySQL 5.7之后,默认模式里带了一个叫STRICT_TRANS_TABLES的设置。它在事务表上的作用简单说就是:写入超长数据、非法数值这类问题,直接报错,不让SQL执行成功。而在更老的非严格模式下,MySQL会偷偷把超长数据截断到字段允许的长度内,然后只给你一个Warning。
用刚才的例子对比一下就很清楚。先看当前模式:
SHOW VARIABLES LIKE 'sql_mode';输出大概长这样(不同版本略有差异):
ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION看到了STRICT_TRANS_TABLES,这就是报错模式的开关。
为了模拟非严格模式,我临时把当前会话的sql_mode清空:
SET SESSION sql_mode = '';然后再执行那条超长INSERT:
INSERT INTO user_info (name) VALUES ('这是一段非常长的文本……(超长)'); Query OK, 1 row affected, 1 warning (0.00 sec)MySQL不报错,但给了个Warning。如果你不看SHOW WARNINGS;,直接以为插入成功了,那后面取数据的时候看到的是一段被拦腰截断的内容:
SHOW WARNINGS; +---------+------+------------------------------------------------+ | Level | Code | Message | +---------+------+------------------------------------------------+ | Warning | 1265 | Data truncated for column 'name' at row 1 | +---------+------+------------------------------------------------+这就比报错危险多了。报错至少能让你第一时间发现问题,静默截断则是数据悄悄丢了一截,线上故障往往就是这么埋下的。所以我不建议为了"让插入不报错"去把sql_mode里的STRICT_TRANS_TABLES去掉,那是掩耳盗铃。
4.2 非严格模式下发生了什么:静默截断风险
接上面的例子,改成非严格模式后,虽然INSERT执行成功,但你查询时看不到完整内容:
SELECT id, name, CHAR_LENGTH(name) AS len FROM user_info;返回的name只有前面一小段,后面的内容全部丢失。如果这条数据是用户填写的联系方式、商品详情、订单备注,这种静默丢失的影响可能是灾难级的。
我见过一个真实案例:某系统导入客户留言,字段是VARCHAR(100),线上库关了严格模式,结果所有超过100字符的留言全部被截断,等到运营发现时,已经累计导入了上万条残缺数据。当时只能从原始文件重新清洗再导入,浪费了大量工时。
所以从全局视角看,STRICT_TRANS_TABLES报错反而是好事,它用一次可见的失败换来了数据完整性。
4.3 应用层和ORM的联动修改
数据库字段改完之后,应用层如果不跟着改,同样的报错还会在程序里再上演一次。
举个Java后端的例子,如果实体类里用了字段标注:
@Column(name = "name", length = 50) private String intro;数据库已经改成TEXT了,应用层这个length = 50如果参与了表结构自动更新(比如JPA的ddl-auto=update),下次重启可能又会把字段缩回去,或者在校验阶段直接把超长数据拦截掉,那样数据库层永远收不到数据。
Python的Django里也一样:
class UserInfo(models.Model): intro = models.TextField(verbose_name="个人简介")models.CharField(max_length=50)要同步改成models.TextField,否则ORM生成的SQL还是带着原长度语义。
应用层那一堆参数校验注解、正则表达式、前端输入框的maxlength也要一起梳理。不然就会出现"数据库能存了,但应用层根本不让你提交"的诡异现象。
5. 治标之外的治本:字段长度规划与批量巡检
5.1 建表阶段如何预估字段长度
说实话,大多数Data too long问题的根子都在表设计阶段。字段长度预留得太抠,后面需求一变就崩。
我在新建表时基本遵循一个原则:能用VARCHAR明确限制的短字段,长度按业务上限的1.5到2倍预留;长度不可控的内容字段,直接上TEXT/MEDIUMTEXT。
举几个参考值:
| 业务含义 | 推荐类型 | 预留逻辑 |
|---|---|---|
| 姓名 | VARCHAR(50) | 国内姓名最长一般不超过20个汉字 |
| 手机号/电话 | VARCHAR(30) | 考虑区号、分机号,留足余地 |
| 邮箱 | VARCHAR(100) | 邮箱最长254字符,但这个场景需要做业务校验,长度100足够日常 |
| 商品标题 | VARCHAR(200) | 电商标题动辄上百字,留200比较稳 |
| 个人简介/描述 | TEXT | 长度不可控,直接上TEXT |
| 文章正文 | MEDIUMTEXT | 可能上万字,TEXT不够再升级 |
这个表不是标准答案,但可以当做一个起点。关键是设计表结构时就要问一句:这个字段将来会存什么?最长能有多长?可不可控?
5.2 一条SQL找出潜在截断风险列
如果你的系统已经跑了很多年,表结构里混着一堆早期定义很抠的VARCHAR,想挨个排查,可以借助information_schema这个系统库。
比如找出某个业务库下所有字段类型为VARCHAR、最大字符数小于50的列:
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH AS max_chars, CHARACTER_OCTET_LENGTH AS max_bytes FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'test_db' AND DATA_TYPE = 'varchar' AND CHARACTER_MAXIMUM_LENGTH <= 50 ORDER BY TABLE_NAME, COLUMN_NAME;这条SQL能快速列出所有可能成为"定时炸弹"的窄字段。之后再对重点字段做数据侧检查,比如:
SELECT MAX(CHAR_LENGTH(name)) FROM user_info;如果name字段现有数据的最大长度已经非常接近VARCHAR(50)的限制,那基本可以断定这个字段迟早会炸,趁早扩容才是正路。
5.3 拦截在入口:应用层验证方案
数据库字段类型再怎么改,始终是被动防御。最稳妥的方式是在写入链路的前置环节就做长度校验,把超长数据拦在数据库之外。
后端校验接口里可以加一道"按字段最大长度"的断言。以Java的Hibernate Validator为例:
@Size(max = 500, message = "name字段长度不能超过500字符") private String name;前端输入框上可以加maxlength,但前端限制只能算用户体验优化,后端一定要再校验一遍,因为接口可能被绕过。
这样三层配合:前端给用户友好提示,后端做业务拦截,数据库严格模式做最后兜底。这个组合下来,Data too long这种问题基本就没有生存空间了。
我在实际维护系统时养成了几个习惯:建表时对不可控文本直接上TEXT,定期跑information_schema巡检窄字段,应用层所有入参强制走长度校验。这套组合拳打下来,这几年再没被Data truncation半夜拉起来过。MySQL的错误信息虽然看着吓人,但只要你理解它背后每个词的重量,排查起来其实就那么几件事:看懂报错、查字段定义、搞清楚长度单位、评估改动影响、动工前备份,一气呵成,下次再遇到就是轻车熟路。