update时io暴增主因是数据库必须读取整行(含未修改的大字段)而非字段本身大小;常见现象包括using temporary频繁、buffer_pool_wait_free上升、磁盘%util超90%、从库延迟飙升。

UPDATE TEXT/MAX 字段时为什么IO暴增
不是字段本身“大”,而是数据库在执行 UPDATE 时,必须把整行(含未修改的 TEXT、VARCHAR(MAX)、JSON 等大字段)从磁盘读入内存,再写回——哪怕你只改了一个 status 字段。
常见现象包括:Using temporary 频繁出现、innodb_buffer_pool_wait_free 上升、磁盘 %util 持续 90%+、从库延迟飙升。
- SQL Server 中
VARCHAR(MAX)存储在 LOB 页面,UPDATE触发 LOB 数据页读取 + 复制 + 重写,即使值未变 - MySQL 5.7+ 对含
TEXT的行做UPDATE时,默认走“原地更新”失败路径,转为 delete+insert,导致大字段重复落盘 - PostgreSQL 的 TOAST 表虽分离存储,但更新主表任意字段仍需访问 TOAST 指针页,且若触发
toast_tuple_update,会复制整个大对象
哪些 UPDATE 场景会隐式拖垮大字段
最容易被忽略的是“看起来没动大字段”的操作:
-
UPDATE t SET status = 1 WHERE id = 123;—— 若该行含remark TEXT,仍要加载整行 - 使用触发器或生成列依赖大字段:哪怕
SET列不相关,只要触发器里引用了OLD.remark,就强制读取 - 事务中先
SELECT ... FOR UPDATE再UPDATE:锁住整行,连带拉取大字段进 buffer pool - WHERE 条件用到函数,如
WHERE LENGTH(content) > 1000:必须读取content才能计算长度
绕过大字段 IO 的实用策略
核心原则:让数据库「别碰」大字段,除非真要改它。
- 拆表:把
TEXT字段移到独立关联表(如order_details),主表只留order_id和小字段;UPDATE主表时完全避开大字段 - 条件过滤前置:用
EXISTS替代JOIN大字段表,例如只查“有紧急备注的订单”,用EXISTS (SELECT 1 FROM order_remarks r WHERE r.order_id = o.id AND r.content LIKE '%紧急%') - 显式指定列更新:避免
UPDATE t SET col1 = ?, col2 = ?后面漏掉大字段,但更关键的是——确认你的 ORM 没自动生成全字段UPDATE - SQL Server 可加
WITH (NOLOCK)在子查询中跳过锁和大字段加载,但仅限读一致性要求低的场景
检查和验证是否真被大字段拖累
别猜,直接看执行计划和实际 IO 统计:
- MySQL:运行
EXPLAIN FORMAT=JSON,关注"rows_examined_per_scan"和"pages_read";再查SHOW ENGINE INNODB STATUS\G里的LOG和BUFFER POOL部分 - SQL Server:用
SET STATISTICS IO ON,对比lob logical reads和logical reads的比例;若前者远高于后者,基本锁定问题 - PostgreSQL:
EXPLAIN (ANALYZE, BUFFERS),重点看Shared Hit/Read和Toast Read行数
真正难处理的不是“怎么改快”,而是业务逻辑里那些看似无害的 SELECT *、ORM 全字段映射、以及开发时没人记得那个 content 字段其实是 TEXT。











