大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
你有没有遇到过这种情况:一条SQL昨天还跑得飞快,今天突然慢了一个数量级。执行计划没变、索引没删、数据量也没暴涨。你翻遍了慢查询日志、看了EXPLAIN、检查了服务器负载,一切看起来都正常------但就是慢了。
问题往往藏在两个容易被忽视的地方:索引碎片 和统计信息。
索引碎片让数据库"翻书"翻得更慢;统计信息过时让优化器"看地图"看得更偏。两者叠加,一条正常的SQL就可能变成慢查询。
今天把这两个"隐形杀手"彻底拆开讲一遍。
一、索引碎片:数据在磁盘上"散架"了
索引碎片是什么?
想象一本精装书,原来页码连续、装订整齐。你不断在书里撕掉旧页、插入新页------书脊松动,页码错乱,翻书要找半天。索引碎片就是数据库里的"书脊松动"。
B+Tree索引是InnoDB的索引结构。在理想状态下,索引页是连续存储的,扫描时顺序读取,I/O效率很高。但随着频繁的增删改操作,原本连续的索引页开始出现空洞和不连续。这些空洞和不连续就是索引碎片。
碎片的两种类型:
内部碎片:索引页内部有未被使用的空间。DELETE操作会留下"空洞",UPDATE可能导致页内空间分布不均。索引页的利用率降低,原来一个页能存100条记录,现在只能存70条,数据库就要读更多页。
外部碎片:索引页在物理存储上不再连续。INSERT导致页分裂,新的索引页被分配到磁盘的不同位置。范围扫描时,磁盘磁头需要在不同位置之间跳跃,I/O次数成倍增加。
碎片如何影响性能?
-
I/O次数增加:原本1次磁盘读能加载的索引数据,现在可能要读2-3次
-
缓冲池命中率下降:碎片导致更多不连续的数据页被加载到内存,挤占热点数据
-
范围扫描变慢:原本顺序扫描变成了跳跃式读取
如何诊断索引碎片?
MySQL没有直接的"碎片率"指标,但可以通过以下方式判断:
方法一:查看表空间碎片
bash
SELECT TABLE_NAME,
DATA_LENGTH,
INDEX_LENGTH,
DATA_FREE,
ROUND(DATA_FREE / (DATA_LENGTH + INDEX_LENGTH) * 100, 2) AS fragment_pct
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db'
AND DATA_FREE > 0
ORDER BY DATA_FREE DESC;
DATA_FREE表示表中未使用的空间(碎片)。当DATA_FREE超过100MB时就应该关注了。
方法二:监控缓冲池命中率
当Innodb_buffer_pool_read_requests与Innodb_buffer_pool_reads的比值低于1000:1时,表明可能存在碎片问题。
bash
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
方法三:对比索引大小与实际数据量
bash
SHOW INDEX FROM your_table;
如果索引的Cardinality(不同值数量)与表实际行数明显不符,可能既存在碎片问题,也存在统计信息问题。
如何治理索引碎片?
方法一:OPTIMIZE TABLE
bash
OPTIMIZE TABLE your_table;
OPTIMIZE TABLE对InnoDB表实际上是重建表,回收空间、消除碎片、更新统计信息。但会加读锁,大表操作时可能阻塞业务。
适用场景:业务低峰期、中小表、碎片率较高时。
方法二:ALTER TABLE重建
bash
ALTER TABLE your_table ENGINE=InnoDB;
效果与OPTIMIZE TABLE类似,同样会锁表。
方法三:pt-online-schema-change(推荐线上使用)
生产环境推荐使用Percona Toolkit的pt-online-schema-change工具,可以在线重建表,不阻塞读写。
bash
bash
pt-online-schema-change --alter "ENGINE=InnoDB" D=your_db,t=your_table
什么时候该治理碎片?
| 碎片率 | 处理建议 | 说明 |
|---|---|---|
| <5% | 暂不处理 | 正常范围 |
| 5%-20% | 观察,可择机处理 | 微软建议5%-30%重组索引 |
| >20% | 尽快安排处理 | 性能影响明显 |
| DATA_FREE>100MB | 优先处理 | 空间浪费严重 |
二、统计信息:优化器的"眼睛"花了
统计信息是什么?
统计信息是优化器做出执行计划决策的"眼睛"------它告诉优化器表有多大、列有多少个不同值、数据分布如何。优化器根据这些信息计算每种执行方式的"代价",然后选择代价最小的方案。
统计信息包含什么?
-
表的总行数
-
每列的不同值数量(Cardinality)
-
列的NULL值比例
-
数据分布(直方图)
统计信息过时的后果:优化器"看错"了
如果统计信息过旧,优化器就会基于错误的信息做决策:
-
表实际有1000万行,统计信息显示只有100万行 → 优化器可能选择全表扫描,认为"反正没多少数据"
-
某列实际有50万个不同值,统计信息显示只有5000个 → 优化器低估了索引的选择性,放弃使用该索引
典型案例:一张日志表有500万行,user_id列上有索引。业务方反馈一条查询突然变慢:
bash
SELECT * FROM user_logs WHERE user_id = 12345 AND create_time > '2026-01-01';
正常情况下走(user_id, create_time)复合索引,扫描几十行就返回。但实际执行计划显示type=ALL全表扫描。
检查统计信息后发现,这张表的统计信息还是三个月前收集的------当时表只有50万行。优化器根据旧统计信息估算:user_id=12345可能返回很多行(旧数据中该用户有大量记录),全表扫描更"划算"。执行ANALYZE TABLE更新统计信息后,查询从5秒降到0.05秒。
如何检查统计信息是否过时?
方法一:对比EXPLAIN的rows与实际行数
执行EXPLAIN后看rows列的估算值,再实际执行查询对比实际扫描行数。如果估算值和实际值差了一个数量级以上,统计信息很可能过旧。
方法二:查看统计信息的最后更新时间
bash
SELECT TABLE_NAME, UPDATE_TIME
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db';
如果统计信息已经几周甚至几个月没有更新,而表的数据变化很大,就需要执行ANALYZE TABLE。
方法三:查看索引的Cardinality
bash
SHOW INDEX FROM your_table;
Cardinality列显示索引列的不同值数量估算。如果Cardinality与实际明显不符,说明统计信息需要更新。
如何更新统计信息?
bash
ANALYZE TABLE your_table;
ANALYZE TABLE会重新收集表的统计信息(行数、Cardinality、数据分布等),让优化器获得准确的数据分布。
对于MySQL 8.0,还可以创建直方图来改善数据分布不均时的估算:
bash
ANALYZE TABLE your_table UPDATE HISTOGRAM ON column_name WITH 100 BUCKETS;
统计信息维护的最佳实践:
| 场景 | 建议操作 | 说明 |
|---|---|---|
| 批量数据导入后 | 立即执行ANALYZE TABLE |
数据变化巨大 |
| 大量数据删除后 | 立即执行ANALYZE TABLE |
表行数变化显著 |
| 表结构变更后 | 执行ANALYZE TABLE |
新索引需要统计信息 |
| 日常定期维护 | 每周或每月一次 | 根据数据变化频率调整 |
EXPLAIN中rows与实际差距>10倍 |
立即执行ANALYZE TABLE |
优化器选错索引的风险高 |
三、碎片与统计信息的联动维护
索引碎片和统计信息常常"结伴作案"------表经过了大量增删改,既产生了碎片,又让统计信息过时。一条SQL突然变慢,往往是两者的叠加效应。
联动维护策略:
1. 定期巡检清单
建议每周或每月执行一次以下检查:
bash
-- 1. 检查碎片情况
SELECT TABLE_NAME, DATA_LENGTH, INDEX_LENGTH, DATA_FREE,
ROUND(DATA_FREE / (DATA_LENGTH + INDEX_LENGTH) * 100, 2) AS fragment_pct
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db'
AND DATA_FREE > 0
ORDER BY DATA_FREE DESC;
-- 2. 检查统计信息状态
SHOW INDEX FROM your_table;
2. 日常维护窗口
在业务低峰期(如凌晨),对高频变动的表依次执行:
bash
-- 先更新统计信息
ANALYZE TABLE your_table;
-- 碎片严重时再重建
OPTIMIZE TABLE your_table;
3. 生产环境安全操作规范
-
第一次操作先在测试表上演练
-
线上环境先用
pt-online-schema-change或从库测试 -
监控操作期间的锁等待和业务影响
-
准备回滚方案
四、总结
执行计划突然变差的"隐形杀手",往往不是SQL写错了,而是索引碎片和统计信息这两个"看得到但容易被忽略"的问题。
-
索引碎片让数据库"翻书"翻得更慢------增加I/O、降低缓冲池命中率
-
统计信息过时让优化器"看地图"看得更偏------选错索引、走错执行计划
把索引碎片治理和统计信息维护纳入日常运维体系,你就能在业务方投诉之前,提前发现并消除隐患。定期巡检、及时治理、安全操作------这三件事做好了,执行计划"突然变差"的情况会越来越少。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~