根本原因是innodb的off-page存储机制导致update必须读全量溢出页再写回,引发随机i/o、buffer pool污染和长锁持有;解决需物理分离大字段或应用层精准控制更新。

UPDATE语句一碰TEXT/BLOB就卡住,根本原因不是字段大
真正拖慢的是MySQL的off-page存储机制和“读全量再写全量”的强制行为。InnoDB对超过768字节的TEXT或BLOB值不存主记录页,而是挪到独立溢出页,主记录只留20字节指针。每次UPDATE只要涉及该字段,哪怕内容完全没变,MySQL也必须:先按指针把整个溢出页读进来,再原样或修改后写回去——这导致额外随机I/O、buffer pool污染、锁持有时间拉长。
- 常见错误现象:
UPDATE doc SET title = ?, content = ? WHERE id = 1中content根本没改,但执行耗时从几毫秒飙到秒级 - EXPLAIN看不出问题,因为
UPDATE不走执行计划;得看SHOW PROFILE FOR QUERY N里Handler_read_rnd_next是否异常高 - 事务里混写大字段和高频小字段(如
status),会把行锁卡死,其他更新全排队 - 批量
UPDATE ... WHERE id IN (1,2,3,...)带大字段,极易触发max_allowed_packet超限或OOM
为什么WHERE条件走索引也没用
索引能加速定位哪几行要更新,但解决不了更新动作本身带来的I/O开销。哪怕WHERE id = ?走了主键索引,只要SET子句包含content,就会触发溢出页加载。更隐蔽的是:如果表用COMPACT行格式,哪怕content只存了800字节,也已溢出;而DYNAMIC下超约8KB才溢出——但实际业务中,日志、HTML、JSON等文本动辄数MB,基本都落在溢出页。
- 别信“我加了
INDEX(content(100))就能快”,前缀索引对UPDATE无加速作用 -
SELECT LENGTH(content) FROM t WHERE id = 123结果若远大于768(COMPACT)或8000(DYNAMIC),基本可判定进了溢出页 -
SHOW CREATE TABLE t\G确认ROW_FORMAT,比查文档更可靠
怎么让UPDATE真正避开大字段I/O
核心思路是物理分离元数据与内容,或逻辑绕过无意义写入。MySQL不支持UPDATE SET x = IF(x != ?, ?, x)这类安全赋值,必须由应用层控制。
- 拆成两条语句:
UPDATE doc SET title = ?, updated_at = NOW() WHERE id = ?(不含content) + 单独UPDATE doc_body SET content = ? WHERE doc_id = ? - 应用层对比新旧值:仅当
content实际变更时,才发含该字段的UPDATE - 必须合并在一条SQL?用
COMPRESS()写入(注意字段类型至少为MEDIUMTEXT),客户端读取后自行解压;别在SELECT里调UNCOMPRESS(content),CPU白耗且无法复用连接池缓存 - 已压缩二进制(如JPG、ZIP)再套
COMPRESS()几乎无效,反而增加延迟
批量更新大字段时最容易踩的坑
批量不是“一次写更多”,而是“一次少写一点,但写得准”。盲目堆数量只会放大溢出页I/O的破坏力。
- 别用
UPDATE ... WHERE id IN (...)塞几百个ID——MySQL会构建隐式临时表,强制加载所有对应溢出页到内存 - 分批按主键范围切:
UPDATE t SET content = COMPRESS(?) WHERE id BETWEEN 10001 AND 11000,每批显式COMMIT - 绝对避免
UPDATE t SET size = LENGTH(content)这类计算型更新——它会把所有目标溢出页全拉进内存 - 非实时场景(如归档)改用异步:消息队列+Worker逐条处理,失败可重试,不阻塞主线程
TEXT字段,其实只需要和主记录保持关联性,而不是强求原子性落盘。外置对象存储+URL字段,往往比硬扛InnoDB溢出页更稳定、更便宜、更易扩展。










