ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MySQL报错ERROR 1406 Data too long for column ‘name‘排查与修复指南

MySQL报错ERROR 1406 Data too long for column ‘name‘排查与修复指南 我上周就在一个线上项目里遇到这个报错正在跑一个数据导入任务往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 姓名/简介 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;然后执行一条插入语句往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是根据表定义指定的受行大小限制变长存储在行内超过一定长度放到溢出页姓名、手机号、标题、短备注TINYTEXT255字节变长存储在行外超短说明TEXT65535字节约64KB变长存储在行外文章正文、JSON、长描述MEDIUMTEXT16777215字节约16MB变长存储在行外日志、大量文本LONGTEXT4294967295字节约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 ;然后再执行那条超长INSERTINSERT 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-autoupdate下次重启可能又会把字段缩回去或者在校验阶段直接把超长数据拦截掉那样数据库层永远收不到数据。Python的Django里也一样class UserInfo(models.Model): intro models.TextField(verbose_name个人简介)models.CharField(max_length50)要同步改成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的错误信息虽然看着吓人但只要你理解它背后每个词的重量排查起来其实就那么几件事看懂报错、查字段定义、搞清楚长度单位、评估改动影响、动工前备份一气呵成下次再遇到就是轻车熟路。
返回列表