首页 > 数据库 > 数据库开发技术 > 论索引影响性能的一面④ 索引失踪之谜【上】
往期回顾
作为数据库的守护者,我们最信赖的伙伴莫过于“索引”。它如同一位效率超群的图书管理员,能于浩如烟海的数据中,精准地为我们取出所需的那一页。但你是否也曾遇到过这样的“灵异事件”:明明为它精心创建了索引,性能却依旧惨不忍睹?执行计划里,那个熟悉的身影消失不见,取而代之的是令人绝望的“全表扫描”(TABLE ACCESS FULL)。
索引失踪了,到底去哪儿了?
这并非灵异故事,而是一系列潜藏在日常操作中的“性能悬案”。今天,我们将化身侦探,深入四个经典的“案发现场”,揭开索引失踪的秘密。

案情简介:
江湖传言,“LIKE一出,索引必废”。这个说法流传甚广,让许多开发者谈%色变。但这究竟是真相,还是误解?让我们用证据说话。
现场勘查与证据收集:
首先,我们准备“案发现场”——一张T表,并在OBJECT_NAME列上建立索引。
drop table t purge; create table t as select * from dba_objects where object_id is not null; set autotrace off update t set object_id=rownum; update t set object_name='AAALJB' where object_id=8; update t set object_name='LJBAAA' where object_id=10; commit; create index idx_object_name on t(object_name); SET AUTOTRACE ON SET LINESIZE 1000
select object_name,object_id from t where object_name like 'LJB%'; OBJECT_NAME OBJECT_ID ------------------------------------------------------------------------------------- LJBAAA 10 LJB_TMP_SESSION 72521 LJB_TMP_SESSION 72910 LJB_TMP_TRANSACTION 72522 LJB_TMP_TRANSACTION 72911 已选择5行。 执行计划 -------------------------------------------------------------------------------------- | Id |Operation | Name |Rows | Bytes | Cost (%CPU)| Time | | 0 |SELECT STATEMENT | | 5| 395 | 6 (0)| 00:00:01 | | 1 | TABLE ACCESS BY INDEX ROWID| T | 5| 395 | 6 (0)| 00:00:01 | |* 2 | INDEX RANGE SCAN | IDX_OBJECT_NAME | 5| | 3 (0)| 00:00:01 | -------------------------------------------------------------------------------------- 统计信息 ---------------------------------------------------------- 0 recursive calls 0 db block gets 9 consistent gets 0 physical reads 0 redo size 602 bytes sent via SQL*Net to client 415 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 5 rows processed
破案分析:
索引健在,且表现优异! INDEX RANGE SCAN(索引范围扫描)赫然在列。like 'LJB%'这种前缀匹配,Oracle可以精准定位到索引树中“LJB”的位置开始扫描,当然能用到索引。
select object_name,object_id from t where object_name like '%LJB%'; 执行计划 -------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | | 0 | SELECT STATEMENT | | 12 | 948 | 292 (1)| 00:00:04 | |* 1 | TABLE ACCESS FULL| T | 12 | 948 | 292 (1)| 00:00:04 | -------------------------------------------------------------------------------------- 统计信息 ---------------------------------------------------------- 0 recursive calls 0 db block gets 1049 consistent gets 0 physical reads 0 redo size 653 bytes sent via SQL*Net to client 415 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 6 rows processed
破案分析:
索引“人间蒸发”! TABLE ACCESS FULL出现了。因为前导%的存在,Oracle不知道检索何时能停下来,只能放弃索引,选择全表扫描。
正常情况下,后缀匹配与模糊匹配原理相似,无法使用常规索引。但我们可以通过reverse函数和函数索引,巧妙破局。
create index idx_reverse_objname on t(reverse(object_name));
set autotrace on
select object_name,object_id from t where reverse(object_name) like reverse('%LJB');
OBJECT_NAME OBJECT_ID
-------------------------------------------------------------------------------------
AAALJB 8
执行计划
--------------------------------------------------------------------------------------
|Id |Operation |Name |Rows |Bytes |Cost (%CPU)| Time |
| 0|SELECT STATEMENT | | 3596| 509K| 290 (0)| 00:00:04 |
| 1| TABLE ACCESS BY INDEX ROWID|T | 3596| 509K| 290 (0)| 00:00:04 |
|* 2| INDEX RANGE SCAN |IDX_REVERSE_OBJNAME| 647| | 6 (0)| 00:00:01 |
--------------------------------------------------------------------------------------
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
5 consistent gets
0 physical reads
0 redo size
496 bytes sent via SQL*Net to client
415 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

案情简介:
这是一起发生在某大型制造业系统的真实“血案”。一个看似无害的ALTER TABLE ... MOVE操作,竟导致系统核心查询性能骤降,业务近乎瘫痪。
现场勘查与证据收集:
drop table t_p cascade constraints purge; drop table t_c cascade constraints purge; CREATE TABLE T_P (ID NUMBER, NAME VARCHAR2(30)); ALTER TABLE T_P ADD CONSTRAINT T_P_ID_PK PRIMARY KEY (ID); CREATE TABLE T_C (ID NUMBER, FID NUMBER, NAME VARCHAR2(30)); ALTER TABLE T_C ADD CONSTRAINT FK_T_C FOREIGN KEY (FID) REFERENCES T_P (ID); INSERT INTO T_P SELECT ROWNUM, TABLE_NAME FROM ALL_TABLES; INSERT INTO T_C SELECT ROWNUM, MOD(ROWNUM, 1000) + 1, OBJECT_NAME FROM ALL_OBJECTS; COMMIT; CREATE INDEX IND_T_C_FID ON T_C (FID); -- 检查索引初始状态 SELECT TABLE_NAME,INDEX_NAME,STATUS FROM USER_INDEXES WHERE INDEX_NAME='IND_T_C_FID'; TABLE_NAME INDEX_NAME STATUS -------------------- -------------------- -------- T_C IND_T_C_FID VALID -- 执行一个“不小心”的操作 ALTER TABLE T_C MOVE; -- 再次检查索引状态 SELECT TABLE_NAME,INDEX_NAME,STATUS FROM USER_INDEXES WHERE INDEX_NAME='IND_T_C_FID'; TABLE_NAME INDEX_NAME STATUS -------------------- -------------------- -------- T_C IND_T_C_FID UNUSABLE
破案分析:
凶手正是MOVE命令! MOVE操作改变了表中所有行的ROWID,而索引中存储的恰恰是旧的ROWID。Oracle为了避免数据错乱,将该索引标记为UNUSABLE。这个“暗杀”是无声的,操作本身不报错,但索引已悄然“死亡”。
受害者(索引失效后的查询):
SET LINESIZE 1000
SET AUTOTRACE TRACEONLY
SELECT A.ID, A.NAME, B.NAME FROM T_P A, T_C B WHERE A.ID = B.FID AND A.ID = 880;
执行计划
--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
| 0 | SELECT STATEMENT | | 25 | 1500 | 111 (1)| 00:00:02 |
| 1 | NESTED LOOPS | | 25 | 1500 | 111 (1)| 00:00:02 |
| 2 | TABLE ACCESS BY INDEX ROWID| T_P | 1 | 30 | 0 (0)| 00:00:01 |
|* 3 | INDEX UNIQUE SCAN | T_P_ID_PK | 1 | | 0 (0)| 00:00:01 |
|* 4 | TABLE ACCESS FULL | T_C | 25 | 750 | 111 (1)| 00:00:02 |
--------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("A"."ID"=880)
4 - filter("B"."FID"=880)
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
394 consistent gets
0 physical reads
0 redo size
3602 bytes sent via SQL*Net to client
459 bytes received via SQL*Net from client
6 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
72 rows processed
对T_C表执行了TABLE ACCESS FULL,逻辑读高达394。
“救治”方案与效果:
ALTER INDEX IND_T_C_FID REBUILD;
-- 再次执行查询
SELECT A.ID, A.NAME, B.NAME FROM T_P A, T_C B WHERE A.ID = B.FID AND A.ID = 880;
执行计划
--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
| 0 | SELECT STATEMENT | | 72 | 4320 | 87 (0)| 00:00:02 |
| 1 | NESTED LOOPS | | 72 | 4320 | 87 (0)| 00:00:02 |
| 2 | TABLE ACCESS BY INDEX ROWID| T_P | 1 | 30 | 0 (0)| 00:00:01 |
|* 3 | INDEX UNIQUE SCAN | T_P_ID_PK | 1 | | 0 (0)| 00:00:01 |
| 4 | TABLE ACCESS BY INDEX ROWID| T_C | 72 | 2160 | 87 (0)| 00:00:02 |
|* 5 | INDEX RANGE SCAN | IND_T_C_FID | 72 | | 1 (0)| 00:00:01 |
--------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("A"."ID"=880)
5 - access("B"."FID"=880)
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
81 consistent gets
0 physical reads
0 redo size
3602 bytes sent via SQL*Net to client
459 bytes received via SQL*Net from client
6 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
72 rows processed
侦探笔记:
ALTER TABLE ... MOVE是一个极其危险的操作。操作规范:在执行MOVE后,必须立即REBUILD该表上的所有索引。

案情简介:
如果说上一个案例是MOVE命令造成的直接性能伤害,那么这个案例,则是它更隐蔽、更阴险的“并发症”——锁等待。
现场勘查与证据收集:
-- 准备环境 drop table t_p cascade constraints purge; drop table t_c cascade constraints purge; CREATE TABLE T_P (ID NUMBER, NAME VARCHAR2(30)); ALTER TABLE T_P ADD CONSTRAINT T_P_ID_PK PRIMARY KEY (ID); CREATE TABLE T_C (ID NUMBER, FID NUMBER, NAME VARCHAR2(30)); ALTER TABLE T_C ADD CONSTRAINT FK_T_C FOREIGN KEY (FID) REFERENCES T_P (ID); INSERT INTO T_P SELECT ROWNUM, TABLE_NAME FROM ALL_TABLES; INSERT INTO T_C SELECT ROWNUM, MOD(ROWNUM, 1000) + 1, OBJECT_NAME FROM ALL_OBJECTS; COMMIT; -- 注意,这里没有给外键列T_C(FID)创建索引 -- 以下操作导致外键相关的索引失效(此处原文为move,但实际场景是未创建索引) -- ALTER TABLE T_C MOVE;
并发测试:
-- 首先开启会话1 select sid from v$mystat where rownum=1; DELETE T_C WHERE ID = 2; -- 接下来开启会话2,也就是开启一个新的连接 select sid from v$mystat where rownum=1; DELETE T_P WHERE ID = 2000; -- 居然发现卡住半天不动了!
破案分析:
这起“幽灵锁”的根源,在于外键约束的底层工作机制。当你操作主表(如DELETE T_P)时,Oracle必须确保子表中没有任何记录引用你将要删除的主表记录。
索引有效时: Oracle会通过外键列上的索引,快速检查子表T_C中是否存在FID = 2000的记录。
索引失效(或不存在)时: Oracle无法快速检查,只能在子表T_C上施加一个全表锁,然后慢慢地进行全表扫描。当会话1持有T_C的行级锁时,会话2想要获取全表锁自然会被阻塞。
侦探笔记:
外键列上必须建立索引,并保证其永远有效! 这不仅是查询性能的需要,更是保证高并发环境下系统稳定性的“生命线”。
案情简介:
既然MOVE如此危险,那我们用更现代的SHRINK命令来收缩表空间,总该安全了吧?它确实能避免索引失效,但新的问题又来了:索引明明VALID,优化器为何依然弃之不用?
现场勘查与证据收集:
drop table t purge; create table t as select * from dba_objects where object_id is not null; alter table t modify object_id not null; set autotrace off insert into t select * from t; insert into t select * from t; commit; create index idx_object_id on t(object_id); set linesize 1000 set autotrace on select count(*) from t; set autotrace off delete from t where rownum<=292000; commit; set autotrace on alter table t enable row movement; alter table t shrink space; select count(*) from t; 执行计划 ------------------------------------------------------------------------------------ Plan hash value: 2966233522 | Id | Operation | Name | Rows | Cost (%CPU)| Time | | 0 | SELECT STATEMENT | | 1 | 5 (0)| 00:00:01 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | TABLE ACCESS FULL| T | 740 | 5 (0)| 00:00:01 | ------------------------------------------------------------------------------------ 统计信息 ---------------------------------------------------------- 0 recursive calls 0 db block gets 15 consistent gets 0 physical reads 0 redo size 424 bytes sent via SQL*Net to client 415 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed
破案分析:
索引再次“失踪”!它明明状态VALID,为何被优化器无情抛弃?我们强制让它走索引(使用hint)看看。
select /*+index(t)*/ count(*) from t; 执行计划 -------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Cost (%CPU)| Time | | 0 | SELECT STATEMENT | | 1 | 675 (1)| 00:00:09 | | 1 | SORT AGGREGATE | | 1 | | | | 2 | INDEX FULL SCAN| IDX_OBJECT_ID | 740 | 675 (1)| 00:00:09 | -------------------------------------------------------------------------------------- 统计信息 ---------------------------------------------------------- 0 recursive calls 0 db block gets 649 consistent gets 0 physical reads 0 redo size 424 bytes sent via SQL*Net to client 415 bytes received via SQL*Net from client 2 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 1 rows processed
真相大白!强制走索引的成本(Cost=675,逻辑读=649)远高于走全表扫描(Cost=5,逻辑读=15)。原因在于:SHRINK操作虽然收缩了表,但并未有效地收缩索引段的空间。 大量DELETE操作在索引中留下了许多“空洞”,导致索引变得稀疏而“臃肿”,扫描它反而得不偿失。
侦探笔记:
索引的VALID状态,仅仅是它的“准入证”,并不代表它的“健康证”。一个因大量删除而变得臃肿的索引,即便有效,也可能成为性能的拖累。在这种情况下,ALTER INDEX ... REBUILD 才是整理索引碎片、恢复其紧凑结构和高性能的“特效药”。
今天的调查到此告一段落。我们揭开了四个导致索引“失踪”或“失效”的经典悬案:
伪装者LIKE: 被前导通配符%所迷惑。
刺客MOVE: 无声地让索引状态变为UNUSABLE。
帮凶“无索引外键”: 在并发操作中制造“幽灵锁”。
“亚健康”的SHRINK后遗症: 索引虽在,却因臃肿而被优化器嫌弃。
然而,索引失踪之谜远未结束。在下一集中,我们将继续追踪另外四位“嫌疑人”,它们同样狡猾,同样致命。敬请期待《索引失踪之谜(下)》。
未完待续...
公众号:收获不止数据库
微信:ljbyxl1213
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/9091385/viewspace-3088594/ ,如需转载,请注明出处,否则将追究法律责任。