MySQL的行溢出数据是指InnoDB将超长字段(如TEXT、BLOB、长VARCHAR)移至独立溢出页并仅在主记录保留20字节指针的设计机制,仅在ROW_FORMAT=DYNAMIC或COMPRESSED下启用,会引发额外随机I/O影响性能。

什么是 MySQL 的行溢出数据
当一行数据太大(比如 TEXT、BLOB 或很长的 VARCHAR),InnoDB 无法把整行塞进一个 16KB 的页里,就会把超长字段的内容挪到单独的「溢出页」(overflow page)中,主记录只保留 20 字节的指针。这不是 bug,是 InnoDB 的标准设计。
常见错误现象:SELECT * FROM t WHERE id = 1 看似简单,但若该行有大字段,实际会触发额外的溢出页随机 I/O;EXPLAIN 完全不体现这点,容易误判性能。
关键点:
- 只有
ROW_FORMAT = DYNAMIC或COMPRESSED才启用溢出页;REDUNDANT和COMPACT会把前 768 字节存主页、其余溢出(更易填满页) - 溢出页本身是普通 B-tree 叶子页,但归属该聚簇索引,并非独立结构
-
innodb_page_size不影响溢出页大小——它始终是 16KB(除非你改源码重编译)
怎么确认某行是否发生了溢出
不能靠肉眼猜,得查内部视图或用工具验证。最直接的是看 INFORMATION_SCHEMA.INNODB_SYS_COLUMNS 和字段长度定义,但更实用的是运行时诊断。
实操建议:
- 用
SELECT LENGTH(col), LENGTH(col) > 8000 FROM t WHERE id = ?快速筛可能溢出的列(8000 是经验值,因页头、事务信息等占空间) - 开启
innodb_monitor_output并执行查询,搜日志里的overflow或off-page关键字 - 用
mysqlpump或mysqldump --no-data查建表语句,确认ROW_FORMAT是否为DYNAMIC - 注意:即使
VARCHAR(65535),只要实际存的短,也不会溢出;反之VARCHAR(1000)存了 900 字节 UTF8MB4 文本,也可能因页碎片而溢出
溢出页读取时的真实流程
不是“先读主页、再读溢出页”这么线性。InnoDB 在构造记录时才按需加载,且受缓冲池和预读机制影响。
典型路径:
- 执行
SELECT col FROM t WHERE id = 1,若col是溢出列,InnoDB 先从聚簇索引页读出 20 字节指针 - 解析指针得到溢出页的
FIL_PAGE_OFFSET,检查缓冲池(buf_pool)中是否已缓存该页 - 未命中则发起同步单页读(
os_aio),此时产生一次随机 I/O —— 这才是性能瓶颈所在 - 如果 SQL 是
SELECT *且多个大字段溢出,会依次读多个溢出页,但不会合并 I/O
性能影响明显:SSD 上单次随机读约 0.1ms,HDD 上可能 10ms+;并发高时容易卡在 os_aio 等待队列。
能绕过溢出页读取吗
不能跳过,但可以减少触发或降低开销。
可行做法:
- 查询时明确避开溢出列:
SELECT id, name FROM t而非SELECT *,避免无谓加载 - 对大字段建独立表(如
t_content),用主键关联,让主表保持紧凑 - 用
COMPRESSED行格式 +KEY_BLOCK_SIZE=4,压缩溢出页内容(但 CPU 开销上升) - 禁用
innodb_file_per_table=OFF时,溢出页仍写入系统表空间(ibdata1),迁移/清理更麻烦,务必打开
最容易被忽略的是:ALTER TABLE 修改 ROW_FORMAT 不会自动重写已有溢出数据,必须 ALTER TABLE ... FORCE 或 OPTIMIZE TABLE 才真正迁移。










