1. 为什么一个索引要讲透?——这不是DBA的专属课,而是每个写SQL的人绕不开的生存技能
“Oracle 索引详解(index)”这个标题看起来像教科书目录里的一节,但如果你在某次线上服务突然响应变慢、某张千万级订单表的查询从0.02秒飙到8秒、或者开发同事甩来一句“加个索引就好了”却没人能说清“为什么是这个字段、为什么用B树、为什么不用位图”,那你就会明白:索引不是数据库管理员(DBA)的黑箱工具,而是每一个和数据打交道的人必须亲手调试、亲手验证、亲手踩坑的底层逻辑。我做过6年Oracle核心系统性能优化,经手过金融、电信、政务三类高并发场景下的索引重构项目,最深的体会是:90%的SQL性能问题,根源不在SQL写法本身,而在索引设计是否匹配真实访问模式;而其中70%的索引失效,不是因为没建,而是因为建错了位置、选错了类型、或者被隐式转换悄悄绕过了。这篇文章不讲概念定义,不列官方文档参数,只讲我在生产环境里反复验证过的判断链路:什么时候该建索引?建在哪几个字段上?B树索引和函数索引到底差在哪?为什么有时候加了索引反而更慢?如何用一条EXPLAIN PLAN命令就看出执行计划是否真的走了索引?我会把一次典型的订单查询优化全过程拆解给你看——从慢SQL定位、索引候选字段分析、组合索引字段顺序推演、隐式转换陷阱识别,到最终压测对比数据。无论你是刚写完第一个SELECT * FROM user_tab_columns的新手,还是已经能熟练写PL/SQL但总在性能瓶颈前卡住的中级开发者,只要你每天要写SQL、要看执行计划、要对查询速度负责,这篇就是为你写的实操手册。
2. 索引不是“加速器”,而是“路径规划器”——理解B树结构才能避开最大认知误区
2.1 别再背“索引加快查询速度”了,先搞懂它到底在做什么
很多人把索引想象成给表加了个“快进键”,这是最大的误解。Oracle索引本质上是一套独立于表数据的有序导航结构,它的核心任务不是“让查询变快”,而是帮优化器快速定位数据物理位置,从而大幅减少需要扫描的数据块数量。关键点在于:索引本身不存完整行数据(除非是索引组织表IOT),它只存索引键值+对应行的ROWID(数据块地址+槽位号)。当执行SELECT order_id, amount FROM orders WHERE cust_id = 1001 AND status = 'SHIPPED'时,Oracle的执行流程其实是:
- 查找
cust_id + status组合索引的根节点(通常在内存中); - 沿B树逐层下探,找到满足条件的叶节点范围(比如cust_id=1001且status='SHIPPED'的所有键值);
- 从这些叶节点中取出所有匹配的ROWID;
- 根据ROWID直接跳转到数据块,读取对应行的
order_id和amount字段。
提示:这解释了为什么
SELECT *在有索引时可能比SELECT order_id, amount更慢——前者需要回表(table access by index rowid)读取整行,后者若索引已覆盖所需字段(即“覆盖索引”),则完全不需要访问表数据块。
B树结构决定了索引的三大硬约束:
- 有序性:键值按升序排列,支持范围扫描(
WHERE create_date BETWEEN '2023-01-01' AND '2023-12-31'); - 平衡性:所有叶节点到根节点的路径长度相同,保证任意键值查找最多只需3~4次IO(对亿级表也成立);
- 分支因子:每个节点可存数百个键值,极大压缩树高度(1000万行表,B树通常仅3层高)。
我曾在一个物流系统里见过反面案例:开发为order_no(唯一字符串)和create_time(时间戳)分别建了单列索引,但业务查询永远是WHERE order_no = ? AND create_time > ?。结果每次查询都走order_no索引(因等值匹配优先级高),拿到ROWID后再回表过滤create_time,实际扫描了数万行。后来我们删掉两个单列索引,新建组合索引(order_no, create_time),执行时间从1.2秒降到0.015秒——因为order_no等值匹配后,create_time自动成为范围扫描的连续区间,B树天然支持这种高效切片。
2.2 四种主流索引类型怎么选?别让类型错配毁掉所有努力
Oracle提供多种索引机制,选错类型等于在高速公路上开拖拉机。以下是我在生产环境高频使用的四种,按适用场景排序:
| 索引类型 | 适用场景 | 关键限制 | 实测性能特征 |
|---|---|---|---|
| B树索引(默认) | 高基数字段(如ID、手机号)、等值/范围查询、排序需求 | 不适合低基数字段(如性别、状态码);组合索引字段顺序敏感 | 单键查询最快;范围扫描稳定;叶节点双向链表支持ORDER BY |
| 位图索引(Bitmap) | 低基数字段(如status IN ('PENDING','SHIPPED','CANCELLED'))、星型模型事实表 | 严禁用于OLTP高并发更新场景(锁粒度是整个位图段);不支持LONG/LOB字段 | 对AND/OR多条件组合查询极快(位运算合并);空间占用仅为B树1/10 |
| 函数索引(Function-based) | 查询常对字段做函数操作(如UPPER(name) = 'ZHANG SAN'、TO_CHAR(create_date,'YYYYMM') = '202301') | 必须在查询中完全匹配函数表达式;需QUERY_REWRITE_ENABLED=TRUE | 让原本无法走索引的函数操作获得索引加速;避免全表扫描 |
| 反向键索引(Reverse Key) | 主键为序列生成的递增ID,且存在“热点块”争用(如大量INSERT集中在索引最右叶节点) | 无法支持范围扫描(BETWEEN、>等);ORDER BY失效 | 解决索引块争用,提升并发INSERT吞吐量;适用于日志类流水表 |
举个真实例子:某支付系统每秒产生2000笔交易,主键trans_id为SEQ_TRANS.NEXTVAL。上线初期发现V$SESSION_WAIT中enq: TX - index contention等待事件占比超40%,DBA诊断为索引最右叶节点争用。我们未改应用逻辑,仅将主键索引重建为REVERSE KEY,争用等待下降至0.3%,TPS提升27%。但注意:如果业务需要查trans_id > 1000000的最近交易,反向索引就完全失效——这就是类型选择必须匹配访问模式的铁律。
2.3 组合索引的字段顺序不是“随便排”,而是决定生死的数学问题
组合索引(A, B, C)的字段顺序,本质是定义B树的排序维度:先按A排序,A相同时按B排序,A+B相同时按C排序。这直接决定哪些查询能走索引、哪些只能部分走、哪些完全走不了。判断规则很简单:从左到右连续匹配,中间不能断。
假设索引为(cust_id, status, create_date):
- ✅
WHERE cust_id = 1001→ 走索引(匹配最左字段) - ✅
WHERE cust_id = 1001 AND status = 'SHIPPED'→ 走索引(连续匹配前两字段) - ✅
WHERE cust_id = 1001 AND status = 'SHIPPED' AND create_date > DATE'2023-01-01'→ 走索引(create_date作为范围条件,在等值字段后生效) - ⚠️
WHERE status = 'SHIPPED'→不走索引(跳过最左字段cust_id) - ⚠️
WHERE cust_id > 1000 AND create_date > DATE'2023-01-01'→仅cust_id部分走索引(create_date因cust_id是范围条件,无法利用B树有序性)
这里有个反直觉但关键的经验:等值查询字段放左,范围查询字段放右,高区分度字段优先。比如用户表有country_code(50个值)、city_name(5000个值)、last_login_date(每天变化)。若查询常为WHERE country_code = 'CN' AND city_name = 'BEIJING' AND last_login_date > SYSDATE-30,索引应建为(country_code, city_name, last_login_date)而非(last_login_date, country_code, city_name)——因为last_login_date范围太大(30天内可能占表70%数据),放在最左会导致索引选择率暴跌,优化器宁愿全表扫描。
我曾帮某电商重构商品搜索索引。原索引(category_id, brand_id, price)导致WHERE brand_id = 123 AND price BETWEEN 100 AND 500无法走索引。重排为(brand_id, category_id, price)后,该查询从全表扫描变为索引范围扫描,QPS从800提升至3200。根本原因:brand_id区分度(数千品牌)远高于category_id(百级类目),等值匹配时优先级更高。
3. 建索引前必须完成的三步验证——跳过任何一步都可能引发雪崩
3.1 第一步:确认SQL是否真的需要索引?先排除其他性能杀手
很多团队一遇到慢SQL就喊“加索引”,结果发现是更基础的问题。我坚持在建索引前必做三件事:
1. 检查绑定变量窥探(Bind Variable Peeking)是否导致执行计划固化
Oracle 10g后默认开启绑定变量窥探,即首次硬解析时用传入的实际值生成执行计划,并缓存复用。若首次传入cust_id = 1(该客户只有1条订单),优化器认为索引高效,生成索引访问计划;但后续传入cust_id = 1001(该客户有50万订单),计划仍强制走索引,导致大量回表IO。验证方法:
-- 查看该SQL的执行计划历史 SELECT sql_id, child_number, plan_hash_value, TO_CHAR(last_active_time,'YYYY-MM-DD HH24:MI') last_active, executions, buffer_gets/executions avg_lio FROM v$sql WHERE sql_text LIKE '%WHERE cust_id = %';若同一sql_id有多个child_number且plan_hash_value不同,说明存在绑定变量问题。解决方案:升级到Oracle 11g+启用自适应游标共享(Adaptive Cursor Sharing),或对关键SQL使用/*+ BIND_AWARE */提示。
2. 检查统计信息是否陈旧
索引有效性高度依赖统计信息。某次金融系统凌晨批处理后,orders表数据量从1000万增至1500万,但统计信息未更新。优化器仍按1000万估算,认为全表扫描成本低于索引访问,拒绝走新索引。用以下命令检查:
-- 查看表统计信息最后更新时间 SELECT table_name, num_rows, last_analyzed, stale_stats FROM dba_tab_statistics WHERE owner = 'SCOTT' AND table_name = 'ORDERS'; -- 强制收集(生产环境建议在低峰期) EXEC DBMS_STATS.GATHER_TABLE_STATS('SCOTT','ORDERS',CASCADE=>TRUE);3. 检查是否存在隐式类型转换
这是最隐蔽的索引失效原因。例如cust_id字段为VARCHAR2(20),但应用传参为数字1001,Oracle自动转为TO_NUMBER('1001'),导致索引无法匹配。验证方法:
-- 查看执行计划中的FILTER PREDICATES(非ACCESS PREDICATES) EXPLAIN PLAN FOR SELECT * FROM orders WHERE cust_id = 1001; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);若看到filter("CUST_ID"=1001)而非access("CUST_ID"=1001),说明发生了隐式转换。解决方案:应用层确保传参类型与字段一致,或在索引中显式转换(如建函数索引UPPER(cust_id))。
注意:不要迷信
AUTOTRACE,它可能显示错误的执行计划。务必用EXPLAIN PLAN+DBMS_XPLAN.DISPLAY,或直接查v$sql_plan获取真实执行路径。
3.2 第二步:用真实数据验证索引选择率——别信“理论上应该快”
索引是否有效,取决于选择率(Selectivity):满足条件的行数 / 总行数。选择率越低,索引价值越高。经验法则:选择率<5%时索引收益显著;>15%时优化器大概率放弃索引。计算公式:
选择率 = (满足条件的键值数量) / (索引中该字段的总键值数量)但实际中需考虑数据分布。例如status字段有3个值('PENDING','SHIPPED','CANCELLED'),理论选择率33%,但若95%订单是'SHIPPED',则查status='SHIPPED'的选择率高达95%,此时建索引毫无意义。
我常用以下脚本快速估算:
-- 估算cust_id=1001的选择率(需提前收集统计信息) SELECT ROUND((COUNT(*)/MAX(num_rows))*100,2) AS selectivity_pct, COUNT(*) AS estimated_rows FROM dba_tab_histograms h JOIN dba_tab_statistics s ON h.owner=s.owner AND h.table_name=s.table_name WHERE h.owner='SCOTT' AND h.table_name='ORDERS' AND h.column_name='CUST_ID' AND h.endpoint_actual_value = '1001';若无直方图,则用简单采样:
-- 对10000行随机采样估算 SELECT ROUND(AVG(CASE WHEN cust_id=1001 THEN 1 ELSE 0 END)*100,2) selectivity_pct FROM (SELECT cust_id FROM orders SAMPLE(0.1) WHERE ROWNUM<=10000);3.3 第三步:压力测试前必须做的三组对比实验
建索引不是“一键生效”,必须在测试环境模拟真实负载。我坚持做三组对比:
实验1:单SQL执行计划对比
-- 建索引前 EXPLAIN PLAN FOR SELECT * FROM orders WHERE cust_id=1001 AND status='SHIPPED'; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); -- 建索引后(立即执行,不刷新缓存) ALTER SYSTEM FLUSH SHARED_POOL; -- 清空执行计划缓存 EXPLAIN PLAN FOR SELECT * FROM orders WHERE cust_id=1001 AND status='SHIPPED'; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);重点观察:Operation是否从TABLE ACCESS FULL变为INDEX RANGE SCAN;Rows预估是否合理;Cost是否下降30%以上。
实验2:并发场景下的锁竞争
-- 模拟10个会话同时执行该查询 BEGIN FOR i IN 1..10 LOOP DBMS_SCHEDULER.CREATE_JOB( job_name => 'TEST_IDX_'||i, job_type => 'PLSQL_BLOCK', job_action => 'BEGIN SELECT /*+ INDEX(o IDX_CUST_STATUS) */ * FROM orders o WHERE cust_id=1001 AND status=''SHIPPED''; END;', start_date => SYSTIMESTAMP, repeat_interval => 'FREQ=SECONDLY; INTERVAL=1', end_date => SYSTIMESTAMP + INTERVAL '30' SECOND, enabled => TRUE ); END LOOP; END; / -- 监控锁等待 SELECT event, COUNT(*) FROM v$session_wait GROUP BY event ORDER BY 2 DESC;若出现大量enq: TX - row lock contention,说明索引设计引发热点(如反向索引未启用)。
实验3:DML性能影响基线测试
索引加速查询,但拖慢INSERT/UPDATE/DELETE。用真实业务SQL压测:
-- 测INSERT影响(插入10000行) SET TIMING ON INSERT INTO orders (order_id,cust_id,status,create_date) SELECT seq_order.NEXTVAL, 1001, 'NEW', SYSDATE FROM dual CONNECT BY LEVEL<=10000; -- 记录耗时,与无索引时对比经验数据:单列索引使INSERT慢15%~25%;组合索引每增加一个字段,DML开销增加8%~12%。若业务写多读少(如日志表),需谨慎权衡。
4. 索引维护的黑暗森林法则——不清理的索引比没有更危险
4.1 识别“僵尸索引”:那些从不被使用的索引正在 silently 吞噬你的IO
生产库中常有大量“名义上存在、实际上无用”的索引。它们不参与查询,却在每次DML时强制更新,消耗CPU和IO。Oracle 11g+提供索引监控功能,但需主动开启:
-- 开启监控(对特定索引) ALTER INDEX SCOTT.IDX_ORDERS_CUST_STATUS MONITORING USAGE; -- 等待业务运行1周后检查 SELECT index_name, used, start_monitoring, end_monitoring FROM v$object_usage WHERE index_name = 'IDX_ORDERS_CUST_STATUS'; -- 若used='NO',且start_monitoring早于1周前,可安全删除 DROP INDEX SCOTT.IDX_ORDERS_CUST_STATUS;注意:
v$object_usage只记录是否被使用,不记录使用频率。对高频表,建议结合AWR报告分析:-- 查看索引在AWR中的使用次数(需有AWR权限) SELECT object_name, statistic_name, value FROM dba_hist_seg_stat s JOIN dba_objects o ON s.obj# = o.object_id WHERE o.object_type = 'INDEX' AND o.owner = 'SCOTT' AND s.snap_id IN (SELECT snap_id FROM dba_hist_snapshot WHERE begin_interval_time > SYSDATE-7) ORDER BY value DESC;
4.2 处理“碎片化索引”:B树分裂后的性能衰减不可逆
B树索引随DML持续分裂,叶节点变得稀疏,导致相同数据量需更多IO读取。判断标准:
- 逻辑读(buffer gets)持续上升:同SQL执行逻辑读比建索引初增高20%以上;
- 索引块利用率低:
ANALYZE INDEX ... VALIDATE STRUCTURE后查INDEX_STATS;
-- 分析索引结构 ANALYZE INDEX SCOTT.IDX_ORDERS_CUST_STATUS VALIDATE STRUCTURE; -- 查看碎片指标 SELECT name, height, lf_rows, lf_blks, ROUND((del_lf_rows/lf_rows)*100,2) del_pct, ROUND((lf_rows_len/(lf_blks*8192))*100,2) pct_used FROM index_stats WHERE name = 'IDX_ORDERS_CUST_STATUS';关键指标解读:
height > 4:树过高,需重建;del_pct > 20%:删除行过多,空间未回收;pct_used < 65%:叶节点填充率低,严重碎片化;
修复方案:
- 在线重建(推荐):
ALTER INDEX SCOTT.IDX_ORDERS_CUST_STATUS REBUILD ONLINE; - 降低PCTFREE(谨慎):
ALTER INDEX ... REBUILD PCTFREE 5;(默认10%,对只读索引可设更低); - 分区索引(超大表):
CREATE INDEX ... LOCAL;(按时间分区,避免全局索引维护开销);
4.3 应对“隐式失效”:那些让你以为索引还在工作的假象
即使索引存在且被调用,也可能因以下原因实效:
1. 统计信息过期导致优化器误判
某次升级后,orders表统计信息停留在3个月前,而新增了200万订单。优化器估算cust_id=1001返回100行,实际返回5万行,仍选择索引访问,造成大量回表。解决方案:
- 对DML频繁的表,设置自动收集:
EXEC DBMS_STATS.SET_TABLE_PREFS('SCOTT','ORDERS','INCREMENTAL','TRUE'); EXEC DBMS_STATS.SET_TABLE_PREFS('SCOTT','ORDERS','STALE_PERCENT','5'); - 每日凌晨自动收集:
BEGIN DBMS_SCHEDULER.CREATE_JOB( job_name => 'GATHER_STATS_ORDERS', job_type => 'PLSQL_BLOCK', job_action => 'BEGIN DBMS_STATS.GATHER_TABLE_STATS(''SCOTT'',''ORDERS'',CASCADE=>TRUE); END;', start_date => TRUNC(SYSDATE)+1/24, -- 凌晨1点 repeat_interval => 'FREQ=DAILY', enabled => TRUE ); END;
2. 组合索引字段顺序与查询模式错配
如前所述,(A,B)索引无法支持WHERE B=1。但更隐蔽的是:当查询为WHERE A=1 OR B=2时,即使A和B都有索引,Oracle 11g前无法合并索引(Index Merge),必须全表扫描。12c+虽支持,但成本极高。正确做法:
- 对
OR条件,建包含所有字段的组合索引; - 或改写为
UNION ALL(需确保无重复数据):SELECT * FROM orders WHERE cust_id=1001 UNION ALL SELECT * FROM orders WHERE status='SHIPPED' AND cust_id<>1001;
3. 函数索引的表达式不完全匹配
建了FUNCTION-BASED INDEX UPPER(name),但查询写成WHERE UPPER(name) LIKE 'ZHANG%'。注意:LIKE在函数索引中仅当通配符在右侧时才有效('ZHANG%'可走索引,'%ZHANG'不可)。验证方法:
EXPLAIN PLAN FOR SELECT * FROM customers WHERE UPPER(name) LIKE 'ZHANG%'; -- 若显示INDEX RANGE SCAN,说明有效;若为FULL SCAN,则表达式不匹配。5. 真实世界中的索引优化全景图——从一张订单表的十年演化说起
5.1 阶段一:初创期(10万行,单实例)——索引够用就行
某SaaS电商系统上线初期,订单表orders仅10万行,字段:order_id(PK), cust_id, status, create_date, amount。
- 初始索引:主键索引
PK_ORDERS(order_id)+ 单列索引IDX_ORDERS_CUST_ID(cust_id)。 - 问题暴露:运营部门查“某客户所有订单”很快,但查“某状态所有订单”(
WHERE status='SHIPPED')需2秒。 - 解决:建单列索引
IDX_ORDERS_STATUS(status)。因status仅3个值,选择率高,但数据量小,全表扫描成本也不高,索引效果有限。 - 教训:低基数字段在小表上建索引收益不大,优先保障高区分度字段。
5.2 阶段二:成长期(500万行,读写分离)——组合索引成为性能支柱
用户量增长,orders达500万行,新增字段last_update_time,业务查询变为:
WHERE cust_id = ? AND status IN ('SHIPPED','DELIVERED')WHERE create_date BETWEEN ? AND ? AND status = ?WHERE cust_id = ? ORDER BY last_update_time DESC
重构动作:
- 删除
IDX_ORDERS_CUST_ID和IDX_ORDERS_STATUS; - 新建组合索引
IDX_ORDERS_CUST_STATUS(cust_id, status, create_date); - 新建函数索引
IDX_ORDERS_CUST_LUT(cust_id, last_update_time)(支持ORDER BY); - 为时间范围查询建
IDX_ORDERS_DATE_STATUS(create_date, status);
效果:核心查询平均响应时间从1.8秒降至0.04秒,数据库CPU使用率下降35%。
关键洞察:组合索引必须覆盖最频繁的查询模式,且字段顺序严格按“等值→范围→排序”排列。
5.3 阶段三:成熟期(3000万行,分库分表)——索引策略升维为架构决策
订单量突破3000万,单库压力大,实施分库分表:按cust_id哈希分16库,每库按create_date月分区。此时索引策略发生质变:
- 全局索引失效:跨库查询无法使用单库索引;
- 分区裁剪成为新索引:
WHERE create_date >= '2023-01-01'自动定位到2023年1月及之后分区; - 本地索引(LOCAL INDEX)成为标配:每个分区独立维护索引,避免全局索引维护开销;
- 新增位图索引:在宽表
orders_fact(星型模型)上为status,payment_method等低基数字段建位图索引,支撑BI多维分析;
典型SQL改造:
-- 改造前(全库扫描) SELECT SUM(amount) FROM orders WHERE status='SHIPPED' AND create_date > DATE'2023-06-01'; -- 改造后(分区裁剪+本地索引) SELECT SUM(amount) FROM orders PARTITION (ORDERS_202306) WHERE status='SHIPPED'; -- 自动走本地位图索引5.4 阶段四:稳定期(1亿行,混合云)——索引治理进入自动化时代
系统接入混合云,部分冷数据归档至对象存储,热数据保留在Oracle。此时索引管理重心转向:
- 自动索引(Auto Indexing):Oracle 19c+开启自动索引,由AI引擎分析AWR和SQL Tuning Set,自动创建/验证/删除索引;
- 索引使用率监控大屏:基于
v$segment_statistics构建实时看板,标记7天未使用的索引为“待评估”; - 索引健康度评分:综合碎片率、选择率、DML影响、缓存命中率,生成0-100分健康度,低于60分自动告警;
我个人在实际运维中发现:自动索引在OLAP场景效果显著(如报表查询),但在OLTP高频事务中需人工审核。曾有自动索引为
UPDATE orders SET status='PROCESSED' WHERE order_id=?创建了status单列索引,导致所有UPDATE语句额外维护索引,TPS下降18%。因此,我的原则是:自动索引作为辅助发现工具,最终决策权必须在人手中。
6. 常见问题与排查技巧实录——那些文档不会写的血泪经验
6.1 “明明建了索引,为什么执行计划还是全表扫描?”——五步定位法
这是最高频问题。按此顺序排查,95%可定位:
步骤1:确认索引状态是否VALID
SELECT index_name, status, funcidx_status FROM dba_indexes WHERE table_name = 'ORDERS' AND owner = 'SCOTT';若status='UNUSABLE',索引已失效,需ALTER INDEX ... REBUILD。
步骤2:检查WHERE条件是否触发隐式转换
如前文所述,用EXPLAIN PLAN看filtervsaccess。常见陷阱:
- 字符串字段用数字比较:
WHERE cust_id = 1001(cust_id为VARCHAR2); - 日期字段用字符串:
WHERE create_date = '2023-01-01'(未用TO_DATE); NVL函数破坏索引:WHERE NVL(status,'PENDING') = 'SHIPPED';
步骤3:验证统计信息是否最新
SELECT last_analyzed, num_rows, sample_size FROM dba_tables WHERE table_name = 'ORDERS' AND owner = 'SCOTT';若last_analyzed超过3天,立即收集。
步骤4:检查组合索引字段顺序是否匹配
用dba_ind_columns确认索引字段顺序:
SELECT column_name, column_position FROM dba_ind_columns WHERE index_name = 'IDX_ORDERS_CUST_STATUS' AND table_name = 'ORDERS' ORDER BY column_position;确保查询条件从左到右连续。
步骤5:排除绑定变量窥探干扰
查v$sql中该SQL的多个子游标:
SELECT sql_id, child_number, plan_hash_value, executions, buffer_gets/executions avg_lio, is_bind_sensitive, is_bind_aware FROM v$sql WHERE sql_text LIKE '%WHERE cust_id = %';若is_bind_aware='YES'但avg_lio波动大,说明存在自适应问题,需加BIND_AWARE提示或升级优化器。
6.2 “加了索引,查询变快了,但INSERT变慢了10倍!”——DML性能暴雷应对指南
索引DML开销是线性的:每建一个索引,INSERT/UPDATE/DELETE就要多一次索引块修改。当出现DML性能骤降:
紧急措施:
- 立即查
v$session_event,确认是否db file sequential read或enq: TX等待飙升; - 临时禁用索引(非删除):
ALTER INDEX ... UNUSABLE;(后续可REBUILD恢复);
根因分析:
- 索引字段过多:如组合索引
(a,b,c,d,e),每次INSERT需更新5个键值; - 索引列含LOB/长文本:
VARCHAR2(4000)字段建索引,叶节点膨胀; - 反向索引缺失:递增主键导致索引右倾,INSERT争用;
优化方案:
- 删除冗余索引:用
v$object_usage确认未使用索引; - 缩短索引列:对
VARCHAR2字段用前缀索引(Oracle 12c+支持):CREATE INDEX idx_orders_cust_name ON orders (SUBSTR(cust_name,1,20)); - 对高并发INSERT表,主键索引改用
REVERSE KEY;
6.3 “索引碎片化了,REBUILD ONLINE卡住怎么办?”——在线重建的避坑清单
REBUILD ONLINE可能因以下原因卡住:
| 场景 | 表现 | 解决方案 |
|---|---|---|
| DML阻塞 | v$session_blockers显示被其他会话阻塞 | 查v$locked_object,杀掉长事务会话 |
| 空间不足 | ORA-01652: unable to extend temp segment | 扩充TEMP表空间,或指定TABLESPACE参数 |
| 并行度失控 | CPU 100%,影响业务 | ALTER SESSION DISABLE PARALLEL DML;后重建 |
| 锁升级失败 | ORA-00054: resource busy | 用DBMS_LOCK.SLEEP(1)循环重试,或选择低峰期 |
安全重建脚本模板:
-- 1. 检查索引大小和碎片 ANALYZE INDEX SCOTT.IDX_ORDERS_CUST_STATUS VALIDATE STRUCTURE; SELECT name, height, lf_blks, del_lf_rows FROM index_stats; -- 2. 在低峰期执行(带超时) BEGIN DBMS_ADVISOR.TUNE_MVIEW('SCOTT', 'IDX_ORDERS_CUST_STATUS', 'ALTER INDEX SCOTT.IDX_ORDERS_CUST_STATUS REBUILD ONLINE TABLESPACE IDX_TBS PCTFREE 5'); EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('Rebuild failed: '||SQLERRM); END; -- 3. 验证重建结果 SELECT index_name, status FROM dba_indexes WHERE index_name = 'IDX_ORDERS_CUST_STATUS';6.4 索引优化速查表:什么情况下绝对不要建索引?
根据十年实战,总结出以下“红线场景”,违反任一条件都应放弃建索引:
| 场景 | 原因 | 替代方案 |
|---|---|---|
| 表数据量 < 1万行 | 全表扫描成本低于索引访问(B树深度+回表IO) | 保持无索引,依赖全表扫描 |
| **字段选择率 > 25% |