MySQL InnoDB持久化统计信息详解
这段内容是 MySQL InnoDB 存储引擎关于“持久化优化器统计信息(Persistent Optimizer Statistics)”的详细配置说明,属于数据库性能调优中非常核心和深入的部分。
我会用 清晰、结构化、通俗易懂 的方式,为你逐层解读这一大段文档的核心思想、关键机制和实际应用价值。
🎯 一、一句话总结
InnoDB 的“持久化统计信息”功能,就是让数据库记住每张表的数据分布特征(比如有多少行、索引有多“散”),并把这些信息保存到磁盘上,避免每次重启后重新估算,从而保证 SQL 执行计划稳定、可预测。
🔁 二、为什么需要“持久化统计”?—— 对比旧模式
旧模式(非持久化) | 新模式(持久化) ✅ |
---|---|
每次 MySQL 重启,统计信息清空 | 统计信息保存在磁盘上,重启不丢 |
下次访问表时重新采样计算 | 直接读取上次保存的结果 |
可能每次采样结果不同 → 执行计划漂移 | 执行计划更稳定 |
查询性能忽快忽慢 | 性能更可预期 |
📌 举个例子:
- 昨天你的 SQL 走了索引,很快。
- 今天重启 MySQL 后,采样结果变了,优化器觉得“索引不值得走”,于是全表扫描 → 变慢了。
这就是 执行计划不稳定 的典型问题。持久化统计就是为了解决这个问题。
🔧 三、如何开启持久化统计?
✅ 全局开启(默认已开启)
SET GLOBAL innodb_stats_persistent = ON;
⚠️ 这个参数从 MySQL 5.6 开始默认就是
ON
,所以大多数情况下你不需要手动设置。
✅ 表级开启(更灵活)
你可以在建表或改表时指定:
CREATE TABLE t1 (id INT PRIMARY KEY,name VARCHAR(100),KEY idx_name(name)
) ENGINE=InnoDBSTATS_PERSISTENT=1 -- 启用持久化统计STATS_AUTO_RECALC=1 -- 数据变化超10%自动更新统计STATS_SAMPLE_PAGES=25; -- 采样25页数据来估算
💡 即使全局关闭了持久化,也可以通过
STATS_PERSISTENT=1
单独为某张表开启。
🔄 四、自动更新统计:innodb_stats_auto_recalc
默认行为:
- 当一张表有 超过 10% 的数据被修改(INSERT/UPDATE/DELETE)后,
- 并且
innodb_stats_auto_recalc = ON
(默认 ON), - InnoDB 会在后台异步自动执行
ANALYZE TABLE
来更新统计信息。
⚠️ 注意:
- 是“异步”的,所以不会立刻更新,可能延迟几秒。
- 如果你需要立刻更新统计信息(比如刚导入大量数据),应该手动运行:
ANALYZE TABLE your_table_name;
这会同步地重新计算统计信息。
❌ 如果关闭自动更新:
SET GLOBAL innodb_stats_auto_recalc = OFF;
那你就要自己负责在大批量 DML 操作后手动执行 ANALYZE TABLE
,否则优化器可能基于过时的统计做出错误决策。
📊 五、采样页数控制:STATS_SAMPLE_PAGES
和 innodb_stats_persistent_sample_pages
核心概念:随机采样(Random Dive)
InnoDB 不会扫描整张表来统计索引的“选择性”(cardinality),而是:
- 随机读取若干个索引页(默认 20 页)
- 通过这些样本估算整个索引的基数(不同值的数量)
参数说明:
参数 | 作用 |
---|---|
innodb_stats_persistent_sample_pages | 全局默认采样页数(默认 20) |
STATS_SAMPLE_PAGES | 可以为单个表单独设置采样页数 |
⚖️ 权衡:准确 vs 速度
采样页数多(如 100) | 采样页数少(如 5) |
---|---|
✅ 统计更准确 → 执行计划更好 | ❌ 统计不准 → 可能选错索引 |
❌ ANALYZE TABLE 更慢 | ✅ ANALYZE TABLE 更快 |
📌 建议:
- 小表:保持默认(20)
- 大表或统计不准导致性能问题:提高到 50~100
- 高频执行
ANALYZE TABLE
的场景:适当降低
🗑️ 六、删除标记记录是否计入统计?innodb_stats_include_delete_marked
背景问题:
InnoDB 的删除是“标记删除”——数据还在,只是加了个“已删除”标记。
默认情况下,统计信息计算时会忽略这些“已删除但未清理”的记录。
问题来了:
- 事务 A 删除了 90% 的数据,但还没提交。
- 事务 B 此时做
ANALYZE TABLE
,发现表几乎空了 → 生成一个“适合小表”的执行计划。 - 但事务 A 回滚了,数据还在!
- 结果:执行计划错得离谱!
解决方案:
SET GLOBAL innodb_stats_include_delete_marked = ON;
✅ 开启后:统计信息会把“已标记删除”的记录也算进去,避免因未提交事务导致统计失真。
📌 适用场景:高并发写入 + 频繁删除的系统。
🗃️ 七、统计信息存在哪?—— 两张系统表
InnoDB 把持久化统计信息存入以下两张表:
1. mysql.innodb_table_stats
—— 表级统计
字段 | 含义 |
---|---|
database_name | 数据库名 |
table_name | 表名(或分区名) |
last_update | 最后更新时间 ✅(重要!) |
n_rows | 表的行数估计 |
clustered_index_size | 主键索引占用页数 |
sum_of_other_index_sizes | 其他索引总页数 |
2. mysql.innodb_index_stats
—— 索引级统计
字段 | 含义 |
---|---|
index_name | 索引名 |
stat_name | 统计项名称(如 n_diff_pfx01 ) |
stat_value | 统计值 |
sample_size | 本次统计采样了多少页 |
stat_description | 描述(如 a,b 表示统计的是 a 和 b 列) |
🔍 八、关键统计项:n_diff_pfxNN
—— 索引基数(Cardinality)
这是优化器选择索引的核心依据!
含义:
n_diff_pfx01
:索引第 1 列的不同值数量n_diff_pfx02
:前 2 列组合的不同值数量- …
n_diff_pfxN
:前 N 列组合的不同值数量
示例解析(来自文档中的 t1
表):
| index_name | stat_name | stat_value | stat_description |
|------------|--------------|------------|------------------|
| PRIMARY | n_diff_pfx01 | 1 | a |
| PRIMARY | n_diff_pfx02 | 5 | a,b |
| i1 | n_diff_pfx01 | 1 | c |
| i1 | n_diff_pfx02 | 2 | c,d |
| i1 | n_diff_pfx03 | 2 | c,d,a |
| i1 | n_diff_pfx04 | 5 | c,d,a,b |
分析:
- 主键
(a,b)
:a
只有 1 个值,但(a,b)
有 5 个不同组合 → 唯一性强 - 二级索引
i1(c,d)
:虽然定义是(c,d)
,但 InnoDB 会自动加上主键(a,b)
→ 实际是(c,d,a,b)
- 所以有
n_diff_pfx03
和n_diff_pfx04
c
只有 1 个值 → 选择性极差,不适合做单列索引
- 所以有
📌 优化器就是靠这些 n_diff_pfx
值来判断:走哪个索引更高效。
🛠️ 九、高级技巧:手动修改统计信息(慎用!)
你可以直接更新 mysql.innodb_index_stats
来“欺骗”优化器:
UPDATE mysql.innodb_index_stats
SET stat_value = 1000000
WHERE table_name = 'orders' AND stat_name = 'n_rows';-- 必须执行,否则不生效
FLUSH TABLE orders;
用途:
- 测试不同执行计划
- 强制优化器选择某个索引(比如你知道数据分布特殊)
- 排查执行计划问题
⚠️ 警告:仅限测试环境!生产环境乱改可能导致严重性能问题。
📈 十、如何查看索引大小?(实用 SQL)
-- 查看每个索引占了多少页和字节
SELECT index_name,SUM(stat_value) AS pages,SUM(stat_value) * @@innodb_page_size AS size_bytes
FROM mysql.innodb_index_stats
WHERE table_name = 't1' AND stat_name = 'size'
GROUP BY index_name;
输出示例:
+------------+-------+-------------+
| index_name | pages | size_bytes |
+------------+-------+-------------+
| PRIMARY | 1 | 16384 |
| i1 | 1 | 16384 |
| i2uniq | 1 | 16384 |
+------------+-------+-------------+
💡 默认 InnoDB 页大小是 16KB(16384 字节)
✅ 十一、最佳实践建议
场景 | 建议 |
---|---|
所有生产环境 | ✅ 确保 innodb_stats_persistent=ON |
大表频繁修改 | ✅ STATS_AUTO_RECALC=1 ,自动更新 |
批量导入/删除后 | ✅ 手动 ANALYZE TABLE |
执行计划异常 | 🔍 查 last_update 和 n_diff_pfx 是否过时或不准 |
统计不准 | 📈 增加 STATS_SAMPLE_PAGES |
ANALYZE TABLE 太慢 | 🐢 适当减少采样页数 |
高并发删除 | ✅ 考虑开启 innodb_stats_include_delete_marked |
调试执行计划 | 🧪 可临时修改 innodb_index_stats (测试环境) |
🧩 总结:核心要点图谱
持久化统计 (ON by default)
│
├── 存在哪? → mysql.innodb_table_stats + mysql.innodb_index_stats
│
├── 何时更新?
│ ├── 自动:数据变化 >10% 且 innodb_stats_auto_recalc=ON(异步)
│ └── 手动:ANALYZE TABLE(同步,立即生效)
│
├── 采样多少? → STATS_SAMPLE_PAGES(越大越准,越慢)
│
├── 删除的数据算不算? → innodb_stats_include_delete_marked
│
└── 能不能骗优化器? → 可手动改统计表 + FLUSH TABLE(高级玩法)
如果你有具体的性能问题,比如:
- “为什么这个 SQL 有时候走索引,有时候不走?”
- “
ANALYZE TABLE
太慢怎么办?” - “如何判断统计信息是否过时?”
欢迎继续提问,我可以结合真实场景帮你分析。