
业务里有一张订单扩展表大概结构如下CREATE TABLE order_ext ( id BIGINT PRIMARY KEY, order_id BIGINT, ext_info LONGTEXT, create_time DATETIME, KEY idx_order_id(order_id) );其中ext_info用来存储订单扩展信息包括用户提交的 JSON第三方返回报文风控字段审计信息上线初期数据量不大一切正常。但随着业务增长问题开始逐渐暴露根据order_id查询越来越慢buffer pool命中率下降磁盘IO持续升高即使只查少量数据响应时间依然不稳定更奇怪的是SELECT id, order_id FROM order_ext WHERE order_id ?;这种只查普通字段、不查LONGTEXT的 SQL也开始变慢。这就有点反常了。第一步排查索引没问题执行计划也正常首先查看执行计划EXPLAIN SELECT id, order_id FROM order_ext WHERE order_id 10001;结果显示命中了二级索引rows很小type ref看起来没任何异常。接着查看慢日志buffer pool使用情况磁盘IOundo / redo 状态依旧没有发现明显问题。直到后来我们注意到一个现象这张表的单行记录异常大。真正的问题大量字段进入了“溢出页”继续分析表结构后发现LONGTEXT字段里存储了大量 JSON 数据。有些记录甚至超过了几十 KB。而 InnoDB 的数据页默认只有16KB也就是说一行数据根本塞不进一个普通数据页。这时候InnoDB 就会启动一种机制溢出页Overflow Page什么是溢出页简单来说当一行数据太大普通页放不下时InnoDB 会把超长字段拆出去单独存储到其他页中。原始数据页里只保留前缀数据指针信息真正的大字段内容则放在“溢出页”里。大概可以理解成这样为什么溢出页会拖慢性能很多人会有一个误区“我又没查询 TEXT 字段为什么也会慢”原因在于虽然 SQL 没查大字段但行记录本身仍然更大页能容纳的记录数变少buffer pool缓存效率下降页分裂概率提升回表成本增加更关键的是某些场景下InnoDB仍然需要访问溢出页。比如MVCC版本读取行完整性校验回表读取行迁移一旦大量随机 IO 打到磁盘性能就会迅速恶化。InnoDB 是如何决定使用溢出页的这部分稍微深入一点。InnoDB 并不是所有 TEXT/BLOB 都直接放溢出页。它会根据行格式ROW_FORMAT字段大小页剩余空间综合决定。常见行格式包括CompactDynamicCompressed其中Compact 行格式会在数据页中保留768 字节前缀剩余部分放到溢出页。因此即使字段非常大主页里仍然会保留部分内容。Dynamic 行格式这是 MySQL 5.7/8.0 更推荐的方式。它会尽量只保留 20 字节指针大字段完整放入溢出页这样能让主页更“轻”。很多线上库升级后性能改善其实就和这个有关。为什么我们的线上问题越来越严重后来继续分析发现业务表存在几个典型问题。1. JSON 数据持续膨胀最初{status:ok}后来逐渐变成{ risk: {...}, audit: {...}, third_response: {...}, ... }单条数据越来越大。2. 热数据和冷大字段混在一起实际上业务查询只关心order_idstatuscreate_time但每次回表时大字段依然跟着存储在同一行逻辑结构里。导致热点数据缓存效率越来越差。3. 页利用率急剧下降正常情况下16KB 页可以放很多记录。但有大量大字段后一个页只能放几条记录。buffer pool很快被“低效占用”。最终解决方案最后我们用了几个手段解决问题。方案一拆分大字段