先说为什么这事值得单独拎出来讲。
干 DBA 的都遇到过这么个场景:某张表数据量变了,或者刚做完大批量数据导入, CBO 的统计信息该重新收集了。 dbms_stats.gather_table_stats 一敲下去,心里就开始打鼓——这次收集完,执行计划会不会变?变了是变好还是变坏?
最怕的是那种 " 收集完反而变慢 " 的情况。统计信息一旦更新,原来的计划就成了过眼云烟,想回到 " 昨天那个能跑的计划 " 却发现自己连昨天的统计信息长什么样都没存。这时候如果能拿出一套之前备份好的统计信息导回去,问题马上就解决了。
所以统计信息备份这事,本质上是给优化器买的一份保险,应用场景不止一个:
重新收集统计信息前,先存一份,万一计划劣化好回滚;
生产出了执行计划问题,想把生产的统计信息搬到测试环境复现;
数据库升级、迁移前,留个底;
SYSTEM 统计信息(机器的 CPU/IO 特征)调整前的快照。
Oracle 官方有篇文档专门讲这个, MOS Doc ID 464939.1 。下面把整套流程捋一遍。
第一步:建一张统计信息存放表
统计信息不是凭空存的,得先有个 " 容器 " 。 dbms_stats.create_stat_table 就是干这个的,它会在指定 schema 下建一张普通表,专门用来装导出的统计信息。
注意一个细节:这张表要建在谁的 schema 下? 官方建议建在被统计对象的所有者( OWNER ) schema 下,也就是那张表的 owner 。原因是后面 export_table_stats 默认按当前用户去匹配对象,建对地方能省很多权限上的麻烦。
exec dbms_stats.create_stat_table(user,'STAT_TIMESTAMP');
这里 STAT_TIMESTAMP 是你给这张存放表起的名字。建议带上时间戳或者用途,比如 STAT_20260606 、 STAT_BEFORE_UPGRADE ,别图省事叫 STAT_TABLE ——过几个月回头看,根本想不起来这是哪次的备份。命名是个习惯问题,但在这个场景下真的能救命。
第二步:把统计信息导进去
单张表或多张表
用 export_table_stats ,每张表跑一次:
sql
-- 表 1
exec dbms_stats.export_table_stats(user,'<TABLE_NAME>',NULL,'STAT_TIMESTAMP');
-- 表 2
exec dbms_stats.export_table_stats(user,'<TABLE_NAME_2>',NULL,'STAT_TIMESTAMP');
第三个参数是 partname (分区名),整表导出时传 NULL ;第四个参数就是上面建好的存放表名。
整个 schema
如果不想一张张挑,直接整个 schema 一锅端:
sql
exec dbms_stats.export_schema_stats(user,'STAT_TIMESTAMP');
这个我比较推荐在升级、迁移前用 ——省得漏。生产环境跑大变更之前,整个业务 schema 导一份,是个性价比很高的动作。
别忘了 SYSTEM 统计信息
这里有个容易漏的点: SYSTEM 统计信息( System Statistics )和表的统计信息不是一回事。它描述的是这台机器的 " 身体素质 " —— CPU 速度、单块 / 多块读的耗时、 MBRC 等等。 CBO 算成本时离不开它,但它存在 sys.aux_stats$ 里,跟表统计信息分开管。
先查一下有没有自定义的 system stats :
sql select sname, pname, pval1 from sys.aux_stats$ where pval1 is not null;
如果只返回 STATUS 、 DSTART 、 DSTOP 这类,没有 CPUSPEED 、 SREADTIM 、 MREADTIM 、 MBRC 这些实质性的值,说明你用的是默认的 NOWORKLOAD 模式,没什么好备份的。但如果跑过 gather_system_stats ,建议单独存一份:
sql
-- 存放表前面已经建过了,这里复用
exec dbms_stats.export_system_stats('STAT_TIMESTAMP');
顺带提一句, SYSTEM 统计信息一旦改变,影响的是整个库所有 SQL 的成本计算,所以动它之前一定要留底,而且最好挑业务低峰——它会让大量 SQL 重新硬解析。
第三步:该收集就收集
备份做完了,底气就有了。这时该怎么重新收集就怎么收集,官方那篇 1226841.1 ( How To: Gather Statistics for the Cost Based Optimizer )讲得很细,这里不展开。
第四步:出事了,还原
万一收集完发现计划劣化,或者新统计信息有问题,就把备份导回去:
sql
-- 单表还原
exec dbms_stats.import_table_stats(NULL,'<TABLE_NAME>', NULL,'STAT_TIMESTAMP'); exec dbms_stats.import_table_stats(NULL,'<TABLE_NAME_2>', NULL,'STAT_TIMESTAMP');
注意这里第一个参数( ownname )写的是 NULL ,因为它会去存放表所在的 schema 找。如果备份表和目标对象不在同一个 schema 下,要把 ownname 显式写出来。
想确认这张存放表里到底存了哪些表的统计信息:
sql
select distinct c1 from STAT_TIMESTAMP where type='T'; type='T' 表示 table 类型。c1 字段存的就是对象名。一行行看下来,确认要还原的对象都在,再一把梭:
sql
exec dbms_stats.import_schema_stats(user,'STAT_TIMESTAMP');
几个实操上的坑
把这套流程跑顺之后,回头看几个容易翻车的地方:
1. 备份表本身也得管。 create_stat_table 建出来的就是一张普通用户表,会跟着 schema 一起被 exp/imp 、 expdp/impdp 带走,也会占空间。备份完最好导出成 dmp 或者记录一下位置,别哪天被人当成废弃表给删了。
2. 跨库搬统计信息要配套搬结构。 把生产的统计信息导到测试库复现执行计划问题时,测试库的表结构、索引、分区定义必须和生产一致,否则 import 时对不上号,统计信息导进去也是错位的。这一点我见过好几次栽跟头的。
3. 统计信息有 " 锁定 " 这一说。 如果目标表上设置了 LOCK_TABLE_STATS , import 会被挡掉,得先 unlock_table_stats 。复现问题或者保护关键表计划时,锁定统计信息其实是个挺有用的手段,配合备份一起用更稳。
4. 别把统计信息备份和统计数据本身搞混。 备份的是 CBO 看到的 " 统计画像 " ,不是表里的业务数据。它能帮你还原执行计划,但还原不了业务数据——那是 RMAN 和闪回的活。
5. 还原之后别忘了重新看看计划。 import 完成意味着统计信息回退了,但已经在 shared pool 里的游标不会自动作废。需要的话,刷一下相关 SQL 的游标(比如 dbms_shared_pool.purge ,或者改个 hint 触发硬解析),让 CBO 按回退后的统计信息重新生成计划。
写在最后
整套备份 / 还原统计信息的流程其实不复杂,核心 : create_stat_table 、 export_*_stats 、 import_*_stats ,加上一个 system stats 的特例。难点不在语法,在于养成习惯——在每一个 " 动统计信息 " 的动作之前,先花两分钟留个底。
统计信息引发的执行计划问题,排查起来往往很折磨人,因为它不像报错那样直白,而是 " 昨天好好的,今天就慢了 " 。手里攥着一份干净的备份,等于把 " 昨天 " 这个状态固化了下来,进可复现、退可回滚。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70013625/viewspace-3174004/ ,如需转载,请注明出处,否则将追究法律责任。