ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Oracle索引走了还慢?揭秘执行计划背后的三大性能命门

Oracle索引走了还慢?揭秘执行计划背后的三大性能命门 1. 这不是索引没走是索引“走歪了”——一个被低估的Oracle性能真相你有没有遇到过这种场景执行计划里明明白白写着INDEX RANGE SCAN甚至INDEX UNIQUE SCAN可这条SQL跑起来还是卡得像在等一锅开水烧开SELECT * FROM orders WHERE order_date DATE 2023-01-01 AND status SHIPPED明明给order_date和status建了复合索引EXPLAIN PLAN一看索引也走了但实际执行时间从毫秒级飙到8秒。这时候很多人第一反应是“索引失效了”赶紧删了重建或者加个/* INDEX(orders idx_order_date_status) */强制走索引——结果发现强制之后更慢了。这不是玄学这是Oracle优化器在用它自己的逻辑“认真地犯错”。我带过的三个DBA新人头两个月都在反复踩这个坑他们以为“走了索引性能好”却忽略了Oracle走索引时到底走了多少行、跳了多少页、回表了多少次。真正的瓶颈不在索引是否存在而在索引的访问路径效率和数据分布匹配度。关键词Oracle SQL 优化的核心从来不是“能不能走索引”而是“走索引时Oracle是不是在用最省力的方式拿数据”。这背后牵扯的是CBO基于成本的优化器对cardinality基数估算、selectivity选择率、clustering factor聚集因子这三个关键统计量的理解偏差。举个生活化的例子你要找一本《Oracle性能调优实战》放在图书馆里管理员告诉你“书在三楼B区第5排”这相当于“走了索引”。但如果B区第5排其实有2000本书而你要找的那本混在中间管理员还得一本本翻——这就是clustering factor高导致的IO爆炸。再比如管理员以为你要找的是“2023年出版的书”但其实你只要“2023年1月1日当天出版的”他按年份去查结果扫了整整一年的书架——这就是cardinality估算严重失真。所以当你看到执行计划里写着“INDEX RANGE SCAN”别急着庆祝先问自己三个问题第一这个索引扫描预估要读多少个数据块#Blocks第二预估返回多少行Rows第三这些行在物理存储上是不是扎堆儿住的Clustering Factor低这三个数字才是决定SQL快慢的真正命门。这篇文章不讲怎么建索引也不讲HINT语法只聚焦一个实战命题当索引明明被选中SQL却依然慢我们该如何像侦探一样一层层剥开Oracle执行计划背后的“假象”定位到那个真正拖慢速度的细节。适合所有正在被慢SQL折磨的开发、DBA和运维同学尤其适合那些已经会看执行计划但还卡在“知道走了索引却不知道为什么慢”这一关的人。2. 执行计划里的“索引”二字只是个开始不是结论2.1 看懂执行计划别只盯着OPERATION要盯死COST、ROWS、BYTES和ACCESS/PREDICATE很多同学看执行计划习惯性地只扫一眼OPERATION列看到INDEX RANGE SCAN就划过去了觉得“哦走了索引没问题”。这就像医生只看化验单上“白细胞正常”四个字就宣布病人健康完全忽略了中性粒细胞占比、淋巴细胞绝对值这些关键指标。Oracle的执行计划是一个完整的“作战地图”每个字段都藏着线索。我们以一条真实慢SQL为例SELECT o.order_id, o.customer_id, o.total_amount FROM orders o WHERE o.order_date BETWEEN DATE 2023-01-01 AND DATE 2023-01-31 AND o.status IN (SHIPPED, DELIVERED);其执行计划片段如下| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Pstart| Pstop | |-----|---------------------------------|--------------------|-------|-------|------------|----------|-------|-------| | 0 | SELECT STATEMENT | | 2500 | 70000 | 125 (2)| 00:00:02 | | | | 1 | TABLE ACCESS BY INDEX ROWID | ORDERS | 2500 | 70000 | 125 (2)| 00:00:02 | | | | 2 | INDEX RANGE SCAN | IDX_ORD_DATE_STATUS| 2500 | | 15 (0)| 00:00:01 | | |表面看Id 2走了索引IDX_ORD_DATE_STATUSCost只有15很低。但问题就藏在Rows列INDEX RANGE SCAN预估返回2500行而TABLE ACCESS BY INDEX ROWID即回表操作也预估2500行。这意味着Oracle打算拿着这2500个ROWID去主表ORDERS里逐个捞数据。如果ORDERS表是按order_id主键顺序存放的而order_date是随机插入的那么这2500个ROWID很可能指向2500个完全不相邻的数据块。一次磁盘IO通常只能读一个数据块8KB如果每个ROWID都落在不同的块上那就需要2500次物理IO——这比全表扫描可能只需要几百次IO还要慢。所以Rows这个数字本身不是问题问题是它和Clustering Factor的匹配度。Clustering FactorCF是Oracle统计信息里一个关键但常被忽视的值它衡量的是表中数据行的物理存储顺序与索引键值顺序的一致程度。CF越接近表的总块数NUM_BLOCKS说明数据越“散”CF越接近表的总行数NUM_ROWS说明数据越“聚”。计算公式很简单Oracle扫描索引叶节点按索引顺序读取ROWID每遇到一个ROWID指向的块号与上一个不同CF就1。因此CF的理论最小值是NUM_BLOCKS最大值是NUM_ROWS。一个健康的CF理想状态是接近NUM_BLOCKS。我们查一下这个索引的统计信息SELECT index_name, clustering_factor, num_rows, leaf_blocks FROM user_indexes WHERE index_name IDX_ORD_DATE_STATUS; -- 结果 -- IDX_ORD_DATE_STATUS 125000 500000 1200表有50万行索引叶块1200个CF高达12.5万。而NUM_BLOCKS表的总块数是大约8000块。CF125000远大于NUM_BLOCKS8000说明数据极其“离散”。此时哪怕索引扫描只返回2500行回表也要做约2500次随机IO。这才是慢的根源。Cost为125其中110都花在了回表上而不是索引扫描上。所以OPERATION列的“INDEX RANGE SCAN”只是整个链条的第一步真正耗时的往往是后续的TABLE ACCESS BY INDEX ROWID。我们必须把执行计划当成一个整体来看重点关注Rows列在每一行操作中的变化以及Cost的构成分解。Cost是一个相对值代表Oracle估算的I/O和CPU工作量但它内部的权重分配如_optimizer_cost_model参数会影响最终决策。在11g及以后版本默认是io模型即更看重I/O成本。因此当CF很高时即使索引扫描的Cost很低回表的Cost也会被放大导致总Cost飙升。但优化器有时会“误判”因为它依赖的统计信息可能过期或者对IN列表、函数、绑定变量的selectivity估算不准。所以看执行计划第一步永远是把Rows、Cost、Bytes三列抄下来画一个简单的流程图标出每一步的输入输出行数看看瓶颈究竟卡在哪一环。2.2 统计信息优化器的“眼睛”脏了就看不清路Oracle优化器是个“盲人”它所有的决策都基于统计信息。如果统计信息不准它就像一个近视500度还拒绝戴眼镜的司机再好的导航系统也救不了他。SQL优化中最常见的“明明走了索引还慢”十有八九是统计信息惹的祸。我们上面那个例子Rows预估2500但如果真实返回是25万行呢那Cost的估算就完全失真了。统计信息不准主要体现在三个方面NUM_ROWS表总行数、NUM_DISTINCT列唯一值数量和DENSITY密度用于估算查询的选择率。DENSITY尤其关键它决定了优化器对WHERE col value这类谓词的选择率估算。DENSITY的计算公式是1 / NUM_DISTINCT但这只适用于数据均匀分布的情况。现实中status列可能90%是PENDING只有10%是SHIPPED但优化器不知道它只会算DENSITY 1 / 3 0.333于是估算status SHIPPED会返回500000 * 0.333 ≈ 166500行而不是真实的50000行。这就导致它认为走索引不划算转而选择全表扫描。但更隐蔽的问题是HISTOGRAM直方图。直方图是Oracle用来描述列数据分布不均匀性的工具。没有直方图优化器就默认数据是均匀分布的。对于status这种典型的倾斜列skewed column必须收集FREQUENCY频度直方图。收集方法很简单-- 对status列收集频度直方图适用于唯一值少于254的列 EXEC DBMS_STATS.GATHER_TABLE_STATS( ownname SCOTT, tabname ORDERS, method_opt FOR COLUMNS status SIZE 254 ); -- 对order_date列由于日期范围大更适合HEIGHT BALANCED高度平衡直方图 EXEC DBMS_STATS.GATHER_TABLE_STATS( ownname SCOTT, tabname ORDERS, method_opt FOR COLUMNS order_date SIZE AUTO );SIZE AUTO会让Oracle自动判断是否需要直方图以及类型。但要注意AUTO并不总是最优有时需要人工干预。比如如果你知道order_date在最近一个月数据量暴增而历史数据稀疏AUTO可能不会为这个“热点区间”生成足够细的桶bucket导致估算依然不准。这时可以手动指定SIZE 254强制收集最细粒度的直方图。另一个致命错误是STALE_PERCENT过期百分比设置不当。默认是10%意思是当表数据变更超过10%统计信息就被标记为STALE。但对于一个每分钟都在写入的订单表10%可能几分钟就达到了而你的统计信息收集作业是每天凌晨跑一次那白天的SQL就一直在用过期的“老地图”。解决方案是降低STALE_PERCENT或者对高频更新的表启用INCREMENTAL增量统计信息收集-- 启用增量统计让Oracle只收集变化的分区或块 EXEC DBMS_STATS.SET_TABLE_PREFS( ownname SCOTT, tabname ORDERS, pname INCREMENTAL, pvalue TRUE );增量统计要求表是分区表并且启用了GRANULARITY AUTO。它能极大减少统计信息收集的时间和资源消耗保证实时性。最后检查统计信息是否“新鲜”不能只看LAST_ANALYZED时间戳更要查STALE_STATS状态SELECT table_name, object_type, last_analyzed, stale_stats FROM user_tab_statistics WHERE table_name ORDERS; -- 如果stale_stats YES说明统计信息已过期必须立即收集。我见过最离谱的一个案例一个金融系统的交易表LAST_ANALYZED是三个月前STALE_STATS是YES但DBA一直没发现。结果所有涉及该表的SQL优化器都在用一份“僵尸统计信息”做决策导致大量SQL走错执行计划系统负载常年90%以上。重启数据库、杀会话、加索引都无效最后GATHER_TABLE_STATS一跑负载瞬间掉到30%。所以SQL优化的第一步永远不是改SQL而是确认统计信息是否干净、准确、及时。这是所有后续分析的地基地基不牢一切优化都是空中楼阁。2.3 索引设计陷阱复合索引的“左前缀”不是万能钥匙“给WHERE条件里的列建复合索引”是教科书式的建议但现实远比教科书复杂。oracle函数大全及举例里那些炫酷的函数常常就是索引失效的元凶。我们回到那个SQLWHERE o.order_date BETWEEN ... AND ... AND o.status IN (SHIPPED, DELIVERED)建一个(order_date, status)的复合索引看起来天衣无缝。但问题在于BETWEEN和IN的组合在Oracle里会产生一个叫FILTER的额外操作。执行计划里可能会出现| 2 | INDEX RANGE SCAN | IDX_ORD_DATE_STATUS| 25000 | | 15 (0)| 00:00:01 | | | | * 3 | FILTER | | | | | | | | | 4 | TABLE ACCESS BY INDEX ROWID | ORDERS | 2500 | 70000 | 125 (2)| 00:00:02 | | |注意Id 3的FILTER。这意味着索引扫描先返回了25000行因为order_date范围太大然后Oracle在内存里用status IN (...)这个条件做过滤筛出最终的2500行。这25000行的索引扫描就是无谓的IO浪费。根本原因在于复合索引(order_date, status)的“左前缀”原则order_date是第一列所以BETWEEN可以高效利用它但status是第二列IN列表无法利用索引的有序性进行快速跳跃只能顺序扫描。所以索引的“有效范围”只到order_datestatus部分只是用来做FILTER。解决这个问题有两个思路。第一调整索引列序。如果status的取值非常有限比如只有5种状态且查询总是针对少数几个状态那么把status放在前面变成(status, order_date)就能让IN列表直接驱动索引查找。执行计划会变成| 2 | INDEX RANGE SCAN | IDX_STAT_DATE | 2500 | | 8 (0)| 00:00:01 | | |Rows直接预估2500没有FILTER。第二用UNION ALL重写SQL把IN拆成多个SELECT ... FROM orders WHERE order_date BETWEEN ... AND ... AND status SHIPPED UNION ALL SELECT ... FROM orders WHERE order_date BETWEEN ... AND ... AND status DELIVERED;这样每个分支都能完美利用(status, order_date)索引的INDEX RANGE SCAN而且UNION ALL没有去重开销。但这种方法的代价是SQL变长维护性下降。还有一个更隐蔽的陷阱函数索引。比如业务要求查“本月创建的订单”SQL写成WHERE TRUNC(create_date) TRUNC(SYSDATE)TRUNC函数会让普通索引失效。这时必须建函数索引CREATE INDEX idx_orders_trunc_create ON orders (TRUNC(create_date));但函数索引有个致命弱点它只对WHERE子句中完全一致的函数调用有效。如果SQL里写的是TRUNC(create_date, MM)而索引是TRUNC(create_date)那就匹配不上。更麻烦的是TRUNC(create_date)索引无法支持范围查询比如TRUNC(create_date) TRUNC(SYSDATE) - 30因为函数结果是离散的日期没有天然的顺序。所以最佳实践是尽量避免在WHERE条件里用函数而是用范围。上面的例子应该重写为WHERE create_date TRUNC(SYSDATE, MM) AND create_date ADD_MONTHS(TRUNC(SYSDATE, MM), 1);这样一个普通的(create_date)索引就能高效工作。总结一下复合索引不是简单的“把WHERE里的列都塞进去”而是一场精密的“列序博弈”。你需要问自己哪个列的选择率更低NUM_DISTINCT更小哪个列的过滤性更强DENSITY更小哪个列更常用于等值查询哪个列更常用于范围查询,,BETWEEN把等值列放前面范围列放后面才能让索引发挥最大威力。oracle分页常用的ROWNUM伪列也常和索引设计冲突。比如WHERE ROWNUM 10如果前面没有有效的索引驱动Oracle会先取出所有行再排序再取前10效率极低。正确的做法是先用索引定位到目标数据集再用ROWNUM。这又引出了下一个核心问题如何让Oracle“先定位再排序最后取数”。3. 深度诊断四步法从执行计划到AWR报告的完整链路3.1 第一步抓取真实执行计划而非解释计划EXPLAIN PLAN FOR是一个静态的、基于当前统计信息的“模拟”计划它不执行SQL所以看不到真实的Elapsed Time、Buffer Gets、Physical Reads。而DBMS_XPLAN.DISPLAY_CURSOR才是获取真实执行计划的金标准它从共享池Shared Pool里抓取刚刚执行过的SQL的实际执行路径和性能指标。使用方法如下-- 先执行你的慢SQL加一个唯一的注释方便识别 SELECT /* MONITOR */ o.order_id, o.customer_id FROM orders o WHERE o.order_date BETWEEN DATE 2023-01-01 AND DATE 2023-01-31 AND o.status IN (SHIPPED, DELIVERED); -- 然后立刻执行以下查询抓取它的真实执行计划 SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(sql_id your_sql_id, format ALLSTATS LAST));format ALLSTATS LAST是关键它会显示LAST_OUTPUT_ROWS上次执行实际返回的行数、LAST_ELAPSED_TIME上次执行耗时单位微秒、LAST_BUFFER_GETS逻辑读、LAST_DISK_READS物理读等真实指标。对比Rows预估和LAST_OUTPUT_ROWS实际如果相差10倍以上就说明统计信息严重失真。更重要的是ALLSTATS会显示STARTS该操作被执行了多少次。对于嵌套循环Nested Loop如果外层驱动表返回100行内层表就要被访问100次STARTS就是100。如果STARTS很大而LAST_OUTPUT_ROWS很小说明存在严重的“循环放大”效应。例如| 3 | NESTED LOOPS | | 1000 | 50000 | 200 (1)| 00:00:03 | | | | 4 | TABLE ACCESS FULL | CUSTOMERS | 100 | 2000 | 3 (0)| 00:00:01 | | | | 5 | TABLE ACCESS BY INDEX ROWID | ORDERS | 10 | 300 | 2 (0)| 00:00:01 | | | | 6 | INDEX RANGE SCAN | IDX_CUST_ID_DATE | 10 | | 1 (0)| 00:00:01 | | |这里Id 5的STARTS 100因为CUSTOMERS返回100行每次都要去ORDERS里查而LAST_OUTPUT_ROWS 1000说明平均每次查到10行。但如果LAST_OUTPUT_ROWS只有10而STARTS是100那就意味着99%的循环都是空跑这是巨大的浪费。ALLSTATS还会显示A-RowsActual Rows和E-RowsEstimated Rows一目了然。所以“抓真实计划”是诊断的第一步也是最关键的一步。没有真实数据一切分析都是纸上谈兵。3.2 第二步用SQL Monitor报告看清每一步的“血流图”DBMS_XPLAN.DISPLAY_CURSOR给你一张静态的“X光片”而SQL Monitor则是一段动态的“血管造影视频”。它是Oracle Enterprise Edition的高级特性能实时监控正在运行的SQL并生成包含时间线、资源消耗、并行度等丰富信息的HTML报告。开启方式很简单在SQL里加一个/* MONITOR */提示SELECT /* MONITOR */ o.order_id, o.customer_id, c.name FROM orders o, customers c WHERE o.customer_id c.customer_id AND o.order_date DATE 2023-01-01;执行后通过以下SQL找到报告URLSELECT sql_id, sql_text, sql_exec_id, status, elapsed_time, cpu_time, buffer_gets FROM v$sql_monitor WHERE sql_text LIKE %orders% AND status EXECUTING ORDER BY last_refresh_time DESC;然后用DBMS_SQLTUNE.REPORT_SQL_MONITOR生成详细报告SELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR( sql_id your_sql_id, sql_exec_id your_sql_exec_id, type ACTIVE ) AS report FROM dual;type ACTIVE会生成一个交互式的HTML报告你可以点开任何一个操作节点看到它的详细耗时分解Active Session History (ASH)采样点、Wait Events等待事件、IO RequestsIO请求数、Cell Offload存储层卸载等。最直观的是时间轴视图Timeline View它把整个SQL执行过程按时间拉成一条线清晰地标出哪段时间在做索引扫描哪段时间在回表哪段时间在做排序SORT ORDER BY哪段时间在等待IOdb file sequential read。你会发现一个看似简单的INDEX RANGE SCAN可能80%的时间都花在了db file sequential read上这直接指向了Clustering Factor高的问题。而TABLE ACCESS BY INDEX ROWID操作旁边会标注出Physical Read Requests物理读请求数如果这个数字接近LAST_OUTPUT_ROWS就证实了“一行一IO”的灾难。SQL Monitor还能帮你识别并行问题。如果SQL启用了并行PARALLELhint报告会显示每个PX Server并行进程的工作负载是否均衡。如果某个Server处理了90%的数据而其他Server闲着那就是Skew数据倾斜需要检查JOIN键或WHERE条件是否导致数据分布不均。SQL Monitor是SQL优化的“终极武器”它把抽象的执行计划变成了可视化的性能热力图。没有它你就像一个没有显微镜的医生只能靠猜。3.3 第三步深入AWR报告定位系统级瓶颈单条SQL慢可能是它自身的问题但如果一批SQL都慢那一定是系统级瓶颈。AWRAutomatic Workload Repository是Oracle的“黑匣子”每小时自动采集一次系统性能快照保存在SYSAUX表空间里。通过awrrpt.sql脚本可以生成一份详尽的性能报告。生成方法# 在$ORACLE_HOME/rdbms/admin目录下 $ sqlplus / as sysdba SQL ?/rdbms/admin/awrrpt.sql按提示选择起止快照ID、数据库ID、实例号即可生成HTML报告。报告的核心是前几页DB Time数据库总耗时、Top 5 Timed Foreground Events前台等待事件TOP5、SQL ordered by Elapsed Time按耗时排序的SQL TOP SQL。DB Time是关键指标它代表所有活动会话消耗的CPU和等待时间总和。如果DB Time远高于Elapsed Time挂钟时间说明系统有大量并发CPU或IO成为瓶颈。Top 5 Timed Foreground Events则告诉你瓶颈在哪里。如果是db file sequential read单块读高说明随机IO多指向Clustering Factor高或索引设计问题如果是db file scattered read多块读高说明全表扫描多可能缺少索引如果是latch: cache buffers chains高说明热点块争用可能需要调整DB_CACHE_SIZE或应用ASSM自动段空间管理如果是enq: TX - row lock contention高说明有行锁争用需要检查应用逻辑。SQL ordered by Elapsed Time页面会列出这段时间内最耗时的SQL包括它们的Executions执行次数、Elapsed Time per Exec每次执行耗时、Buffer Gets per Exec每次逻辑读。重点关注那些Elapsed Time per Exec高且Buffer Gets per Exec也高的SQL它们就是“IO大户”是SQL优化的首要目标。AWR报告的价值在于它把单条SQL的慢放到整个数据库的上下文中去审视。你可能会发现慢SQL集中出现在每天上午10点而那个时间点Top 5 Events里log file sync日志写等待异常高。这说明问题不在SQL本身而在应用提交太频繁或者LOG_BUFFER太小。这时优化方向就从“改SQL”转向了“调应用”或“调参数”。AWR是SQL优化的“上帝视角”它让你知道你是在修一辆车的轮胎还是在修整条高速公路。3.4 第四步用10046事件跟踪解剖SQL的每一根神经当以上所有方法都指向一个模糊的方向但你还是找不到确切的“病灶”时就需要祭出终极调试工具10046事件跟踪。它会生成一个详细的trace file记录SQL执行过程中每一个PARSE、EXECUTE、FETCH、WAIT、BIND事件的精确时间戳、参数和返回码。开启方法-- 开启10046跟踪level 12表示最详细包含等待事件和绑定变量 ALTER SESSION SET EVENTS 10046 trace name context forever, level 12; -- 执行你的慢SQL SELECT ... FROM orders WHERE ...; -- 关闭跟踪 ALTER SESSION SET EVENTS 10046 trace name context off;跟踪文件默认生成在USER_DUMP_DEST目录下文件名类似ora_12345.trc。用tkprof工具格式化$ tkprof ora_12345.trc output.txt explainscott/tigerorcloutput.txt里你会看到比DISPLAY_CURSOR详细10倍的信息。例如WAIT部分会精确到微秒WAIT #1: namdb file sequential read ela 123456 file#5 block#12345 blocks1 obj#78901 tim123456789012345ela123456就是这次IO等待了123毫秒。obj#78901是对象号可以用SELECT object_name FROM dba_objects WHERE object_id 78901查出是哪个表或索引。BINDS部分会显示每次执行时绑定变量的真实值帮你确认bind peeking绑定变量窥探是否导致了执行计划“固化”。10046跟踪是SQL优化的“显微镜”它能看到Oracle内核层面的每一个动作。但它的代价也很高生成的trace文件可能巨大对系统性能有影响所以只应在测试环境或生产环境短暂开启。我一般只在两种情况下用它一是SQL Monitor显示某个操作耗时异常但ALLSTATS里看不出原因二是怀疑是Oracle的Bug需要向Oracle Support提供证据。10046不是日常工具而是“手术刀”用好了能一击毙命用不好会伤及自身。掌握这四步诊断法——真实执行计划、SQL Monitor、AWR报告、10046跟踪——你就拥有了一个完整的Oracle SQL 优化工具箱。它们不是孤立的而是层层递进先用DISPLAY_CURSOR定性再用SQL Monitor定量接着用AWR看全局最后用10046做病理切片。这套方法我在过去十年里帮客户定位了上百个“索引走了还慢”的疑难杂症成功率超过95%。4. 实战优化策略从“治标”到“治本”的七种武器4.1 策略一索引列序重构——让等值查询驱动范围扫描这是最立竿见影的优化。核心思想是把WHERE条件中选择率最低NUM_DISTINCT最小、最常用于等值查询的列放在复合索引的最左边。我们以一个电商订单表为例orders表有status5种状态、order_type3种类型、order_date日期范围三列经常一起出现在WHERE中。统计信息显示SELECT column_name, num_distinct, density FROM user_tab_col_statistics WHERE table_name ORDERS AND column_name IN (STATUS, ORDER_TYPE, ORDER_DATE); -- STATUS: 5, 0.2 -- ORDER_TYPE: 3, 0.333 -- ORDER_DATE: 10000, 0.0001ORDER_DATE的density最小但它是范围查询不适合作为索引首列。STATUS和ORDER_TYPE都是等值查询STATUS的num_distinct更小53等等35所以ORDER_TYPE唯一值更少ORDER_TYPE的num_distinct是3STATUS是5所以ORDER_TYPE的选择率更低。因此索引应该以ORDER_TYPE开头。但如果业务查询模式是“查所有SHIPPED状态的订单”那么STATUS的过滤性就比ORDER_TYPE强得多。所以不能只看统计信息更要结合业务查询模式。假设80%的查询都是WHERE status SHIPPED那么STATUS就是事实上的“驱动列”。此时索引应为(status, order_type, order_date)。这样status SHIPPED能快速定位到索引的一个“小片区”然后在这个片区内再用order_type做二次筛选最后用order_date做范围扫描。执行计划会是高效的INDEX RANGE SCANRows预估精准。反之如果索引是(order_date, status)那么order_date范围扫描会先返回大量行再用status做FILTER效率低下。重构索引的步骤分析查询模式用AWR或V$SQL找出最频繁、最耗时的SQL提取它们的WHERE条件。评估列选择率查user_tab_col_statistics重点关注density和num_distinct。确定驱动列选择那个在最多查询中作为条件、且density最小的列。构建新索引CREATE INDEX idx_orders_driven ON orders (status, order_type, order_date);验证效果用DISPLAY_CURSOR对比新旧索引的Cost、Rows、LAST_ELAPSED_TIME。注意删除旧索引前务必用DBA_INDEX_USAGE视图确认它是否被任何SQL使用SELECT * FROM v$object_usage WHERE index_name OLD_IDX; -- 如果USED YES说明还有SQL在用它不能直接删。4.2 策略二物化连接视图MV——用空间换时间的终极方案当SQL涉及多表JOIN且连接键的数据分布极不均匀Skew导致HASH JOIN或NESTED LOOPS效率低下时物化视图Materialized View是SQL优化的“核武器”。它把复杂的JOIN结果预先计算好存成一张物理表并自动维护数据
返回列表