很多人对索引的理解停留在"加个索引查询就变快",但真正在项目中,却常常踩中"索引无效""过度索引"的坑。今天我们不聊复杂的底层源码,只讲能直接落地的索引知识:从是什么、为什么有用,到怎么用、怎么避坑,帮你彻底吃透数据库索引,让你的查询效率翻倍。
一、索引到底是什么?
其实索引一点都不抽象,我们可以用生活中的"书籍目录"来类比:
你想在一本几百页的编程书中找到"Oracle查询优化"的内容,有两种方式:
- 一是从第一页逐页翻找,运气好可能很快找到,运气差要翻完整本书(对应数据库的"全表扫描");
- 二是先看目录,找到对应章节的页码,直接翻到那一页(对应"通过索引查询")。
数据库中的索引,本质就是为数据表建立的"目录" ,它是一个独立的、有序的数据结构(常见的是B+树),存储着数据表中"索引列"的值和对应的行数据地址,目的是帮数据库快速定位到目标数据,避免全表扫描带来的性能消耗。
这里有一个关键误区:索引不是越多越好 。就像书籍的目录,如果目录太详细(每句话都做目录),反而会占用大量篇幅,查找目录的时间比翻书还长;数据库中过多的索引,会增加数据插入、更新、删除的开销(因为每次修改数据,都要同步更新所有相关索引)。
二、为什么索引能让查询变快?
要理解索引的效率,首先要知道数据库的"全表扫描"到底有多慢。假设我们有一张用户表(user),包含100万条数据,表结构如下:
sql
CREATE TABLE "USER" (
"ID" NUMBER(11) NOT NULL,
"USERNAME" VARCHAR2(50) NOT NULL,
"PHONE" VARCHAR2(20) NOT NULL,
"AGE" NUMBER(11),
CONSTRAINT "PK_USER_ID" PRIMARY KEY ("ID")
) ;
如果我们执行SQL:SELECT * FROM "USER" WHERE PHONE = '13800138000',没有给PHONE列加索引时,数据库会逐行遍历这100万条数据,对比每一行的PHONE值,直到找到匹配的记录------这个过程就像翻完一整本书,时间复杂度是O(n),数据量越大,越慢 。
如果我们给PHONE列加了索引:CREATE INDEX IDX_USER_PHONE ON "USER"(PHONE);,数据库会为PHONE列建立一个B+树索引结构,这个结构有两个核心特点:
- 有序性:索引列(phone)的值会按升序/降序排列,数据库可以通过二分查找快速定位到目标值,时间复杂度降至O(log n)。100万条数据,二分查找只需约20次就能找到目标,比全表扫描快几万倍。
- 指向性:索引节点中存储的不是完整的行数据,而是行数据的地址(类似书籍目录的页码),找到索引节点后,就能通过地址直接获取对应的行数据,无需再遍历表。
Oracle数据库中,无论是默认的B树索引(最常用),还是针对特定场景的位图索引、函数索引,核心存储结构均基于B+树变体------因为B+树更适合磁盘存储,能减少磁盘IO次数(磁盘IO是数据库查询的主要性能瓶颈),这一点不用深究,记住"Oracle主流索引均基于B+树"即可。
三、常用索引类型
数据库索引有很多类型,按功能可分为4种常用类型,不同类型适用不同场景,选错了不仅没用,还会拖慢性能。我们结合实战场景,一个个讲清楚:
1. 主键索引(Primary Key)------ 必选,唯一且非空
主键索引是数据表的"唯一标识",每张表只能有一个主键索引 ,索引列的值必须唯一、非空(比如user表的id列)。
特点 :Oracle中,主键索引默认是B树索引,且与表数据存储在指定的表空间中(可单独指定索引表空间),Oracle没有"InnoDB聚簇索引"的概念,但主键索引因唯一且非空,查询时无需额外关联,效率同样是所有索引中最高的。
场景:用于唯一标识一条数据,比如用户id、订单id,几乎所有表都必须有主键索引。
2. 唯一索引(Unique Index)------ 唯一,可空
唯一索引和主键索引类似,索引列的值必须唯一,但允许为空(注意:多个null值不算重复)。
示例 :给USER表的PHONE列加唯一索引,防止出现重复手机号,指定索引表空间(推荐做法):CREATE UNIQUE INDEX IDX_USER_PHONE ON "USER"(PHONE) ;
场景 :用于需要唯一约束,但又不能作为主键的列(比如手机号、邮箱),既能保证数据唯一,又能加快查询速度。
误区:不要把唯一索引当主键索引用,唯一索引允许为空,且Oracle中唯一索引与主键索引的约束逻辑不同,查询时虽能加快速度,但因可能存在空值,效率略低于主键索引。
3. 普通索引(Normal Index)------ 最常用,无约束
普通索引是最常用的索引类型,没有唯一、非空的约束,只用于加快查询速度,一张表可以有多个普通索引。
示例 :给USER表的AGE列加普通索引,用于快速查询特定年龄的用户,指定索引表空间:CREATE INDEX IDX_USER_AGE ON "USER"(AGE) ;
场景:用于频繁作为查询条件,但不需要唯一约束的列(比如年龄、性别、分类id)。
4. 联合索引(Composite Index)------ 多条件查询必备
联合索引(也叫复合索引)是将多个列组合在一起创建的索引,比如给USER表的"AGE+USERNAME"创建联合索引(指定索引表空间,Oracle实战推荐):CREATE INDEX IDX_USER_AGE_USERNAME ON "USER"(AGE, USERNAME) ;
关键:联合索引遵循"最左前缀匹配原则"------查询时,必须从联合索引的第一列开始匹配,否则索引会失效。举个例子(基于上面的联合索引):
- 有效:
SELECT * FROM "USER" WHERE AGE = 25(匹配第一列AGE) - 有效:
SELECT * FROM "USER" WHERE AGE = 25 AND USERNAME = 'ZHANGSAN'(匹配第一列+第二列) - 无效:
SELECT * FROM "USER" WHERE USERNAME = 'ZHANGSAN'(跳过第一列AGE,索引失效)
场景 :频繁使用多列组合作为查询条件(比如"年龄+性别"查询用户、"订单号+用户id"查询订单),用联合索引比单独建两个普通索引效率更高(减少索引维护开销)。
四、实战避坑
很多人加了索引,却发现查询速度没变,甚至更慢------原因是索引失效了。以下6种常见场景,一定要避开:
1. 索引列使用函数/运算
错误示例 :SELECT * FROM "USER" WHERE SUBSTR(PHONE, 1, 3) = '138';(对PHONE列使用SUBSTR函数)
原因 :数据库无法对索引列的函数结果进行排序和查找,只能全表扫描。
正确做法 :避免在索引列上使用函数,改写成:SELECT * FROM "USER" WHERE PHONE LIKE '138%';若必须使用函数查询,可创建Oracle函数索引。
2. 索引列使用模糊查询(%开头)
错误示例 :SELECT * FROM "USER" WHERE USERNAME LIKE '%ZHANG'(%在开头)
原因 :%在开头时,数据库无法利用索引的有序性进行匹配,只能全表扫描。
正确做法 :尽量让%在结尾,比如SELECT * FROM "USER" WHERE USERNAME LIKE 'ZHANG%'(可利用索引);如果必须%在开头,可考虑使用Oracle全文索引(CREATE FULLTEXT INDEX)。
3. 索引列使用不等于(!=、<>)、not in
** 错误示例**:SELECT * FROM "USER" WHERE AGE != 25、SELECT * FROM "USER" WHERE AGE NOT IN (20,25)
原因 :不等于查询会导致数据库无法精准定位索引,大概率走全表扫描(除非索引列数据分布极不均匀)。
正确做法 :尽量用范围查询替代,比如SELECT * FROM "USER" WHERE AGE < 25 OR AGE > 25(部分场景可利用索引);Oracle中也可使用NOT EXISTS替代NOT IN,提升查询效率。
4. 联合索引不满足最左前缀
联合索引 必须从第一列开始匹配,跳过第一列,索引直接失效。
避坑技巧:创建联合索引时,把查询频率最高的列放在最前面。
5. 索引列值为null,使用=判断
错误示例 :SELECT * FROM "USER" WHERE PHONE IS NULL(如果PHONE列有唯一索引,且允许null)
原因 :数据库对null值的处理特殊,用=判断时,索引可能失效(不同数据库表现不同,Oracle中IS NULL可利用索引,IS NOT NULL大概率失效,建议通过执行计划实测)。
避坑技巧:尽量避免索引列存null值,可用默认值(比如空字符串'')替代。
6. 数据量过小,索引无效
数据表只有几百条、几千条数据,全表扫描的速度比索引查询还快------因为索引查询需要额外读取索引文件,反而增加了开销。
避坑技巧:数据量小于1万条的表,尽量不要建索引;只有数据量达到10万级以上,索引的优势才会体现出来。
五、索引优化实战技巧
掌握了基础和避坑点,再分享3个实战中能直接落地的优化技巧:
1. 按需建索引,拒绝"冗余索引"
冗余索引 :比如给phone列建了唯一索引,又给phone列建了普通索引------这就是冗余,唯一索引已经能满足查询需求,普通索引纯属多余,还会增加数据修改的开销。
优化建议 :建索引前,先查看当前表的索引(Oracle用SELECT INDEX_NAME, INDEX_TYPE, TABLESPACE_NAME FROM ALL_INDEXES WHERE TABLE_NAME = 'USER';),删除冗余索引;只给"频繁作为查询条件""频繁用于排序/分组"的列建索引。
2. 用"覆盖索引",避免"回表查询"
回表查询 :非聚簇索引(比如普通索引、唯一索引)查询时,只能拿到行数据地址,需要再通过地址去表中获取完整行数据------这个过程就是回表,会增加磁盘IO。
覆盖索引:如果索引中包含了查询所需的所有列,就不需要回表,直接从索引中获取数据,效率极高。
示例:查询
SELECT ID, PHONE FROM "USER" WHERE PHONE = '13800138000',如果给PHONE列建的索引是IDX_USER_PHONE(PHONE, ID)(联合索引,包含PHONE和ID),那么查询时直接从索引中获取PHONE和ID,无需回表,这在Oracle中称为"索引覆盖扫描",效率极高。
3. 定期维护索引,清理"碎片索引"
碎片索引 :数据表频繁进行插入、更新、删除操作后,索引会产生碎片(类似书籍目录被撕毁、打乱),导致索引查询效率下降。
优化建议:定期优化索引(Oracle中无MySQL的OPTIMIZE TABLE命令,需通过重建索引、 coalesce索引等方式清理碎片);对于长期不修改的表(比如字典表),可通过Oracle自带的重建索引语句重建索引,有效整理索引碎片、提升查询效率。以下是Oracle重建索引的完整实战示例及注意事项:
(1) 基础重建索引(最常用,适用于大多数场景)
sql
-- 语法:ALTER INDEX 索引名 REBUILD;
ALTER INDEX idx_user_phone REBUILD; -- 重建user表的phone列索引
适用场景:索引碎片率中等(10%-30%)、业务低峰期,重建过程中会生成临时索引,不影响原索引的查询使用(但会锁定表的DML操作,即插入、更新、删除会阻塞)。
(2) 在线重建索引(核心推荐,不阻塞业务)
sql
-- 语法:ALTER INDEX 索引名 REBUILD ONLINE;
ALTER INDEX idx_user_age_username REBUILD ONLINE; -- 在线重建联合索引
关键说明 :加上ONLINE参数后,重建索引期间,用户的查询、插入、更新、删除操作均可正常执行,不会阻塞业务,适合生产环境高峰时段使用。唯一缺点是会占用更多的临时表空间,需提前确认临时表空间充足。
(3) 重建索引并指定表空间
sql
-- 语法:ALTER INDEX 索引名 REBUILD TABLESPACE 表空间名;
ALTER INDEX idx_user_age REBUILD TABLESPACE IND_TBS ONLINE; -- 在线重建并指定索引表空间
适用场景 :原索引表空间碎片化严重、或空间不足,可通过该语句将索引重建到指定的空闲表空间,同时清理碎片。建议Oracle索引单独放在独立的索引表空间(如IND_TBS),与数据文件分离,提升IO效率。
注意 :执行语句ALTER INDEX idx_user_age REBUILD TABLESPACE IND_TBS ONLINE;若报「SQL 错误 [959] [42000]: ORA-00959: 表空间 'IND_TBS' 不存在」,核心原因是指定的表空间未创建,解决方案如下:
sql
-- 1. 先查询当前数据库已存在的表空间(确认可用表空间名称)
-- 报错说明:执行SELECT TABLESPACE_NAME FROM DBA_TABLESPACES; 报「ORA-00942: 表或视图不存在」,
-- 核心原因是当前登录用户无DBA_TABLESPACES视图的查询权限(普通用户无该权限,需管理员授权或换用普通视图)
-- 解决方案1:普通用户可用(推荐,无需管理员权限),查询当前用户可访问的表空间
SELECT TABLESPACE_NAME FROM USER_TABLESPACES;
-- 解决方案2:若有管理员权限(如sys、system用户),可直接执行原语句,或给普通用户授权后执行
-- 管理员授权语句(登录sys/system用户执行):
-- GRANT SELECT ON DBA_TABLESPACES TO 普通用户名; -- 替换"普通用户名"为实际登录用户名
-- 授权后,普通用户可执行原语句:SELECT TABLESPACE_NAME FROM DBA_TABLESPACES;
-- 2. 若确实无IND_TBS,新建索引表空间(推荐配置,适配生产环境)
-- 注意:新建表空间需CREATE TABLESPACE权限,普通用户无此权限,需管理员执行或授权
CREATE TABLESPACE IND_TBS
DATAFILE 'IND_TBS.DBF' -- 数据文件名称,可自定义(路径可补充,如'D:\ORACLE\DATA\IND_TBS.DBF')
SIZE 100M -- 初始大小,根据需求调整
AUTOEXTEND ON NEXT 50M -- 空间不足时自动扩展,每次扩展50M
MAXSIZE UNLIMITED; -- 最大大小无限制
-- 3. 再次执行重建索引语句(此时表空间已存在,可正常执行)
ALTER INDEX idx_user_age REBUILD TABLESPACE IND_TBS ONLINE;
若无需新建表空间,可将语句中的「IND_TBS」替换为查询到的已存在表空间(如默认的USERS表空间),修改后语句:
ALTER INDEX idx_user_age REBUILD TABLESPACE USERS ONLINE
(4) 重建索引的核心注意事项(必看)
-
确认碎片率 :重建索引前,先查询索引碎片率,避免无效操作。查询语句:
SELECT INDEX_NAME, DEGREE, FRAGMENTATION FROM DBA_INDEXES WHERE TABLE_NAME = 'USER';(碎片率>10%建议重建,<10%无需处理)。注:Oracle中DBA_INDEXES视图无FRAGMENTATION字段,实际查询可用SELECT INDEX_NAME, (1 - (LEAF_BLOCKS / BLOCKS)) * 100 AS FRAGMENTATION FROM DBA_INDEXES WHERE TABLE_NAME = 'USER';计算碎片率。 -
避开业务高峰:即使是在线重建(ONLINE),也会消耗一定的系统资源(CPU、IO),建议在业务低峰期(如凌晨)执行,避免影响生产性能。
-
临时表空间检查 :重建索引(尤其是在线重建)会占用临时表空间,若临时表空间不足,会导致重建失败。可通过
SELECT TABLESPACE_NAME, FREE_SPACE FROM DBA_TEMP_FREE_SPACE;查询临时表空间空闲情况。 -
避免频繁重建:索引重建不是越频繁越好,频繁重建会增加系统开销,建议按周期(如每周/每月)检查碎片率,按需重建。
-
分区索引特殊处理 :若表是分区表,对应的分区索引重建需指定分区,语法:
ALTER INDEX 索引名 REBUILD PARTITION 分区名 ONLINE;,避免重建整个索引浪费资源。
六、Oracle函数索引(解决索引列用函数失效问题)
前文提到"索引列使用函数会导致索引失效",但实际业务中,难免会有"对索引列做函数运算"的查询场景(比如截取手机号前缀、转换字段大小写查询),此时就需要用到Oracle专属的函数索引,专门解决这类场景的索引失效问题。
1. 函数索引实战示例(直接落地)
场景 :需要频繁执行"截取手机号前3位查询",如SELECT * FROM "USER" WHERE SUBSTR(PHONE, 1, 3) = '138',普通PHONE列索引会失效,创建函数索引即可解决。
sql
-- 1. 创建函数索引(指定索引表空间,Oracle实战推荐)
CREATE INDEX IDX_USER_PHONE_SUBSTR ON "USER"(SUBSTR(PHONE, 1, 3)) TABLESPACE "IND_TBS";
-- 2. 执行查询(此时会正常使用函数索引,避免全表扫描)
SELECT * FROM "USER" WHERE SUBSTR(PHONE, 1, 3) = '138';
其他常用函数索引示例:
sql
-- 大小写转换函数索引(解决用户名大小写不敏感查询)
CREATE INDEX IDX_USER_USERNAME_UPPER ON "USER"(UPPER(USERNAME)) TABLESPACE "IND_TBS";
-- 查询时使用对应函数,索引生效
SELECT * FROM "USER" WHERE UPPER(USERNAME) = 'ZHANGSAN';
2. 函数索引核心特点与适用场景
- 核心特点:基于"函数运算结果"建立索引,查询时需与索引创建时的函数完全一致(包括参数、函数名),才能触发索引生效;本质仍是B树索引,查询效率与普通B树索引一致。
- 适用场景:频繁对同一列做固定函数运算查询(如截取、转换、日期运算),且数据量较大(10万级以上),普通索引无法满足需求的场景。
- 不适用场景:函数运算不固定(如有时SUBSTR(PHONE,1,3),有时SUBSTR(PHONE,1,4))、查询频率低、数据量小的场景(创建函数索引会增加存储和维护开销)。
3. 函数索引注意事项(必看)
- 函数一致性:查询时的函数必须与索引创建时的函数完全匹配,否则索引失效。例:创建索引时用SUBSTR(PHONE,1,3),查询时用SUBSTR(PHONE,1,4),索引不生效。
- 维护开销:函数索引的维护开销比普通索引高------每次插入、更新、删除数据时,数据库会自动计算函数结果,同步更新函数索引,需谨慎使用。
- 避免过度创建:同一列不要创建多个不同函数的索引(如同时创建SUBSTR(PHONE,1,3)和UPPER(PHONE)索引),会大幅增加维护开销。
- 结合分区表使用:若表是分区表,可创建分区函数索引,语法:CREATE INDEX 索引名 ON 表名(函数(列名)) TABLESPACE 表空间名 LOCAL;,减少维护成本。