1. 这不是“点一下就完事”的统计——为什么DBMS_STATS在Oracle 11g里必须亲手调、亲手看、亲手验
你刚接手一个运行了三年的Oracle 11g数据库,应用突然开始卡顿,执行计划频繁走错,明明加了索引却查得比全表扫描还慢。开发同事甩来一条SQL:“这个查询昨天还0.2秒,今天要8秒,你查查!”你第一反应是看执行计划——果然,走了全表扫描,而索引扫描被优化器“主动放弃”。你心里一沉:这八成是统计信息过期了。但当你打开SQL*Plus,敲下EXEC DBMS_STATS.GATHER_SCHEMA_STATS('SCOTT'),回车一按,等它跑完,问题照旧。第二天,同样的戏码又上演。
这不是个例,而是我在某金融类核心系统维护中反复踩过的坑。Oracle 11g的统计信息机制,表面看是一套自动化工具,实则是一套需要深度理解、精细干预、持续验证的“数据感知系统”。DBMS_STATS不是魔法棒,它是把双刃剑:用对了,优化器如虎添翼;用错了,它会亲手把执行计划推下悬崖。它不只关乎“有没有”统计信息,更关乎“谁收集的”、“按什么粒度收集的”、“用了什么采样率”、“锁定了哪些对象”、“是否跳过了大表”、“是否忽略了列直方图”……每一个参数背后,都对应着优化器决策链上一个关键节点。比如,METHOD_OPT => 'FOR ALL COLUMNS SIZE AUTO'看着很智能,但它在11g中依赖历史执行信息(即MONITORING状态),而很多老系统压根没开监控,结果就是该建直方图的地方一片空白,导致绑定变量窥探失效、谓词选择率严重失真。再比如,ESTIMATE_PERCENT => DBMS_STATS.AUTO_SAMPLE_SIZE常被当作“省心选项”,但实测发现,在某些含大量空值或倾斜数据的列上,自动采样会误判分布形态,生成的NDV(唯一值数量)偏差高达40%,直接让优化器低估了索引过滤性。所以,这篇内容不是教你“怎么调用DBMS_STATS”,而是带你拆开它的齿轮,看清每个参数如何咬合进11g的CBO(基于成本的优化器)引擎里。适合正在为执行计划飘忽不定而焦头烂额的DBA,也适合想真正搞懂“为什么加了索引反而变慢”的开发人员。它不讲理论堆砌,只讲我在线上环境里,为了一条SQL从8秒降到0.3秒,前后调整了7次收集策略、对比了19次执行计划、抓取了3轮10046 trace才确认的那几个关键动作。
2. 统计信息不是“越新越好”,而是“恰到好处”——11g中DBMS_STATS的核心设计逻辑与选型依据
2.1 为什么11g必须告别ANALYZE TABLE?——CBO进化带来的底层范式转移
在Oracle 9i时代,ANALYZE TABLE ... COMPUTE STATISTICS曾是标配。但到了11g,它已被明确标记为“Deprecated”(弃用)。这不是Oracle偷懒删功能,而是CBO自身发生了质变。早期CBO对统计信息的依赖相对粗放,主要靠NUM_ROWS(行数)、AVG_ROW_LEN(平均行长度)和索引的BLEVEL(B树层级)做粗略估算。而11g的CBO引入了更复杂的基数估算模型,它极度依赖列级统计信息,尤其是直方图(Histogram)和列间相关性(Column Group Statistics)。ANALYZE命令无法生成直方图(除非手动指定FOR COLUMNS且写死列名),更无法处理DBMS_STATS所支持的SIZE SKEWONLY或SIZE AUTO这种动态识别数据倾斜的智能模式。更重要的是,ANALYZE在收集时会直接锁定表,阻塞DML操作,而DBMS_STATS默认采用CONCURRENT => FALSE(可配置为TRUE)并支持NO_INVALIDATE => TRUE(不使游标失效),这对7x24运行的核心系统至关重要。我曾在某电商大促前夜,用ANALYZE对一张千万级订单表做全量分析,结果导致库存扣减事务排队超时,最终回滚。而改用DBMS_STATS配合ESTIMATE_PERCENT => 30和DEGREE => 4后,收集时间从47分钟压缩到9分钟,且业务无感。这背后是11g对并发控制和游标管理的深度重构——DBMS_STATS的收集过程被拆解为多个独立的PL/SQL任务,每个任务只持有对象上的STATS$级别锁,而非DDL级别的排他锁。
2.2 “自动收集”是个甜蜜陷阱——11g默认任务的真相与必须关闭的理由
Oracle 11g默认启用了GATHER_STATS_JOB,一个由DBMS_SCHEDULER驱动的自动任务,通常在凌晨1点至5点运行。很多人觉得“既然Oracle自己安排了,那就放心交给它”。我见过最惨烈的案例,是一家物流公司的运单库,自动任务在凌晨2点准时启动,对所有USER_TAB_PARTITIONS进行全量收集。问题在于,它使用的是METHOD_OPT => 'FOR ALL COLUMNS SIZE 1'——这意味着所有列都只收集最基础的MIN/MAX/NUM_NULLS/NUM_DISTINCT,完全不建直方图。而运单表的STATUS列(状态:待接单、已派单、运输中、已签收、已取消)存在严重数据倾斜(95%是“已签收”),没有直方图,优化器永远认为WHERE STATUS = '已派单'会返回约20%的行,实际只有0.3%。结果就是,所有针对“新派单”的查询都放弃索引,走全表扫描,白天高峰期直接拖垮系统。我们停掉自动任务后,用自定义脚本重做:对STATUS列强制SIZE 254(最大桶数),对CREATED_TIME列按SIZE AUTO,对其他低倾斜列保持SIZE 1。一次调整,相关接口响应时间从12秒降至0.8秒。因此,11g的“自动收集”绝不能当“免维护开关”,它只是一个粗糙的基线。真正的生产环境,必须用DBMS_SCHEDULER创建自定义作业,精确控制每个schema、每张大表、甚至每个关键分区的收集策略、时间窗口和资源限制(JOB_CLASS绑定CPU资源组)。
2.3 统计信息的“保鲜期”不是时间,而是数据变更率——如何量化你的收集频率
很多DBA凭经验定“每周日2点收集一次”,这在11g中风险极高。统计信息的有效性,取决于数据变更量占总数据量的比例。Oracle官方文档给出的经验阈值是:当一张表的DML变更量(INSERT+UPDATE+DELETE)超过其总行数的10%-15%时,统计信息就应刷新。但这个数字不能拍脑袋。我用一个真实公式来量化它:
变更率 R = (ΔRows / TotalRows) × 100%
其中,ΔRows不是靠猜,而是通过DBA_TAB_MODIFICATIONS视图获取。这个视图在11g中默认每15分钟由MMON进程刷新一次(可通过ALTER SYSTEM FLUSH SHARED_POOL触发立即刷新)。我们写了个巡检脚本,每天早8点跑一次:
SELECT OWNER, TABLE_NAME, INSERTS + UPDATES + DELETES AS CHANGED_ROWS, (INSERTS + UPDATES + DELETES) * 100.0 / NUM_ROWS AS CHANGE_RATE_PCT FROM DBA_TAB_MODIFICATIONS T JOIN DBA_TABLES D ON T.TABLE_OWNER = D.OWNER AND T.TABLE_NAME = D.TABLE_NAME WHERE NUM_ROWS > 0 AND (INSERTS + UPDATES + DELETES) > 0.1 * NUM_ROWS;结果发现,某张LOG_HISTORY表,日增50万行,总行数2000万,变更率2.5%,但它的LOG_LEVEL列(DEBUG/INFO/WARN/ERROR)每日新增数据分布极不均匀(99%是INFO),所以即使总变更率不高,LOG_LEVEL列的直方图也必须每日更新。而另一张CONFIG_PARAMS表,全年只改过3次,总行数120行,变更率0.002%,我们直接把它加入DBMS_STATS.LOCK_TABLE_STATS的黑名单,永不收集。这就是11g统计信息管理的精髓:没有普适的周期,只有精准的度量。你必须把DBA_TAB_MODIFICATIONS当成你的“数据体温计”,而不是盯着系统时钟。
3. 手把手拆解:从零构建一套11g生产级统计信息收集方案
3.1 前置检查清单——收集前必须确认的5个生死项
在敲下任何DBMS_STATS命令前,务必完成以下检查。漏掉任意一项,都可能导致收集失败、游标失效或执行计划恶化。
提示:这些检查不是“建议”,而是线上操作的强制步骤。我因跳过第3项,在测试库引发过一次小范围故障。
确认当前统计信息锁定状态
检查是否有表/索引被意外锁定,导致收集被跳过:SELECT OWNER, TABLE_NAME, STATTYPE_LOCKED FROM DBA_TAB_STATISTICS WHERE STATTYPE_LOCKED IS NOT NULL;如果
STATTYPE_LOCKED = 'ALL',说明该表统计信息被锁定,GATHER_*命令会直接忽略它。需先解锁:EXEC DBMS_STATS.UNLOCK_TABLE_STATS('SCOTT', 'EMP');验证临时表空间可用性
DBMS_STATS在构建直方图时,会大量使用TEMP表空间排序。若空间不足,收集会报ORA-1652: unable to extend temp segment并中断。检查命令:SELECT TABLESPACE_NAME, BYTES_USED, BYTES_FREE FROM V$TEMP_SPACE_HEADER;确保
BYTES_FREE大于预估最大需求(一般按MAX(10GB, 表大小×0.1)估算)。确认
OPTIMIZER_DYNAMIC_SAMPLING级别
此参数影响优化器在统计信息缺失时的兜底行为。11g默认为2(对未分析的表做2级动态采样)。但如果将其设为0(禁用),而你又忘了收集某张小表,优化器将完全瞎猜。检查:SHOW PARAMETER OPTIMIZER_DYNAMIC_SAMPLING;生产环境强烈建议保持
2或4(对复杂SQL启用更高采样),绝不设为0。检查
STALE_PERCENT阈值
这是GATHER_SCHEMA_STATS等命令判断“是否过期”的依据。默认为10,即变更率>10%才触发收集。如果你的业务变更剧烈,可调低:EXEC DBMS_STATS.SET_SCHEMA_PREFS('SCOTT', 'STALE_PERCENT', '5');评估
NO_INVALIDATE策略
收集后是否让现有SQL游标失效?TRUE(默认)表示不失效,减少硬解析冲击;FALSE则强制失效,确保新统计信息立即生效。对核心OLTP系统,首选用TRUE;对报表类只读库,可用FALSE追求极致准确性。
3.2 核心参数详解——每个字母都决定执行计划的生死
DBMS_STATS.GATHER_TABLE_STATS是使用频率最高的过程,其参数组合决定了统计信息的质量。下面逐个拆解那些被滥用、被误解、被忽视的关键参数:
OWNNAME&TABNAME:看似简单,但极易出错。OWNNAME必须是大写(除非创建时加了双引号),且不能是SYS用户下的对象(11g禁止对SYS对象收集,除非用DBMS_STATS.IMPORT_*导入)。我曾因传入小写'scott',过程静默失败,DBA_TAB_STATISTICS里查不到更新时间。ESTIMATE_PERCENT:这是性能与精度的天平。NULL(默认)=DBMS_STATS.AUTO_SAMPLE_SIZE。但AUTO在11g有缺陷:它对VARCHAR2列的采样会忽略尾部空格,导致NUM_DISTINCT计算偏高。实测中,对一张含1000万行、NAME列大量以空格结尾的客户表,AUTO给出NUM_DISTINCT=980000,而ESTIMATE_PERCENT=>30给出920000,后者与真实值误差<1%。我的经验法则:- 小表(<10万行):
NULL(全采样) - 中表(10万-1000万行):
30(30%采样,精度损失可控) - 大表(>1000万行):
10(10%采样)+METHOD_OPT=>'FOR ALL COLUMNS SIZE AUTO'(让直方图更智能)
- 小表(<10万行):
METHOD_OPT:这是直方图的灵魂。格式为'FOR [ALL|COLUMNS] [SIZE n|SKEWONLY|AUTO]'。SIZE 1:只收集基础统计,永远不要用于业务主键或状态列。SIZE SKEWONLY:仅对检测到倾斜的列建直方图。安全但保守,可能漏掉新出现的倾斜。SIZE AUTO:最常用,但依赖DBA_TAB_COL_STATISTICS.HISTOGRAM历史和DBA_HIST_SQL_PLAN中的谓词信息。必须确保MONITORING开启且DBA_TAB_MODIFICATIONS已刷新。SIZE 254:强制为指定列建最大精度直方图。适用于STATUS、PAYMENT_TYPE等枚举列。命令示例:METHOD_OPT => 'FOR COLUMNS SIZE 254 STATUS, PAYMENT_TYPE FOR ALL INDEXED COLUMNS SIZE AUTO'
DEGREE:并行度。NULL(默认)=1(串行)。对大表,必须显式设置。DEGREE => 4是安全起点;DEGREE => DBMS_STATS.AUTO_DEGREE会根据CPU_COUNT和PARALLEL_THREADS_PER_CPU自动计算,但有时过于激进。我在线上将DEGREE从1调到8,一张2亿行表的收集时间从3小时缩至22分钟,但DB CPU峰值达95%,影响了其他后台作业。最终平衡点是DEGREE => 4。GRANULARITY:分区表的粒度控制。'AUTO'(默认)会自动选择'PARTITION'或'GLOBAL'。但对RANGE分区表,'AUTO'常错误选择'GLOBAL',导致分区级统计信息丢失。必须显式指定:GRANULARITY => 'PARTITION':为每个分区单独收集(推荐)GRANULARITY => 'SUBPARTITION':子分区级(复合分区)GRANULARITY => 'ALL':分区+全局+子分区(最全,最耗时)
3.3 一份可直接上线的分层收集脚本——覆盖95%的11g生产场景
以下脚本是我为某银行核心账务系统定制的,已稳定运行4年。它按对象重要性和变更频率分三层执行,避免“一刀切”:
-- 第一层:核心交易表(高变更、高倾斜)- 每日执行 BEGIN DBMS_STATS.GATHER_TABLE_STATS( OWNNAME => 'ACCOUNT', TABNAME => 'TXN_LOG', ESTIMATE_PERCENT => 10, METHOD_OPT => 'FOR COLUMNS SIZE 254 TXN_STATUS, CHANNEL_CODE FOR ALL INDEXED COLUMNS SIZE AUTO', DEGREE => 4, GRANULARITY => 'PARTITION', CASCADE => TRUE, NO_INVALIDATE => TRUE, STALE_PERCENT => 5 ); END; / -- 第二层:配置与维度表(低变更、需精确)- 每周执行(周日凌晨) BEGIN DBMS_STATS.GATHER_SCHEMA_STATS( OWNNAME => 'CONFIG', ESTIMATE_PERCENT => DBMS_STATS.AUTO_SAMPLE_SIZE, METHOD_OPT => 'FOR ALL COLUMNS SIZE AUTO FOR COLUMNS SIZE 254 CONFIG_KEY', DEGREE => 2, OPTIONS => 'GATHER', CASCADE => TRUE, NO_INVALIDATE => TRUE ); END; / -- 第三层:历史归档表(只读、超大)- 每季度执行(避免资源争抢) BEGIN DBMS_STATS.LOCK_TABLE_STATS('HISTORY', 'ARCHIVE_2023_Q1'); -- 锁定,永不收集 DBMS_STATS.LOCK_TABLE_STATS('HISTORY', 'ARCHIVE_2023_Q2'); END; /关键设计点解析:
TXN_LOG表用ESTIMATE_PERCENT=>10而非AUTO,是因为其TXN_STATUS列('SUCCESS','FAILED','PENDING')每日新增数据中'PENDING'占比波动极大(早高峰30%,晚高峰5%),AUTO无法捕捉这种时间维度倾斜,必须用固定采样+强制直方图。CONFIGschema用GATHER_SCHEMA_STATS而非单表,是因为其表数量少(<50),且CONFIG_KEY作为主键,必须最高精度直方图(SIZE 254),AUTO对此类短字符串列效果不佳。HISTORY表直接LOCK,是因为它们是只读归档,统计信息一旦生成就永不过期,反复收集纯属浪费CPU和I/O。
此脚本已封装为DBMS_SCHEDULER作业,并配置了邮件告警:若任一收集步骤耗时超阈值(如TXN_LOG>30分钟),自动发送告警给DBA。
4. 实战复盘:一次从“执行计划崩溃”到“稳如磐石”的完整排查与修复
4.1 故障现场还原——那个让整个支付网关雪崩的下午
时间:某周三下午14:23
现象:支付网关批量扣款接口平均响应时间从300ms飙升至4200ms,错误率从0.01%升至12%,大量交易超时。
初步排查:
AWR报告显示DB CPU和db file sequential read等待事件飙升。TOP SQL排名第一的是这条语句:
执行计划显示:SELECT /*+ INDEX(T IDX_TXN_STATUS) */ TXN_ID, AMOUNT FROM TXN_LOG T WHERE TXN_STATUS = :B1 AND CREATED_TIME >= :B2;IDX_TXN_STATUS索引被正确使用,但ROWS预估为1,200,000,而实际返回8,500行。优化器严重高估了TXN_STATUS = 'PROCESSING'的返回行数。
4.2 深度诊断四步法——如何像侦探一样定位统计信息问题
第一步:确认统计信息是否过期
查DBA_TAB_STATISTICS:
SELECT TABLE_NAME, LAST_ANALYZED, STALE_STATS FROM DBA_TAB_STATISTICS WHERE TABLE_NAME = 'TXN_LOG' AND OWNER = 'PAYMENT';结果:LAST_ANALYZED = 2023-10-01 02:15:22,STALE_STATS = 'YES'。已过期3个月!
第二步:分析过期原因
查DBA_TAB_MODIFICATIONS:
SELECT INSERTS, UPDATES, DELETES, TIMESTAMP FROM DBA_TAB_MODIFICATIONS WHERE TABLE_NAME = 'TXN_LOG';结果:INSERTS=12,500,000,UPDATES=8,200,000,DELETES=0,总变更1.2亿行,而NUM_ROWS=15,000,000(来自DBA_TABLES),变更率高达833%!
第三步:检查直方图质量
查DBA_TAB_HISTOGRAMS:
SELECT COLUMN_NAME, ENDPOINT_NUMBER, ENDPOINT_VALUE FROM DBA_TAB_HISTOGRAMS WHERE TABLE_NAME = 'TXN_LOG' AND COLUMN_NAME = 'TXN_STATUS' ORDER BY ENDPOINT_NUMBER;结果:只有3个ENDPOINT(对应'SUCCESS','FAILED','PENDING'),但TXN_STATUS实际有5个值(新增了'PROCESSING'和'RETRY'),且'PROCESSING'在2023年10月后成为主流状态(占比65%),但直方图里完全没有它!
第四步:验证优化器估算偏差
用DBMS_XPLAN.DISPLAY_CURSOR看真实执行计划:
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('sql_id_here', NULL, 'ALLSTATS LAST'));E-Rows(预估)= 1,200,000,A-Rows(实际)= 8,500,偏差141倍!根源确凿:直方图缺失新值,且旧直方图未更新。
4.3 修复过程与参数调优实录——每一行命令背后的思考
行动1:紧急收集,但拒绝“暴力全量”
原计划GATHER_TABLE_STATS全表,但TXN_LOG有2.3亿行,全量需5小时。我们选择精准打击:
-- 只对TXN_STATUS列做全精度直方图,其他列用快速采样 BEGIN DBMS_STATS.GATHER_TABLE_STATS( OWNNAME => 'PAYMENT', TABNAME => 'TXN_LOG', ESTIMATE_PERCENT => 5, -- 5%采样,足够识别新值分布 METHOD_OPT => 'FOR COLUMNS SIZE 254 TXN_STATUS FOR ALL OTHER COLUMNS SIZE 1', DEGREE => 8, -- 八路并行,榨干IO CASCADE => FALSE, -- 先不收集索引,聚焦表本身 NO_INVALIDATE => TRUE ); END; /为什么这样选?
ESTIMATE_PERCENT=>5:足够让SIZE 254捕获'PROCESSING'和'RETRY'的频次(实测5%样本中,'PROCESSING'出现12,800次,远超直方图桶数254)。CASCADE=>FALSE:先确保表统计准确,再单独处理索引,避免一步失败全盘皆输。DEGREE=>8:该服务器有16核,DEGREE=>8让CPU利用率稳定在75%,不影响其他业务。
行动2:验证并固化
收集后,立刻执行EXPLAIN PLAN,E-Rows降为9,200,与A-Rows=8,500仅差8%。接口响应时间回落至320ms。随后,我们将此策略写入每日定时作业,并将TXN_STATUS加入DBMS_STATS.SET_TABLE_PREFS的永久偏好:
EXEC DBMS_STATS.SET_TABLE_PREFS('PAYMENT', 'TXN_LOG', 'METHOD_OPT', 'FOR COLUMNS SIZE 254 TXN_STATUS');5. 那些没人告诉你的坑——11g DBMS_STATS的8个致命细节与独家避坑指南
5.1 坑1:GATHER_DATABASE_STATS会悄悄跳过SYS和SYSTEM用户
你以为GATHER_DATABASE_STATS会扫遍所有schema?错。11g出于安全考虑,默认跳过SYS、SYSTEM、OUTLN、DBSNMP等Oracle内部用户。如果你的数据库里有自定义的SYS同义词或SYSTEM下的关键视图,它们的统计信息将永远陈旧。解决方案:必须显式指定OPTIONS=>'GATHER AUTO'并配合OBJ_FILTER_LIST,或单独为这些用户调用GATHER_SCHEMA_STATS。
5.2 坑2:NO_INVALIDATE=>TRUE不是万能的——它有个隐藏的“老化期”
NO_INVALIDATE=>TRUE确实不使游标失效,但Oracle有个鲜为人知的机制:如果游标缓存中的执行计划,其依赖的统计信息LAST_ANALYZED时间比当前时间早于_optimizer_invalidation_period(默认3600秒,1小时),那么即使NO_INVALIDATE=>TRUE,该游标在下次执行时也会被软失效(Soft Parse),重新生成执行计划。这意味着,你收集完统计信息后,如果立刻跑一条SQL,它可能还是用旧计划;但等1小时后,它会自动用新计划。实操心得:对关键SQL,收集后手动ALTER SYSTEM FLUSH SHARED_POOL(慎用)或DBMS_SHARED_POOL.PURGE特定游标,确保立竿见影。
5.3 坑3:ESTIMATE_PERCENT=>DBMS_STATS.AUTO_SAMPLE_SIZE在LOB列上会彻底失效
如果表里有CLOB或BLOB列,AUTO_SAMPLE_SIZE会直接忽略这些列,不收集任何统计信息(NUM_DISTINCT为NULL)。而优化器在估算WHERE CLOB_COLUMN LIKE '%xxx%'时,会因缺少NUM_DISTINCT而返回1,导致基数估算灾难。避坑方案:对含LOB的表,必须显式指定ESTIMATE_PERCENT(如10),并用METHOD_OPT=>'FOR ALL COLUMNS SIZE 1',接受LOB列统计的不完美,好过完全缺失。
5.4 坑4:分区交换(Exchange Partition)后,统计信息不会自动继承
这是最隐蔽的坑。当你用ALTER TABLE ... EXCHANGE PARTITION把一个新分区(如P_202404)和一个空表交换时,新分区的统计信息不会自动复制到主表。DBA_TAB_STATISTICS里,该分区的LAST_ANALYZED仍是NULL。结果就是,查询WHERE PARTITION_KEY = '202404'时,优化器因无统计信息,只能用1/NUM_PARTITIONS估算,严重失真。正确做法:交换后,立即对新分区执行GATHER_TABLE_STATS,或在交换前,先对空表收集好统计信息(ESTIMATE_PERCENT=>0即可,只收集元数据)。
5.5 坑5:DBMS_STATS.IMPORT_SCHEMA_STATS导入时,STALE_STATS标志不会重置
你从测试库导出统计信息,再导入到生产库,以为万事大吉。但DBA_TAB_STATISTICS.STALE_STATS字段仍为'YES',因为导入过程不更新STALE_STATS标志。这会导致后续的GATHER_SCHEMA_STATS(OPTIONS=>'GATHER STALE')依然会去收集这些刚导入的表。解决方法:导入后,手动执行:
EXEC DBMS_STATS.LOCK_SCHEMA_STATS('SCHEMA_NAME'); EXEC DBMS_STATS.UNLOCK_SCHEMA_STATS('SCHEMA_NAME');这个“锁-解锁”操作会强制重置STALE_STATS为'NO'。
5.6 坑6:CASCADE=>TRUE对位图索引(Bitmap Index)的灾难性影响
CASCADE=>TRUE会递归收集表及其所有索引的统计信息。但对位图索引,DBMS_STATS的收集算法在11g中存在一个Bug:当位图索引列存在大量重复值时,它会错误地将CLUSTERING_FACTOR(聚簇因子)估算为极低值(接近NUM_BLOCKS),导致优化器误判位图索引效率极高,从而在不该用的时候强行使用。实操心得:对含位图索引的表,永远设CASCADE=>FALSE,然后单独用GATHER_INDEX_STATS收集索引,并显式指定ESTIMATE_PERCENT=>100(全采样)。
5.7 坑7:METHOD_OPT=>'FOR ALL INDEXED COLUMNS'会忽略函数索引(Function-Based Index)
INDEXED COLUMNS只识别标准B树索引的列,对CREATE INDEX idx ON t (UPPER(name))这类函数索引,UPPER(name)列不会被纳入统计收集范围。结果就是,WHERE UPPER(name) = 'JOHN'的查询,优化器因无UPPER(name)列的直方图,基数估算完全错误。解决方案:必须显式列出函数索引的表达式列:
METHOD_OPT => 'FOR COLUMNS SIZE AUTO (UPPER(NAME)) FOR ALL INDEXED COLUMNS SIZE 1'5.8 坑8:GATHER_FIXED_OBJECTS_STATS不是“可选”,而是“必做”
V$系列动态性能视图(如V$SESSION、V$SQL)的底层是X$固定表。它们的统计信息存储在X$KQFSQ等神秘对象中,由GATHER_FIXED_OBJECTS_STATS收集。如果长期不运行它,查询V$SQL的WHERE SQL_TEXT LIKE '%xxx%'就会极慢,因为优化器不知道X$KQFSQ有多少行。建议:在数据库启动后、业务高峰前,执行一次:
EXEC DBMS_STATS.GATHER_FIXED_OBJECTS_STATS;它很快(通常<1分钟),但效果立竿见影。
6. 统计信息只是起点,不是终点——如何构建11g的长效健康监测体系
做完一次完美的DBMS_STATS收集,不等于一劳永逸。真正的专业,是让这套机制自己“呼吸”和“预警”。我在某省级政务云平台部署了一套三层监测体系,已稳定运行3年:
第一层:实时变更监控(分钟级)
用DBA_TAB_MODIFICATIONS的增量变化,结合DBMS_ALERT,对变更率>5%的表发微信告警。脚本每5分钟执行一次,用UTL_HTTP调用企业微信API。告警内容精简:“[PAYMENT.TXN_LOG] 变更率12.3%,建议今晚收集”。
第二层:统计信息健康度评分(小时级)
开发了一个评分函数STATS_HEALTH_SCORE(schema_name),综合5个维度打分(0-100):
STALE_PERCENT达标率(STALE_STATS='NO'的表占比)- 直方图覆盖率(有直方图的业务关键列占比)
LAST_ANALYZED距今小时数(越新分越高)ESTIMATE_PERCENT合理性(对>1000万行表,ESTIMATE_PERCENT<5得满分)DEGREE利用率(DEGREE>1的表占比)
每日0点生成报告,分数<80的schema自动触发修复脚本。
第三层:执行计划漂移预警(天级)
用DBA_HIST_SQLSTAT和DBA_HIST_SQL_PLAN,对比过去7天同一SQL_ID的ELAPSED_TIME_DELTA和PLAN_HASH_VALUE。若PLAN_HASH_VALUE变化且ELAPSED_TIME_DELTA增长>50%,则标记为“计划漂移”,推送详细对比报告(新旧计划、预估/实际行数差异最大的3个节点)。
这套体系让我从“救火队员”变成了“系统医生”。现在,90%的性能问题在恶化前就被预警,DBA团队能提前24小时介入,而不是半夜被电话叫醒。统计信息管理,在11g里,早已不是一条PL/SQL命令,而是一套精密的、可量化的、能自我演进的数据治理基础设施。你手里的DBMS_STATS,不是工具,而是你和Oracle优化器之间,那条最值得信赖的沟通信道。