TiDB 的统计信息被篡改,通常不是外部黑客攻击导致的,而是内部运维误操作、不合理的自动收集策略或统计信息反馈机制异常引发的“逻辑损坏”。这种损坏不会导致 TiKV 节点宕机,但会让优化器“看走眼”,选错索引、选错 Join 算法,导致原本毫秒级的查询变成全表扫描,CPU 飙升甚至拖垮整个集群。
统计信息被篡改最直接的表象是执行计划突变。比如昨天跑得很快的 SQL,今天突然慢查询日志里全是它。用 EXPLAIN ANALYZE 一看,发现原本该走 IndexLookup 的计划变成了 TableFullScan,或者 HashJoin 变成了 IndexJoin,而索引选择错乱的根源往往就是统计信息里的行数预估、直方图或 CMSketch 数据失真。
处理这类问题,首先要快速止血,而不是先分析根因。在 TiDB 5.0 及以上版本,可以直接使用 SQL Binding 强制绑定已知的良好执行计划。操作逻辑是从历史计划或另一台正常集群中提取执行计划,然后创建绑定。示例如下:
-- 从 statements_summary 或慢日志中找到正常时的 SQL 和执行计划 -- 假设目标 SQL 的 digest 已知,创建绑定 CREATE GLOBAL BINDING FOR SELECT * FROM orders WHERE user_id = ? AND status = ? USING SELECT /*+ USE_INDEX(orders, idx_user_status) */ * FROM orders WHERE user_id = ? AND status = ?;
绑定完成后,优化器会强制使用指定的 Hint,绕过错误的统计信息。这是应急处理最快的方式,不需要重启服务,也不需要等待统计信息重新收集。
止血之后,需要定位统计信息到底哪里出了问题。直接查 mysql.stats_meta 表,看对应表的 row_count 和 modify_count。如果 row_count 与实际行数偏差超过一个数量级,或者 modify_count 异常巨大,说明统计信息已经严重失真。更隐蔽的情况是直方图损坏,这时需要查看 mysql.stats_histograms 和 mysql.stats_buckets,检查桶的上下边界是否出现 NULL 或数值越界。
-- 检查统计信息行数 SELECT table_id, row_count, modify_count FROM mysql.stats_meta WHERE table_id = (SELECT id FROM information_schema.tables WHERE table_name = 'orders' AND table_schema = 'your_db');
如果发现 modify_count 非常大,但实际表变更并不多,这通常是因为统计信息收集后发生了大量写入,但自动更新阈值设置不合理,导致统计信息一直没触发重收集。TiDB 默认的自动分析阈值是 modify_count / row_count > tidb_auto_analyze_ratio,默认为 0.5。在高频写入场景下,这个值可能偏大,需要调小到 0.1 甚至 0.05。
还有一种被忽视的篡改来源:手动执行 ANALYZE TABLE 时使用了错误的采样参数。比如有人为了加快收集速度,设置了非常小的采样率,或者在 ANALYZE 命令中指定了错误的列。更危险的是,有人直接通过 INSERT INTO mysql.stats_xxx 系统表手动修改统计信息,这在早期运维中偶尔会发生,目的是“欺骗”优化器选择某个索引,但这种方式极度危险,一旦数据分布变化,反而会引发更严重的计划错误。
如果确认是手动篡改了系统表,恢复方式就是删除错误的统计信息,然后重新执行 ANALYZE。删除统计信息不是直接 DELETE,而是使用 DROP STATS 语法,或者通过删除 mysql.stats_meta 中对应记录后,再清理关联的直方图和 CMSketch 表。但更安全的方式是用官方提供的统计信息管理接口:
-- 删除指定表的统计信息 DROP STATS orders; -- 重新收集 ANALYZE TABLE orders;
在 TiDB 6.0 之后,统计信息有了“历史版本”功能,这是一个非常重要的救命稻草。如果统计信息被篡改,可以直接回滚到之前的版本。通过查询 mysql.stats_history 表,找到篡改前的时间点,然后执行 ALTER TABLE ... SET STATS_HISTORY 或者使用 ADMIN RELOAD STATS 加载历史快照。这个功能要求 tidb_enable_stats_history 变量已开启,默认是开启的。
-- 查看统计信息历史 SELECT * FROM mysql.stats_history WHERE table_name = 'orders' ORDER BY create_time DESC LIMIT 5; -- 回滚到指定时间的统计信息(需要管理员权限) ADMIN RELOAD STATS FOR TABLE orders FROM '2025-01-15 10:00:00';
除了历史版本回滚,另一个容易被忽略的机制是统计信息反馈。TiDB 在执行查询时,会根据实际扫描行数与预估行数的差距,自动修正统计信息。但如果反馈机制本身被异常数据污染,它反而会“篡改”正确的统计信息。这种情况常见于某些极端 SQL 反复执行,导致 CMSketch 中的 TopN 值被过度强化,其他值的预估严重偏低。此时需要关闭统计信息反馈,重新收集统计信息后再开启:
-- 临时关闭统计信息反馈 SET GLOBAL tidb_enable_stats_feedback = OFF; -- 重新收集统计信息 ANALYZE TABLE orders; -- 再次开启 SET GLOBAL tidb_enable_stats_feedback = ON;
在分布式数据库中,统计信息被篡改还有一个特殊场景:分区表。TiDB 的分区表统计信息是全局和分区级别并存的。如果某个分区的统计信息被单独篡改或异常,优化器在生成全局计划时可能会严重跑偏。排查时,需要逐个分区检查 row_count 和直方图,使用 SHOW STATS_META 和 SHOW STATS_HISTOGRAMS 命令,带上 PARTITION 子句逐一确认。
对于核心业务表,建议从运维规范上杜绝统计信息被意外篡改。一是严格限制 ANALYZE 命令的执行权限,不要给业务账号或普通开发人员授予 SUPER 或 ANALYZE 权限。二是将自动统计信息收集的窗口设置在业务低峰期,并通过 tidb_max_auto_analyze_time 限制单表收集的最大时长,防止收集任务超时后留下不完整的统计信息。三是定期备份统计信息,可以使用 ADMIN DUMP STATS 将统计信息导出为 JSON 文件,存储在外部系统,一旦出现问题可以快速恢复。
-- 导出统计信息 ADMIN DUMP STATS mysql.stats_meta, mysql.stats_histograms, mysql.stats_buckets INTO 's3://backup-bucket/stats_backup' FORMAT 'json'; -- 从备份恢复 ADMIN LOAD STATS 's3://backup-bucket/stats_backup';
如果集群已经因为统计信息错误导致大面积慢查询,且无法快速定位具体被篡改的表,还有一个全局回退方案:将 tidb_analyze_version 从 2 切换回 1,然后全量重新收集统计信息。版本 2 的统计信息虽然更精确,但在某些边界情况下直方图合并逻辑可能出现异常,导致预估偏差。切换版本后,统计信息会按 v1 的逻辑重建,虽然精度略低,但稳定性更高,适合紧急恢复。
最后需要关注的是统计信息锁定功能。TiDB 6.5 之后支持 ALTER TABLE ... LOCK STATS 和 UNLOCK STATS,可以将核心表的统计信息锁定,防止自动收集或手动 ANALYZE 覆盖。这对于已经验证过执行计划稳定的表非常有用。但锁定后,如果数据分布发生重大变化,统计信息不会自动更新,反而可能引发新的计划问题。因此锁定统计信息的同时,必须建立配套的监控告警,当表的实际行数与锁定时的行数偏差超过阈值时,及时通知 DBA 介入。
总结下来,TiDB 统计信息被篡改的应对策略分为四个层次:第一层是用 SQL Binding 紧急止血;第二层是通过历史版本或备份快速回滚;第三层是排查篡改来源,修正自动收集策略或关闭异常反馈;第四层是建立长期防护机制,包括权限管控、统计信息锁定和定期备份。每一层都有明确的命令和操作路径,不需要猜测或试错。
