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