1. 一次执行选择了错误的索引:问题现场与复现思路
优化器统计信息失真导致执行计划选错索引,是 Oracle DBA 最头疼的问题之一。我遇到过的典型场景是:一张 230 万行的内容表RES_ARTICLE_INFO,SQL 里同时有status=5、product_id=:1、display_time > sysdate-90三个条件,display_time过滤后返回 13 万到 15 万行,product_id的选择性明显更高,但优化器偏偏选了DISPLAY_TIME DESC上的函数索引NK_RES_ARTICLE_INFO,逻辑读飙升,CPU 直接打满。更让人困惑的是,同一张表、同一批 SQL,在不同时间点收集统计信息后,执行计划会在“正确”和“错误”之间反复横跳。
这篇文章面向正在排查 Oracle 执行计划选错索引的 DBA 和运维同学,也适合需要把 AI 工具接入日常日志与配置排查流程的开发者。我会先复现“统计信息偏差导致选错索引”的完整过程,给出可复制的DBMS_STATS收集脚本、错误索引复现 SQL 和执行计划对比动作,再说明如何用 TaoToken 统一 Key 把 AI 辅助排查接进这套流程里。核心检索词:索引、执行计划、优化器统计信息、DBMS_STATS、游标。下面所有操作都在测试库或可回滚的会话里做,生产库执行前先确认有恢复手段。
先交代一下问题表的背景。RES_ARTICLE_INFO大约 230 万行,DISPLAY_TIME列上有一个降序索引,Oracle 会为它生成一个隐藏虚拟列SYS_NC00062$。当DISPLAY_TIME上只收集了基本列统计信息、没有直方图时,优化器会假设数据均匀分布,而实际上这张表的时间分布极度倾斜:近一年每月 3 万到 5 万条,越往前越少,2004 年之前每月不足 1 万条,还混入了少量 1900 年前甚至公元前的异常日期。这种倾斜加上虚拟列统计信息缺失,就是执行计划选错索引的温床。
2. TaoToken 前置:统一 Key 接入 AI 辅助排查
排查这类问题时,我经常需要把 10053 跟踪文件、执行计划文本、统计信息查询结果丢给 AI 做交叉分析。但不同 AI 工具的接入方式、Key 管理、计费口径都不一样,来回切换很费时间。TaoToken 提供的是一个统一 Key / API 通道,把模型对话、编码辅助、日志分析这些能力收敛到一套凭证下,官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。
它的定位不是替代你的数据库客户端,而是给“排查过程中的文本分析”提供一个稳定通道。比如你把 10053 里ix_sel和ix_sel_with_filters的片段贴进去,让模型帮你归纳选择性计算的可能路径;或者把DBA_TAB_STATS_HISTORY的历史记录整理成表格,让模型辅助判断哪次收集引入了偏差。这些都属于辅助分析,最终判断仍然由你结合执行计划和实际数据来做。
接入前你需要准备两样东西:一个可用的 API Key,以及确认你的调用方式。Key 在控制台的 API Keys 页面创建,地址是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。如果你更习惯在对话界面里直接贴日志,可以用模型对话入口 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。如果你在做长期的编码或 Agent 类工作,比如写自动化的统计信息巡检脚本,Coding Plan 入口是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,Claude Code 相关配置参考 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite 。
注意:TaoToken 是 AI 能力接入通道,不参与你的数据库连接,也不接触生产库数据。贴日志前请自行脱敏,去掉真实库名、IP、业务字段值。
3. 可复制配置:DBMS_STATS 收集脚本与错误索引复现 SQL
3.1 先备份当前统计信息,建立可回滚点
在动任何统计信息之前,先确认历史备份可用。10g 之后 Oracle 每次收集统计信息前会自动备份当前版本,可以用DBA_TAB_STATS_HISTORY查看备份时间点。
-- 查看 RES_ARTICLE_INFO 的统计信息历史 SELECT stats_update_time FROM dba_tab_stats_history WHERE owner = USER AND table_name = 'RES_ARTICLE_INFO' ORDER BY stats_update_time DESC;如果历史备份不够,或者你想手动留一份,可以用export_table_stats导出到自己的备份表:
-- 创建备份表并导出当前统计信息 BEGIN DBMS_STATS.create_stat_table(ownname => USER, stattab => 'ZSJ_STAT_BAK'); DBMS_STATS.export_table_stats( ownname => USER, tabname => 'RES_ARTICLE_INFO', stattab => 'ZSJ_STAT_BAK', statid => 'BAK_BEFORE_FIX' ); END; /这一步的意义在于:一旦后续收集把执行计划带偏,你可以用import_table_stats或restore_table_stats快速回到已知状态。
3.2 复现“选错索引”的 SQL 与执行计划对比
下面两条 SQL 是当时出问题的精简版本,第一条走单表,第二条走视图V_173_HANGQING:
-- SQL 1:单表查询,display_time 过滤返回约 13 万行 SELECT count(a.image3) FROM res_article_info a WHERE a.status = 5 AND a.product_id = :1 AND a.display_time > sysdate - 90; -- SQL 2:视图查询,OR 条件涉及 catalog_id 和 brand_id SELECT count(1) FROM v_173_hangqing WHERE product_catalog_id = 75450 OR product_brand_id = 75450;视图V_173_HANGQING的定义里同样带了display_time > sysdate - 90和site_id=22等条件。当DISPLAY_TIME和虚拟列SYS_NC00062$上没有直方图时,优化器对display_time > sysdate-90的选择性估算会严重偏低,导致它认为走NK_RES_ARTICLE_INFO的代价比走product_id索引更低。
用EXPLAIN PLAN或DBMS_XPLAN对比两次收集前后的计划:
-- 查看当前执行计划 EXPLAIN PLAN FOR SELECT count(a.image3) FROM res_article_info a WHERE a.status = 5 AND a.product_id = 672914 AND a.display_time > sysdate - 90; SELECT * FROM TABLE(DBMS_XPLAN.display('PLAN_TABLE'));如果计划里出现INDEX RANGE SCAN | NK_RES_ARTICLE_INFO,而Rows估算值远小于实际返回行数,就说明选择性估算出了问题。
3.3 纠正统计信息收集:指定列和直方图策略
问题根源在于size auto对这张表的列判断失误:该收集直方图的DISPLAY_TIME和SYS_NC00062$没收集,不该收集的PRODUCT_ID反而收集了。纠正方式是显式指定method_opt:
-- 先删除旧统计信息,避免残留 EXEC DBMS_STATS.delete_table_stats(USER, 'RES_ARTICLE_INFO'); -- 按列指定直方图策略:虚拟列和 display_time 收集 254 桶,product_id 只留基本统计 EXEC DBMS_STATS.gather_table_stats( USER, 'RES_ARTICLE_INFO', cascade => FALSE, estimate_percent => 100, method_opt => 'FOR COLUMNS SIZE 254 SYS_NC00062$, DISPLAY_TIME, PRODUCT_ID SIZE 1', force => TRUE );收集后确认列统计信息:
SELECT column_name, num_buckets, histogram, num_distinct, density FROM user_tab_cols WHERE table_name = 'RES_ARTICLE_INFO' AND column_name IN ('DISPLAY_TIME', 'SYS_NC00062$', 'PRODUCT_ID');预期结果是SYS_NC00062$和DISPLAY_TIME的num_buckets为 254、histogram为HEIGHT BALANCED,PRODUCT_ID为NONE。
3.4 锁定统计信息并定制收集过程
核心业务表的统计信息不应该交给gather_stats_job用默认选项随意收集。先锁定,再自己建 job:
-- 锁定表的统计信息,gather_stats_job 将跳过该对象 EXEC DBMS_STATS.lock_table_stats(USER, 'RES_ARTICLE_INFO'); -- 确认锁定状态 SELECT stattype_locked FROM user_tab_statistics WHERE table_name = 'RES_ARTICLE_INFO';然后创建一个存储过程,用SIZE REPEAT保持已有的直方图策略:
CREATE OR REPLACE PROCEDURE proc_gather_stats_res_article IS BEGIN DBMS_STATS.gather_table_stats( USER, 'RES_ARTICLE_INFO', cascade => TRUE, estimate_percent => 100, method_opt => 'FOR ALL COLUMNS SIZE REPEAT', force => TRUE ); END; /用DBMS_SCHEDULER建 job 调用这个过程,按业务低峰期执行。SIZE REPEAT的含义是:已有直方图的列保持原有桶数,新加的列默认只收集基本统计信息,不会因为个别 SQL 触发不必要的直方图收集。
4. 验证请求与成功结果:执行计划对比与游标失效处理
4.1 用 10053 事件确认选择性计算
要看清优化器为什么选错索引,10053 跟踪是最直接的手段。在会话级别开启:
ALTER SESSION SET tracefile_identifier = 'idx_fix_check'; ALTER SESSION SET events '10053 trace name context forever, level 1'; -- 执行目标 SQL,加上唯一注释便于定位 SELECT /*+ idx_fix_check */ count(a.image3) FROM res_article_info a WHERE a.status = 5 AND a.product_id = 672914 AND a.display_time > sysdate - 90; ALTER SESSION SET events '10053 trace name context off';在跟踪文件里搜索Access Path和ix_sel,重点看两个值:ix_sel表示索引叶块扫描比例,ix_sel_with_filters表示回表行数比例。修复前常见的情况是ix_sel极小(比如 2.46e-06),导致优化器认为扫描代价极低;修复后这两个值应该接近实际数据分布。
4.2 执行计划对比
修复前后各跑一次DBMS_XPLAN,对比Rows和Cost:
-- 修复后查看计划 EXPLAIN PLAN FOR SELECT count(a.image3) FROM res_article_info a WHERE a.status = 5 AND a.product_id = 672914 AND a.display_time > sysdate - 90; SELECT * FROM TABLE(DBMS_XPLAN.display('PLAN_TABLE'));修复成功的标志是:计划从NK_RES_ARTICLE_INFO切换到IND_ARTINFO_PROD_ID,或者第二条 SQL 从单索引扫描切换到INDEX COMBINE使用IND_ARTINFO_PROD_CATAID和IND_ARTINFO_PROD_BRANDID。同时Rows估算值应该接近实际返回行数。
4.3 游标失效:别忽略 no_invalidate 的默认值
这里有一个很容易踩的坑。DBMS_STATS收集统计信息时,no_invalidate默认值是DBMS_STATS.AUTO_INVALIDATE,意思是相关游标不会立刻失效,而是等一段时间后逐渐失效。这样设计是为了避免大量游标集中硬分析造成性能抖动,但在你刚修复完统计信息、希望 SQL 立刻用新计划时,它反而会拖后腿。
我试过在修复后执行 SQL,发现计划还是旧的,就是因为游标没失效。解决办法有两个:
-- 方法一:收集时显式指定 no_invalidate => FALSE EXEC DBMS_STATS.gather_table_stats( USER, 'RES_ARTICLE_INFO', cascade => FALSE, estimate_percent => 100, method_opt => 'FOR ALL COLUMNS SIZE REPEAT', no_invalidate => FALSE, force => TRUE ); -- 方法二:对表做一次权限变更,强制相关游标失效 GRANT SELECT ON res_article_info TO scott; REVOKE SELECT ON res_article_info FROM scott;方法二看起来有点“野”,但在紧急恢复场景下非常有效,权限变更会让依赖该对象的游标立即失效,下次执行时重新硬分析。操作完成后记得回收不需要的权限。
4.4 用 TaoToken 辅助分析跟踪文件
10053 跟踪文件动辄几千行,人工翻找ix_sel和ix_sel_with_filters很费眼。你可以把关键片段脱敏后贴到模型对话里,让 AI 帮你归纳“哪些列的统计信息缺失”“选择性估算偏差出现在哪一步”。入口用 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。如果你在写自动巡检脚本,想把跟踪文件解析、异常检测串成流程,可以用 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 配合接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 来搭。
5. 本篇常见错排查
5.1 收集完统计信息,执行计划没变
最常见的原因是游标没有失效。先确认no_invalidate的设置,再检查V$SQL里目标 SQL 的PLAN_HASH_VALUE是否变化。如果没变,用权限变更或DBMS_SHARED_POOL.purge强制刷新。
5.2 restore_table_stats 之后 CPU 没降下来
restore_table_stats只恢复统计信息,不会让游标立即失效。默认的AUTO_INVALIDATE会让游标逐步失效,所以恢复后短时间内计划可能还是旧的。恢复时加上no_invalidate => FALSE,或者恢复后手动触发游标失效。
5.3 虚拟列 SYS_NC00062$ 没有统计信息
降序索引会生成隐藏虚拟列,USER_TAB_COL_STATISTICS和USER_TAB_COLUMNS里看不到它,需要查USER_TAB_COLS。收集时必须在method_opt里显式写出SYS_NC00062$,否则它连基本统计信息都没有,优化器只能用默认值估算,偏差极大。
5.4 size auto 收集了不该收集的直方图
size auto会根据列是否出现在WHERE条件中、是否被绑定变量 peeking 到,来决定是否收集直方图。这会导致主键列、name这类列被误收集直方图,进而引发游标版本激增、library cache latch争用。对核心表,用SIZE REPEAT或显式列指定来替代size auto。
5.5 异常日期数据导致选择性估算失真
表里混入 1900 年前甚至公元前的日期,会让LOW_VALUE和HIGH_VALUE跨度极大。没有直方图时,优化器假设均匀分布,选择性估算会严重偏离。处理方式是先清理异常数据:
UPDATE res_article_info SET display_time = to_date('2000-05-16', 'yyyy-mm-dd') WHERE display_time < to_date('199901', 'yyyymm') OR display_time > to_date('201102', 'yyyymm'); COMMIT;清理后再收集统计信息,直方图才能反映真实分布。
5.6 ix_sel 和 ix_sel_with_filters 不相等
对单列索引,理论上这两个值应该相等,但在函数索引对应的虚拟列上,它们可能不等。ix_sel反映叶块扫描比例,ix_sel_with_filters反映回表行数比例。当虚拟列统计信息缺失时,ix_sel可能接近 1,而ix_sel_with_filters偏小,导致代价估算失真。确保虚拟列收集了基本统计信息和直方图,是缓解这个问题的关键。
6. 把 AI 排查接进日常流程
统计信息排查的痛点不在于单次修复,而在于“下次还会不会出问题”。我的做法是把这套流程固化下来:核心表锁定统计信息,用定制 job 按SIZE REPEAT收集;每次收集前后用DBMS_XPLAN对比关键 SQL 的计划;把 10053 跟踪文件的关键片段脱敏后,通过 TaoToken 统一 Key 丢给 AI 做辅助归纳。API Key 在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 管理,接入方式参考 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
如果你也在做长期的数据库巡检或 Agent 类工具,Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 可以把模型调用、脚本编排收敛到一套配置里。最后提醒一句:任何统计信息操作前,先确认DBA_TAB_STATS_HISTORY里有可回滚的备份点,生产库上永远给自己留一条退路。