外观
执行计划突变避坑:ANALYZE 与直方图修复实录
问题背景:同一条 SQL,为什么突然走错索引
线上最棘手的性能问题,往往不是 SQL 从来没快过,而是昨天还走索引,今天突然全表扫描或选错联合索引。典型现象是:业务代码没有发布,索引仍然存在,慢查询却在某个时间点集中爆发;EXPLAIN 发现 key 从预期的 idx_status_created 变成了另一个低选择性索引,rows 估算值与真实值相差几十倍。
这类问题很多时候源于 InnoDB 优化器依赖的统计信息失真。MySQL 优化器并不在每次优化时实时扫描全表,而是根据采样页数、索引基数、数据分布等信息估算成本。当大批量写入、删除、字段分布倾斜,或自动采样恰好抽到不均匀页面时,优化器可能误判条件选择率,最终造成执行计划突变。
下面通过一个订单明细表复现问题,并给出 ANALYZE TABLE、持久化统计参数和直方图的修复边界。
sql
-- 建立演示表
CREATE TABLE order_item_demo (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id INT NOT NULL,
status TINYINT NOT NULL,
channel VARCHAR(20) NOT NULL,
amount DECIMAL(12,2) NOT NULL,
created_at DATETIME NOT NULL,
KEY idx_status_created (status, created_at),
KEY idx_tenant_status (tenant_id, status),
KEY idx_channel (channel)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 写入 10 万行:95% 为已完成状态,少量待处理
INSERT INTO order_item_demo
(tenant_id, status, channel, amount, created_at)
WITH RECURSIVE seq AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM seq WHERE n < 100000
)
SELECT
1 + (n % 20),
CASE WHEN n % 20 = 0 THEN 0 ELSE 1 END,
CASE WHEN n % 100 = 0 THEN 'OFFLINE' ELSE 'ONLINE' END,
50 + (n % 500),
DATE_ADD('2025-01-01', INTERVAL n MINUTE)
FROM seq;待处理订单 status = 0 只有 5%,按 status + created_at 查询理应使用 idx_status_created:
sql
EXPLAIN SELECT id, amount
FROM order_item_demo
WHERE status = 0
AND created_at >= '2025-02-01'
ORDER BY created_at DESC
LIMIT 20;但在批量导入、删除或统计页采样不稳定后,优化器可能高估该索引命中行数,转而选择 idx_tenant_status,甚至进行全表扫描后排序。此时不要急着加 FORCE INDEX,因为它可能只是把统计问题掩盖成长期维护负担。
方案对比:ANALYZE、索引提示与直方图怎么选
修复统计失真有三类常见手段,适用场景不同,不能混用。
第一类是重新收集表级索引统计信息,即 ANALYZE TABLE。它成本相对可控,适合批量数据变更后索引基数变化明显、SHOW INDEX 中 Cardinality 明显异常、执行计划在多个普通索引之间摇摆的场景。
第二类是调整持久化统计参数。MySQL 8.0 默认使用持久化优化器统计信息,关键参数包括 innodb_stats_auto_recalc、innodb_stats_persistent_sample_pages 和 innodb_stats_transient_sample_pages。自动重算有阈值和延迟,不能指望大批量写入后立刻得到精确统计。
第三类是列级直方图。普通索引统计主要描述索引前缀的基数,不能准确表达非索引列或高度倾斜列的数据分布。例如 channel 字段中 99% 是 ONLINE,1% 是 OFFLINE,如果查询使用不同渠道,平均选择率会严重误导优化器,此时直方图更有价值。
免责声明
本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。