SELECT *会触发多次随机I/O,因为InnoDB构造MVCC快照时必须读回所有溢出页(如TEXT/BLOB/超长VARCHAR),即使未查询这些字段;该过程同步、透明,EXPLAIN不提示,单次查询可能因50KB JSON跨多页引发数十次16KB随机读。

为什么SELECT *会触发多次随机I/O
只要某行存在溢出字段(如TEXT、BLOB或超长VARCHAR),InnoDB在构造该行的MVCC快照时,**必须读回所有溢出页**——哪怕SQL里根本没选这些字段。这个过程是同步的、对SQL层完全透明的,EXPLAIN也绝不会提示。一次SELECT *可能引发几十次16KB随机读,尤其当大字段存了50KB JSON时,会跨多个溢出页。
怎么确认当前表真有行溢出
别靠字段类型猜,LONGTEXT不一定溢出,VARCHAR(1000)也可能溢出。实操建议:
- 查
INFORMATION_SCHEMA.INNODB_SYS_COLUMNS,看is_virtual = 0且len > 768的列是否标记为extern - 执行
SELECT LENGTH(col) > 8000 FROM t WHERE id = ?快速筛出高风险行(8000是经验值,留出页头和事务开销) - 开启
innodb_monitor_output,执行查询后搜日志里的off-page或overflow关键字
绕过溢出页读取的三个硬手段
核心原则:让InnoDB不必拼完整行。不是“避免读”,而是“让它没机会读”。
-
SELECT id, status FROM t WHERE order_id = ?——明确只选非溢出列,且确保WHERE条件能走索引,避免全表扫描连带拉出所有溢出页 - 给二级索引加覆盖字段:
CREATE INDEX idx_order_id_cover ON order_ext (order_id) INCLUDE (id)(MySQL 8.0.23+),这样SELECT id, order_id可完全走索引,不回表 - 拆表:
ext_info这种纯扩展字段,单独建order_ext_detail表,主表只留业务强相关字段;关联查时再按需JOIN,把溢出页I/O控制在明确场景
ROW_FORMAT和innodb_large_prefix的坑
溢出页机制只在ROW_FORMAT = DYNAMIC或COMPRESSED下启用,但默认配置未必生效:
- 建表时没显式指定
ROW_FORMAT=DYNAMIC,即使引擎是InnoDB,也可能用COMPACT——后者会把前768字节存主页,更容易填满页、加剧碎片 -
innodb_large_prefix = ON必须配合ROW_FORMAT=DYNAMIC才让溢出页参与buffer pool缓存;否则哪怕innodb_buffer_pool_size设得再大,溢出页也几乎不进缓存 - 改已有表格式要用
ALTER TABLE t ROW_FORMAT=DYNAMIC,但会锁表;线上务必评估窗口期


















