select *会触发额外行锁和i/o开销,因mysql 5.7对大字段采用off-page存储,需读取溢出页并加锁,且全表扫描时锁住所有聚簇索引记录及对应溢出页。

为什么SELECT *会触发额外行锁和I/O开销
MySQL 5.7 对 BLOB、TEXT 及长度超 768 字节的 VARCHAR 采用行溢出(off-page)存储:真实数据被挪到独立溢出页,主记录只保留 20 字节指针。这意味着即使你只更新一个普通字段,只要语句是 SELECT * 或未显式排除大字段,InnoDB 就必须读取所有溢出页——每次都是随机 I/O,且每读一页都可能触发新锁(包括对溢出页的 X 锁或 S 锁)。
更关键的是,这种读取不是“只读”那么简单:如果事务隔离级别是可重复读(RR),且查询走的是聚簇索引扫描(比如没用上二级索引),InnoDB 会对**所有扫描到的聚簇索引记录**加记录锁 + 间隙锁。哪怕你只是想查 id = 123,若没索引支撑,仍会全表扫描并锁住全部主键值——而每个主键记录背后还连着溢出页,锁范围实质扩大。
UPDATE/DELETE时锁范围被隐式放大
当 WHERE 条件无法命中索引(如 WHERE content LIKE '%xxx%'),优化器被迫全表扫描,此时每条聚簇索引记录都会被加上记录锁;而每条记录又因含 BLOB 字段,其溢出页在首次访问时也会被加锁(至少是 S 锁)。结果就是:你以为只改 1 行,实际锁了 10 万行主记录 + 对应的 10 万次溢出页访问与潜在锁等待。
常见错误现象:
-
EXPLAIN显示type=ALL,但Rows_examined高得离谱(比如 50 万),慢日志里Rows_sent却只有 1 -
SHOW ENGINE INNODB STATUS中看到大量LOCK WAIT,且Trx_lock_structs数值远超预期 - 执行
UPDATE t SET status = 1 WHERE id = 123却长时间阻塞,查INNODB_TRX发现TRX_ROWS_LOCKED ≈ 表总行数
如何避免BLOB字段拖累锁行为
根本原则:不让大字段参与不必要的扫描和锁竞争。
- 查询务必显式列出所需字段,禁用
SELECT *;尤其避免在 JOIN 或子查询中把含BLOB的表放在驱动表位置 - 对高频查询字段建覆盖索引,确保
WHERE和SELECT所需列都能被索引满足,彻底绕过聚簇索引扫描 - 业务允许时,把
BLOB拆到独立表,主表只留file_path或oss_url;这样主表操作几乎不涉及溢出页 - 必须存库时,优先用
COMPRESS()压缩后再写入MEDIUMBLOB,并确保字段类型足够(压缩后体积可能略增) - 绝不要对
BLOB字段建索引——它不支持前缀索引,LIKE无效,ORDER BY强制文件排序,只会让锁和 I/O 更糟
TEXT比BLOB稍好,但陷阱仍在
虽然 TEXT 支持字符集、可建前缀索引、能参与 ORDER BY,但它同样触发 off-page 存储。所以 SELECT * FROM t WHERE title = 'x' ORDER BY content 这类语句,在 5.7 中仍会因 content 排序强制加载所有溢出页,引发大量随机读与锁争用。
真正起作用的不是字段类型本身,而是「是否被查询计划实际访问」。哪怕字段类型是 TINYINT,只要出现在 SELECT 列表或 WHERE 条件中且无索引支撑,照样拉高锁开销——BLOB 只是把这个代价放大得更明显而已。











