Oracle 索引完整指南
一、索引类型分类
Oracle 索引
│
├── 按结构分类
│ ├── B-Tree 索引(默认,最常用)
│ ├── Bitmap 索引(位图)
│ ├── Reverse Key 索引(反向键)
│ └── Hash 索引(仅 Cluster 中)
│
├── 按列数分类
│ ├── 单列索引
│ └── 复合索引(组合索引)
│
├── 按特殊用途分类
│ ├── Function-Based Index(函数索引)
│ ├── Unique Index(唯一索引)
│ ├── Domain Index(域索引,全文等)
│ ├── Invisible Index(不可见索引,11g+)
│ └── Virtual Index(虚拟索引,调优用)
│
└── 按物理存储分类
├── Local Index(局部分区索引)
├── Global Index(全局索引)
└── Global Partitioned Index(全局分区索引)
二、各类索引详解与示例
1. B-Tree 索引(默认)
最通用的索引类型,适合高基数列(distinct 值多)。
sql
-- 创建普通B-Tree索引
CREATE INDEX IDX_EMP_NAME ON EMPLOYEES(EMP_NAME);
-- 唯一索引
CREATE UNIQUE INDEX IDX_EMP_NO ON EMPLOYEES(EMP_NO);
-- 复合索引(最左前缀原则)
CREATE INDEX IDX_EMP_DEPT_HIRE ON EMPLOYEES(DEPT_ID, HIRE_DATE);
-- 在线创建(不锁表,推荐生产环境)
CREATE INDEX IDX_EMP_NAME ON EMPLOYEES(EMP_NAME) ONLINE;
-- 并行创建(大表加速)
CREATE INDEX IDX_EMP_NAME ON EMPLOYEES(EMP_NAME) PARALLEL 4 NOLOGGING;
-- 创建完成后改回串行+日志
ALTER INDEX IDX_EMP_NAME NOPARALLEL LOGGING;
适用场景:OLTP 系统,列值唯一性高(如主键、身份证号、邮箱)。
2. Bitmap 索引(位图索引)
为每个 distinct 值维护一个位图,适合低基数列。
sql
-- 创建位图索引
CREATE BITMAP INDEX IDX_EMP_GENDER ON EMPLOYEES(GENDER);
-- 位图连接索引(事实表与维度表的JOIN场景)
CREATE BITMAP INDEX IDX_BMJ_DEPT
ON SALES(D.DEPT_NAME)
FROM SALES S, DEPT D
WHERE S.DEPT_ID = D.DEPT_ID;
适用场景:数据仓库(DW)、性别/状态/省份等枚举列。
禁忌:
- ❌ OLTP 高并发更新(位图锁粒度大,会引发严重锁竞争)
- ❌ 列值唯一性高的字段
3. 函数索引(Function-Based Index)
对表达式或函数结果建索引。
sql
-- 大小写不敏感查询
CREATE INDEX IDX_EMP_UPNAME ON EMPLOYEES(UPPER(EMP_NAME));
-- 这样的查询会走索引:
SELECT * FROM EMPLOYEES WHERE UPPER(EMP_NAME) = 'ZHANG SAN';
-- 日期截取
CREATE INDEX IDX_ORD_MONTH ON ORDERS(TRUNC(ORDER_DATE, 'MM'));
-- NVL 函数索引
CREATE INDEX IDX_EMP_MGR ON EMPLOYEES(NVL(MGR_ID, -1));
-- 必须确保查询优化器能用:
ALTER SESSION SET QUERY_REWRITE_ENABLED = TRUE;
适用场景:查询条件中固定使用某种函数转换。
4. 反向键索引(Reverse Key)
将索引列字节反转后存储,分散热点。
sql
-- 创建反向键索引
CREATE INDEX IDX_ORD_ID_REV ON ORDERS(ORDER_ID) REVERSE;
-- 普通索引转反向
ALTER INDEX IDX_ORD_ID REBUILD REVERSE;
适用场景:
- 自增序列主键(避免索引右侧热块)
- RAC 环境减少 GC 缓存等待
禁忌 :❌ 不能用于范围查询(BETWEEN、<、> 都失效)
5. 不可见索引(Invisible Index)⭐
11g 引入,优化器忽略它,但仍维护数据,调优神器。
sql
-- 创建不可见索引(验证用)
CREATE INDEX IDX_EMP_TEST ON EMPLOYEES(EMP_NAME) INVISIBLE;
-- 让现有索引"下线"(不删除,仅不被使用)
ALTER INDEX IDX_EMP_NAME INVISIBLE;
-- 测试用某个索引(仅当前会话生效)
ALTER SESSION SET OPTIMIZER_USE_INVISIBLE_INDEXES = TRUE;
-- 验证 OK 后启用
ALTER INDEX IDX_EMP_NAME VISIBLE;
适用场景:
- 怀疑某索引无用,下线观察后再删除
- 测试新索引而不影响生产 SQL
6. 复合索引(关键:列顺序)
sql
-- 顺序很重要!
CREATE INDEX IDX_EMP_COMP ON EMPLOYEES(DEPT_ID, HIRE_DATE, SALARY);
-- ✅ 走索引(最左前缀)
SELECT * FROM EMPLOYEES WHERE DEPT_ID = 10;
SELECT * FROM EMPLOYEES WHERE DEPT_ID = 10 AND HIRE_DATE > SYSDATE-365;
-- ⚠️ 11g+ Index Skip Scan,前导列基数低时勉强可用
SELECT * FROM EMPLOYEES WHERE HIRE_DATE > SYSDATE-365;
-- ❌ 完全用不上索引
SELECT * FROM EMPLOYEES WHERE SALARY > 10000;
列顺序口诀:
- 等值条件在前,范围条件在后
- 基数高(区分度高)的列在前
- 常用过滤列在前
7. 分区索引
sql
-- Local 局部索引(每个分区一个索引段,推荐)
CREATE INDEX IDX_SALES_LOCAL ON SALES(SALE_DATE) LOCAL;
-- Global 全局索引(一个索引覆盖所有分区)
CREATE INDEX IDX_SALES_GLOBAL ON SALES(CUSTOMER_ID) GLOBAL;
-- Global 分区索引
CREATE INDEX IDX_SALES_GP ON SALES(CUSTOMER_ID)
GLOBAL PARTITION BY RANGE(CUSTOMER_ID) (
PARTITION P1 VALUES LESS THAN (10000),
PARTITION P2 VALUES LESS THAN (20000),
PARTITION P3 VALUES LESS THAN (MAXVALUE)
);
Local vs Global 选择:
| 特性 | Local 索引 | Global 索引 |
|---|---|---|
| 索引列 | 通常包含分区键 | 不含分区键 |
| 分区维护 | DROP 分区时索引自动清理 | 必须手动维护(UPDATE GLOBAL INDEXES) |
| 查询性能 | 分区裁剪友好 | 跨分区查询更快 |
| 运维成本 | 低 | 高 |
三、索引常用 DDL 操作
sql
-- ============== 创建相关 ==============
-- 指定表空间
CREATE INDEX IDX_NAME ON T1(C1) TABLESPACE TBS_IDX;
-- 在线、并行、不写日志(大表加速)
CREATE INDEX IDX_NAME ON T1(C1)
ONLINE PARALLEL 8 NOLOGGING;
-- 改回正常状态
ALTER INDEX IDX_NAME NOPARALLEL LOGGING;
-- ============== 维护相关 ==============
-- 重建(消除碎片)
ALTER INDEX IDX_NAME REBUILD;
ALTER INDEX IDX_NAME REBUILD ONLINE PARALLEL 4;
-- 移动表空间
ALTER INDEX IDX_NAME REBUILD TABLESPACE TBS_NEW;
-- 合并叶子块(轻量整理)
ALTER INDEX IDX_NAME COALESCE;
-- 收集统计信息
EXEC DBMS_STATS.GATHER_INDEX_STATS('SCOTT','IDX_NAME');
-- 启用/禁用
ALTER INDEX IDX_NAME UNUSABLE; -- 失效
ALTER INDEX IDX_NAME REBUILD; -- 重新可用
-- 改名
ALTER INDEX IDX_OLD RENAME TO IDX_NEW;
-- 删除
DROP INDEX IDX_NAME;
DROP INDEX IDX_NAME ONLINE; -- 在线删除
四、查询索引信息的常用 SQL
sql
-- 1. 查看某表所有索引
SELECT INDEX_NAME, INDEX_TYPE, UNIQUENESS,
STATUS, VISIBILITY, TABLESPACE_NAME
FROM USER_INDEXES
WHERE TABLE_NAME = 'EMPLOYEES';
-- 2. 查看索引列
SELECT INDEX_NAME, COLUMN_NAME, COLUMN_POSITION, DESCEND
FROM USER_IND_COLUMNS
WHERE TABLE_NAME = 'EMPLOYEES'
ORDER BY INDEX_NAME, COLUMN_POSITION;
-- 3. 函数索引的表达式
SELECT INDEX_NAME, COLUMN_EXPRESSION
FROM USER_IND_EXPRESSIONS
WHERE TABLE_NAME = 'EMPLOYEES';
-- 4. 索引大小
SELECT SEGMENT_NAME, BYTES/1024/1024 AS MB
FROM USER_SEGMENTS
WHERE SEGMENT_TYPE = 'INDEX';
-- 5. 索引使用情况监控
ALTER INDEX IDX_NAME MONITORING USAGE;
-- 等待一段时间后查询:
SELECT * FROM V$OBJECT_USAGE WHERE INDEX_NAME = 'IDX_NAME';
ALTER INDEX IDX_NAME NOMONITORING USAGE;
-- 12c+ 用新视图(更准确)
SELECT * FROM DBA_INDEX_USAGE
WHERE NAME = 'IDX_NAME';
五、索引使用判断(执行计划)
sql
-- 查看 SQL 是否走索引
EXPLAIN PLAN FOR
SELECT * FROM EMPLOYEES WHERE EMP_NAME = 'ZHANG SAN';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- 关键词解读:
-- TABLE ACCESS FULL → 全表扫描(未走索引)
-- INDEX RANGE SCAN → 索引范围扫描 ✅
-- INDEX UNIQUE SCAN → 唯一索引扫描 ✅
-- INDEX FULL SCAN → 索引全扫描
-- INDEX FAST FULL SCAN → 索引快速全扫描
-- INDEX SKIP SCAN → 跳跃扫描
-- TABLE ACCESS BY INDEX ROWID → 通过索引回表 ✅
六、索引失效的常见原因
sql
-- ❌ 1. 列上使用函数(除非有函数索引)
SELECT * FROM EMP WHERE UPPER(NAME) = 'TOM'; -- 失效
-- ✅ 解决:CREATE INDEX IDX_UPNAME ON EMP(UPPER(NAME));
-- ❌ 2. 隐式类型转换
-- ID 是 NUMBER 类型
SELECT * FROM EMP WHERE ID = '100'; -- 隐式转换可能失效
-- ✅ 解决:传入与列同类型的值
-- ❌ 3. NOT NULL / IS NULL(B-Tree 不索引 NULL)
SELECT * FROM EMP WHERE NAME IS NULL; -- 失效
-- ✅ 解决:复合索引或函数索引 NVL(NAME, 'NA')
-- ❌ 4. 前模糊查询
SELECT * FROM EMP WHERE NAME LIKE '%TOM'; -- 失效
-- ✅ 解决:反转列+反向LIKE,或全文索引
-- ❌ 5. != 或 <> 通常不走索引
SELECT * FROM EMP WHERE STATUS <> 'ACTIVE';
-- ✅ 解决:改写为 IN/NOT IN,或拆分查询
-- ❌ 6. OR 连接的多列且只有部分有索引
SELECT * FROM EMP WHERE NAME = 'TOM' OR AGE = 30;
-- ✅ 解决:UNION ALL 拆分,或两列都建索引
-- ❌ 7. 优化器认为成本更高(数据量小、统计信息过期)
-- ✅ 解决:EXEC DBMS_STATS.GATHER_TABLE_STATS(...);
七、索引最佳实践
✅ 推荐做法
| 实践 | 说明 |
|---|---|
| 主键/外键必建索引 | 外键不建会导致父表DML锁表 |
| WHERE/JOIN 高频列建索引 | 但需评估写入成本 |
| ONLINE 创建 | 不阻塞 DML(生产环境必备) |
| 大表用 PARALLEL + NOLOGGING | 加速创建,完成后改回 |
| 定期 GATHER_INDEX_STATS | 保持优化器决策准确 |
| 使用 INVISIBLE 灰度下线 | 删除前先观察 1~2 周 |
❌ 避免的做法
| 反模式 | 危害 |
|---|---|
| 一个表 10+ 个索引 | DML 性能严重下降 |
| 在低基数列建 B-Tree | 优化器倾向走全表 |
| 在高频更新列建 Bitmap | 锁竞争 + 索引膨胀 |
| 索引列顺序不合理 | 索引利用率低 |
| 从不重建/分析索引 | 碎片严重,统计信息失真 |
八、索引维护脚本(生产实用)
sql
-- 找出可能需要重建的索引(>20% 碎片)
SELECT INDEX_NAME,
BLOCKS,
DEL_LF_ROWS_LEN,
LF_ROWS_LEN,
ROUND(DEL_LF_ROWS_LEN/NULLIF(LF_ROWS_LEN,0)*100, 2) AS DEL_PCT
FROM (
SELECT * FROM INDEX_STATS -- 需先 ANALYZE INDEX ... VALIDATE STRUCTURE
);
-- 找出未使用的索引(开启监控后查询)
SELECT u.NAME, u.OWNER, u.TOTAL_ACCESS_COUNT, u.LAST_USED
FROM DBA_INDEX_USAGE u
WHERE u.TOTAL_ACCESS_COUNT = 0
AND u.OWNER NOT IN ('SYS','SYSTEM');
-- 找出冗余索引(前缀重复)
SELECT a.TABLE_NAME, a.INDEX_NAME AS REDUNDANT,
b.INDEX_NAME AS DOMINANT
FROM USER_IND_COLUMNS a, USER_IND_COLUMNS b
WHERE a.TABLE_NAME = b.TABLE_NAME
AND a.INDEX_NAME != b.INDEX_NAME
AND a.COLUMN_POSITION = 1
AND b.COLUMN_POSITION = 1
AND a.COLUMN_NAME = b.COLUMN_NAME
AND (SELECT COUNT(*) FROM USER_IND_COLUMNS WHERE INDEX_NAME = a.INDEX_NAME)
< (SELECT COUNT(*) FROM USER_IND_COLUMNS WHERE INDEX_NAME = b.INDEX_NAME);
一句话总结
索引是空间换时间,但绝非越多越好 :每个索引都会增加 DML 开销和存储成本。建索引前先用 EXPLAIN PLAN 验证是否被使用,建完后用 DBA_INDEX_USAGE 监控是否真的在工作,无用的索引应该果断 INVISIBLE 后再删除。
如果你有具体的业务场景或 SQL 性能问题,可以贴出来,我帮你分析适合什么类型的索引。