Oracle数据库中 执行计划, 表索引

Oracle Database Performance Tuning: Understanding Execution Plans and Table Indexing

Posted by Wenqin on September 15, 2025

Oracle数据库中 执行计划, 表索引

1. 执行计划

1.1 概述

什么是执行计划?

  • 执行计划是 Oracle 数据库为执行一条 SQL 语句制定的“操作步骤说明书”。就像要组装一台电脑,执行计划就是详细列出“先装主板,再插内存,最后装电源”的步骤清单。

执行计划的作用:

  1. 优化查询性能 数据库可能用不同的方式执行你的 SQL(比如全表扫描或走索引),执行计划帮你找到最高效的方式。
  2. 排查慢查询问题 如果 SQL 执行慢,执行计划会告诉你哪一步耗时最多(比如全表扫描了100万行数据)。
  3. 验证索引是否生效 检查数据库是否按你预期使用了索引,避免无效索引拖慢速度。

1.2 执行计划的使用

1.2.1 EXPLAIN PLAN命令

使用EXPLAIN PLAN 命令:(适合静态分析)

步骤

  1. 创建一个计划表(如果不存在)

    1
    
    EXPLAIN PLAN FOR SELECT * FROM EMPLOYEES WHERE id = 100;
    
  2. 查看执行计划:

    1
    
    SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
    

输出示例:

1
2
3
4
5
6
7
8
Plan hash value: 2949544139
-------------------------------------------------------------------------------------
| Id  | Operation                   | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
-------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |            |     1 |    69 |     1   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| EMPLOYEES  |     1 |    69 |     1   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | EMP_ID_PK  |     1 |       |     0   (0)| 00:00:01 |
-------------------------------------------------------------------------------------

执行计划关键字段解析:

  • Id:步骤的编号(像步骤1、步骤2)。
  • Operation:操作类型(例如:INDEX SCAN = 查索引,TABLE ACCESS = 读表)。
  • Name:对象名称(例如:索引名 EMP_ID_PK)。
  • Rows:预估处理的行数(越小越好)。
  • Cost:代价(类似“花费的精力”,数值越小效率越高)。

1.2.2 自动跟踪实时执行计划

自动跟踪实时执行计划(适合动态分析)

1
2
3
4
5
-- 开启跟踪
SET AUTOTRACE ON;

-- 执行 SQL
SELECT * FROM employees WHERE id = 100;

1.2.3 如何阅读执行计划

  1. 从内向外看: 执行顺序是 从最内层(Id大的步骤)到最外层(Id小的步骤)例如:先执行Id=2的索引扫描,再通过Id=1回表查数据。
  2. 关注高代价操作: 如果某一步的 CostRows 特别大,可能就是性能瓶颈。

1.2.4 常见问题与解决方法

通过执行计划可以确认的问题有:

  1. 全表扫描(TABLE ACCESS FULL
    • 问题:表太大时慢如蜗牛。
    • 解决:对 WHERE 条件中的列创建索引。
  2. 索引未生效
    • 可能原因:查询条件写得不规范(例如:对索引列使用函数 WHERE UPPER(name) = 'A')。
    • 解决:改写 SQL,保持索引”干净”。

2. 索引

2.1 什么是索引

想象一下,你有一本没有目录的书。如果想找某个特定的内容(比如”数据库是什么”),你需要一页页翻看,直到找到为止。这很慢,对吧?

现在这本书有了一个目录,目录里按页码标注了每个主题的位置(比如”数据库:第30页”)。这时候,你想找内容就直接翻到第30页,省时又高效。

索引(index)在数据库中就是这个”目录”的角色

  • 索引是一个用于加速数据获取的数据对象,从而提高数据访问性能。

简单来讲,索引借用一种特有的数据结构来快速定位表中的数据行。

2.2 索引的作用

索引的具体作用:

  1. 加速查询
    • 没有索引时,查询需要扫描整个表(全表扫描),数据量越大越慢。
    • 有了索引后,数据库能像查字典一样快速定位数据,大幅减少查询时间。
    • 举例:如果一个表有100万条数据,没索引可能需要几分钟,而有索引可能只需要几毫秒。
  2. 保证数据的唯一性
    • 如果为某列创建了唯一索引(Unique Index),那么该列的值必须是唯一的。
    • 举例:给”员工ID”列建唯一索引后,就不能有两个员工ID相同。
  3. 优化排序和分组
    • 如果查询中需要对数据进行排序(ORDER BY)或分组(GROUP BY),索引能减少排序的开销。
    • 举例:对”订单金额”列建索引后,按金额排序会更快。
  4. 加速表连接(JOIN)
    • 多个表关联查询时,如果连接字段有索引,查询效率会显著提升。
    • 举例:员工表和部门表通过”部门ID”关联,如果”部门ID”有索引,JOIN操作会更快。
  5. 减少磁盘I/O
    • 索引减少了需要读取的数据量,从而减少了磁盘访问次数,降低了硬件压力。

2.3 索引的原理

在前面有提到:索引是借用一种特有的数据结构来快速定位表中的数据行。

2.3.1 数据结构

数据结构:大白话来讲就是数据的排列结构。

常见的数据结构有:数组、链表、树等

2.3.1.1 数组结构

image-20250501222828275

数组结构的优势:查询速度效率高、修改元素方便。

数组结构的缺点:新增元素和删除元素效率低

2.3.1.2 链表结构

image-20250501223214573

链表结构的优势:增加元素和删除元素效率高。

链表结构的缺点:查询效率低。

2.3.1.3 树结构

在数据结构中,树结构是一种非常重要且独特的结构,它就像我们现实生活中的树一样,有根、有枝、有叶,不过在计算机领域中树结构是倒过来的。

image-20250505192903516

树结构的概念:

  • 节点:树是由一个个节点组成的。每个节点就好比树上的一个 “点”,节点可以存储数据。
  • 根节点:树有一个特殊的节点,叫做根节点,它是树的起始点,就像现实中树的根部一样,是整棵树的基础。
  • 边:节点之间通过边来连接,边表示了节点之间的关系。可以把边想象成树枝,它将不同的节点连接在一起。
  • 子节点和父节点:对于任意一个节点,如果有一条边从另一个节点指向它,那么指向它的那个节点就是它的父节点。一个节点可以有多个子节点,但只能有一个父节点。(根节点没有父节点)
  • 叶子节点:那些没有子节点的节点就是叶子节点,它们就像树的叶子一样,在树的最末端。

树的分类:

  • 二叉树:是一种特殊的树结构,每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树在计算机科学中应用广泛,比如:在查找算法中,二叉搜索树就是一种特殊的二叉树,它可以让查找操作更加高效。
  • 多叉树:与二叉树相对,每个节点可以有多个子节点。例如,在表示文件系统的树结构中,一个文件夹可以包含多个子文件夹和文件,这就可以用多叉树来表示,每个文件夹节点可以有多个子节点,分别对应它包含的子文件夹和文件。

树的遍历方式:

  1. 前序遍历:先访问根节点,然后递归地访问左子树,最后递归地访问右子树。
  2. 中序遍历:先递归地访问左子树,然后访问根节点,最后递归地访问右子树。
  3. 后序遍历:先递归地访问左子树,然后递归地访问右子树,最后访问根节点。

2.3.2 B-Tree索引

ORACLE中最常用的索引类型使用:B-Tree索引

索引是需要占据存储空间的,也可以理解为是一种特殊的数据。形式类似于下图的一棵”树”,而树的节点存储的就是每条记录的物理地址,也就是我们提到的伪列(ROWID)

img

B-Tree索引树概念图:

image-20250505201921091

2.4 索引理论小结

小结:

  1. 索引通常是在表中的数据量比较大,且字段(列)经常被作为检索条件查询时才创建

    1
    2
    
    -- city字段经常用为查询条件使用
    select 1,  from 表名 where city='xxxx'
    
  2. 创建索引时,需要占用一定的空间(基于某列创建索引时,会把列中的数据提取出来,构建索引树)

  3. 通常在某列上创建了索引后,在该列进行插入、更新、删除数据时,会对已有的索引进行维护,此时会降低写的性能

2.5 普通索引

数据库里的数据量往往非常大,要是每次查询都把全量数据扫描一遍,那效率可就太低了。普通索引就是为了提高查询效率而生的。它会按照你指定的列(字段)的值,对数据进行排序,然后把这些值和对应数据的存储位置记录下来。这样在查询时,数据库就可以通过索引快速找到符合条件的数据存储位置,而不用去扫描全量数据。

2.5.1 使用方式

语法:

1
CREATE INDEX 索引名称 ON 表名 (列名);
  • 索引名称:给索引起的名字,要保证这个名字在数据库里是唯一的,方便后续管理和引用。
  • 表名:在哪个表上创建索引。
  • 列名:指定按照哪些列来创建索引,可以是一个列,也可以是多个列。如果是多个列,就用逗号把它们隔开,这种情况叫做复合索引。

索引的使用:

假设有一个名为 employees 的表,里面有 employee_idfirst_namelast_namehire_date 这些列。如果经常根据 last_name 来查询员工信息,就可以为 last_name 列创建一个普通索引。示例代码如下:

  • 步骤1:创建索引
1
CREATE INDEX idx_last_name ON employees (last_name);
  • 步骤2:使用索引进行查询
1
2
-- 创建好索引后,当执行类似下面这样的查询时,数据库就会使用这个索引来提高查询效率
SELECT * FROM employees WHERE last_name = 'smith';

因为数据库可以通过 idx_last_name 索引快速定位到 last_name 为 ‘smith’ 的员工数据存储位置,而不用扫描整个 employees 表。

2.5.2 索引性能测试

创建表:

1
2
3
4
5
6
7
CREATE TABLE index_tb
(
   id NUMBER,
   name VARCHAR2(20),
   sex char(3),
   age number(3)
);

生成数据样本: 编写PL/SQL 插入1000万条记录(PL/SQL会在后续课程中学习)

1
2
3
4
5
6
7
8
9
10
-- 直接复制以下代码运行即可
BEGIN
	 FOR V IN 1..10000000 LOOP 
		 IF MOD(V,2)=1 THEN 
	         INSERT INTO index_tb VALUES (V,initcap(dbms_random.string('L',trunc(dbms_random.value(5,10)))),'',floor(DBMS_RANDOM.value(18,100)));
		 ELSE 
		     INSERT INTO index_tb VALUES (V,initcap(dbms_random.string('L',trunc(dbms_random.value(5,10)))),'',floor(DBMS_RANDOM.value(18,100)));
		 END IF;
	 END LOOP;
END;

生成完数据后,根据条件name=xxxx进行查询:

1
SELECT * FROM index_tb WHERE name='Fqakwaqe'; 

name字段上创建索引:

1
CREATE INDEX idx_name ON index_tb (name);

再次进行查询:

1
SELECT * FROM index_tb WHERE name='Fqakwaqe';

案例:全表扫描 vs 索引扫描

  • 场景1:没有索引的全表扫描

    1
    
    EXPLAIN PLAN FOR SELECT * FROM index_tb WHERE last_name = 'Smith';
    

    执行计划:

    ---------------------------------------------------------------------------------
    | Id  | Operation          | Name      | Rows  | Bytes | Cost (%CPU)| Time     |
    ---------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT   |           |    10 |   690 |     5   (0)| 00:00:01 |
    |*  1 |  TABLE ACCESS FULL | EMPLOYEES |    10 |   690 |     5   (0)| 00:00:01 |
    ---------------------------------------------------------------------------------
    

    问题:TABLE ACCESS FULL 表示全表扫描(效率低,尤其是大表)。

  • 场景2:创建索引后优化

    1
    2
    3
    
    CREATE INDEX idx_last_name ON employees(last_name);
      
    EXPLAIN PLAN FOR SELECT * FROM employees WHERE last_name = 'Smith';
    

    执行计划:

    1
    2
    3
    4
    5
    6
    7
    
    -------------------------------------------------------------------------------------
    | Id  | Operation                   | Name           | Rows | Bytes | Cost | Time   |
    -------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT            |                |   10 |   690 |    2 | 00:00:01|
    |   1 |  TABLE ACCESS BY INDEX ROWID| EMPLOYEES      |   10 |   690 |    2 | 00:00:01|
    |*  2 |   INDEX RANGE SCAN          | IDX_LAST_NAME  |   10 |       |    1 | 00:00:01|
    -------------------------------------------------------------------------------------
    

    优化效果:INDEX RANGE SCAN 表示通过索引查找,Cost 从5降到2。

2.5.3索引树细节

在ORACLE中,在建立索引树时,是通过字段(列)的值和值的长度来决定是存放树结构的哪个位置。

字段值对存放位置的影响:

  • 排序依据:索引树本质上是一种有序的数据结构,字段值是决定数据在索引树中存放位置的关键因素。Oracle 会按照字段值的大小顺序对数据进行排序,在 B-Tree索引(Oracle 中最常见的索引类型)里,较小的值会存放在树的左边分支,较大的值则存放在右边分支。
  • 查找与定位:当执行查询时,数据库会利用索引树的有序特性,通过比较字段值来快速定位数据。比如,在执行 WHERE column_name > 'value' 这样的查询时,数据库会从索引树的根节点开始,根据字段值的大小关系,快速找到满足条件的数据所在的范围。

字段值长度对存放位置的影响:

  • 空间与页的使用:虽然字段值长度本身并非直接决定数据在索引树中存放位置的因素,但它会影响索引块(页)使用。字段值长度较长会占用更多的空间,若一个索引块无法容纳过多长值记录,就可能导致数据被拆分到不同的索引块中。
  • 索引分裂:当插入数据时,如果一个索引块已满,而新记录的字段值需要插入到该块中,就会引发索引块的分裂。字段值长度较长会增加索引块分裂的可能性,因为长值记录更容易使索引块达到容量上限。索引块分裂会影响索引树的结构,进而影响数据的存放位置。

示例说明:

  • 假设你有一个名为 employees 的表,其中包含 employee_idlast_name 字段,现在为 last_name 字段创建索引。

    1
    2
    3
    4
    5
    6
    7
    8
    
    -- 创建示例表
    CREATE TABLE employees (
        employee_id NUMBER,
        last_name VARCHAR2(50)
    );
      
    -- 为 last_name 字段创建索引
    CREATE INDEX idx_last_name ON employees (last_name);
    

在插入数据时,Oracle 会依据 last_name 字段的值按字母顺序将记录插入到索引树中。若 last_name 字段值较长,可能会导致索引块的空间使用更加紧张,从而增加索引块分裂的概率。

综上所述,字段值是决定数据在索引树中存放位置的直接依据,而字段值长度则通过影响索引块的使用和分裂间接影响数据的存放位置。

2.6 复合索引

在开发中如果经常要对某几列进行查询,比如:经常要根据学历和性别对学员进行搜索,如果对这两列分别建立索引,需要创建两个。那么当使用学历和性别作为条件进行查询时就要查两棵树,此时查询性能不一定高。那如何建立索引呢?我们可以建立复合索引,也就是基于两个以上的列建立一个索引 。

语法:

1
create index 索引名称 on 表名(列名,列名.....);

根据地址和门牌号对业主表创建索引,语句如下:

1
create index owners_index_ah on T_OWNERS(addressid,housenumber);

复合索引:

  • 特点:基于多个列的索引,列的先后顺序很重要
  • 适用场景:
    • 查询条件中同时涉及多个列
    • 举例:为addressidhousenumber创建复合索引,查询 WHERE addressid=2 AND housenumber=='2-2' 会触发索引。
  • 规则:
    • 复合索引的顺序决定了哪些查询能命中索引
    • 只有查询条件包含最左前缀列(比如复合索引是A,B,C,查询条件包含A或A,B或A,B,C)才能命中索引
    • 如果查询条件只包含B或C,则无法命中索引

2.7 唯一索引

如果我们需要在某个表某个列创建索引,而这列的值是不会重复的。这是我们可以创建唯一索引。

使用场景:当表中某个字段中经常被作为条件检索时,而且字段中的数据是唯一(仅有一个NULL)。

语法:

1
create unique index 索引名称 on 表名(列名);

需求:在业主表的水表编号一列创建唯一索引

1
create unique index index_owners_watermeter on t_owners(watermeter);

image-20250505202513244

唯一索引和普通索引的区别:

  • 唯一索引在遍历”索引树”时,树节点中的键值只比较一次

  • 普通索引中因为可能会存在重复键值,所以会在找到相同的键值时,再向下一个键值比较,直到匹配不成功。

2.8 反向键索引

用场景:当某个字段的值为连续增长的值,如果构建标准索引,可能会形成歪脖子树。这样会增加查询的层数,性能会下降。建立反向键索引,可以使索引的值变得不规则,从而使索引树能够均匀分布。

结论:当列上所存储的数据长度一致时,且数据的大小比较相近时,在此列上创建反向键索引

img

语法:

1
create index 索引名称 on 表名(列名) reverse;

反向键索引的优点:

  1. 减少索引块争用
    • 通过分散数据,避免多进程同时修改同一块,提升插入性能。
  2. 优化高并发写入
    • 特别适合像订单系统、日志记录这种需要频繁插入的场景。
  3. 降低索引分裂
    • 正常顺序插入会导致索引块频繁分裂(Split),反向键索引能减少这种情况。

反向键索引的缺点:

  1. 不支持范围查询
    • 如果查询是 WHERE id > 1000 AND id < 2000,反向键索引无法高效利用,因为反转后的值是乱序的。
  2. 维护成本略高
    • 反转操作会增加一点 CPU 和存储开销,但对于大多数场景影响不大。
  3. 不适合随机值列
    • 如果列的值本身已经是随机的(比如 UUID),反向键索引反而会浪费资源。

2.9 位图索引

使用场景:位图索引适合创建在低基数列上(例:性别、婚姻状态)

位图索引不直接存储ROWID,而是存储字节位(0或1)到ROWID的映射

优点:减少响应时间,节省空间占用

img

语法:

1
create bitmap index 索引名称 on 表名(列名);

位图索引的优势:

  • 节省存储空间:位图索引使用位图来表示每个索引键值,对于具有大量重复值的列,位图索引能够显著减少存储空间的使用。例如,在一个包含百万条记录的员工表中,性别字段只有 “男” 和 “女” 两个值,使用位图索引可以用非常少的空间来存储这些信息。
  • 高效的多条件查询:当进行多条件查询时,位图索引可以通过位运算(如与、或、非)快速合并多个位图,从而高效地找出满足多个条件的记录。例如,在查询既属于某个部门又具有某种职位的员工时,位图索引可以快速定位到符合条件的记录。
  • 快速统计分析:对于需要进行统计分析的场景,如计数、求和等,位图索引可以通过位运算快速计算出结果,而不需要扫描大量的数据。

位图索引在企业应用中的使用场景:

  • 数据仓库和报表系统:在数据仓库和报表系统中,通常需要进行大量的统计分析和多条件查询。位图索引可以帮助快速完成这些任务,提高查询性能。例如,在分析销售数据时,可以为产品类别、销售地区、销售时间等字段创建位图索引,以便快速生成各种报表。
  • 具有大量重复值的列:对于具有大量重复值的列,如性别、婚姻状况、部门等,位图索引可以显著提高查询性能。例如,在一个包含数百万条客户记录的数据库中,客户的性别字段只有 “男” 和 “女” 两个值,为该字段创建位图索引可以大大加快查询速度。
  • 静态数据:对于不经常更新的静态数据,位图索引是一个很好的选择。因为位图索引的维护成本较高,当数据频繁更新时,会影响性能。而对于静态数据,位图索引可以提供高效的查询性能。例如,在一个包含历史数据的数据库中,为历史事件的类型、发生时间等字段创建位图索引,可以快速查询和分析历史数据。

需要注意的是,位图索引不适合用于频繁更新的表,因为每次更新操作都需要更新位图索引,会带来较大的性能开销。此外,位图索引也不适合用于选择性高的列,因为在这种情况下,位图索引的优势不明显。

2.10 函数索引

场景举例:

  • 假设你有一个员工表 employees,里面有一个列叫 first_name,比如 “john”、“jane” 等。

  • 书写的查询:

    1
    
    SELECT * FROM employees WHERE LOWER(first_name) = 'john';
    
    • LOWER() 函数会把名字变成小写,然后和 'john' 比较。
  • 问题:

  • 给 first_name 列建了一个普通索引,这个查询还是会走“全表扫描”,因为数据库不知道你用了 LOWER() 函数,无法利用索引。

  • 解决方案:创建一个 函数索引,提前把 LOWER(first_name) 的结果存起来。

    这样,数据库就知道:“哦,你要找小写的 john,我已经有这个‘目录’了,直接给你结果吧!”

语法:

1
CREATE INDEX 索引名 ON 表名(单行函数(列名));

函数索引的优缺点:

优点 缺点
加速带有函数的查询(比如 UPPER()SUBSTR() 占用额外存储空间(每个函数索引都需要保存计算结果)
支持复杂运算(比如 salary * 0.9 计算税后工资) 插入/更新数据时会多一步计算函数的操作,稍微拖慢写速度
可以避免“全表扫描”,减少磁盘 I/O 如果函数参数变化,索引可能失效

2.11 索引小结

索引的优缺点:

优点 缺点
加速查询,减少全表扫描 占用额外存储空间
提高排序、分组、连接效率 插入、更新、删除数据时,需要维护索引,降低写性能
支持唯一性约束 频繁修改数据可能导致索引碎片化,需定期重建

如何选择索引?

  1. 高频查询的列:经常出现在 WHEREJOINORDER BY 中的列优先加索引。
  2. 低基数列 vs 高基数列:
    • 低基数列(如性别)适合位图索引。
    • 高基数列(如身份证号)适合B树索引。
  3. 复合索引的顺序:将高选择性(区分度高)的列放在前面。
  4. 避免过度索引:每个索引都需要维护,过多索引会拖慢写操作。
  5. 定期维护:对频繁修改的表,定期重建索引以减少碎片。

Oracle常见索引:

  1. 普通索引(默认类型)
    • 特点:基于B树结构(类似多层目录),适用于大多数场景。
    • 适用场景:
      • 等值查询(如 WHERE id = 100
      • 范围查询(如 WHERE age > 30
      • 排序和分组
    • 优点:通用性强,适合大部分查询需求。
    • 缺点:频繁更新(插入、删除、修改)会导致索引碎片化,影响性能。
  2. 唯一索引
    • 特点:确保索引列的值唯一。
    • 适用场景:
      • 主键列(自动创建唯一索引)
      • 需要强制唯一性的字段(如邮箱、用户名)
    • 注意:唯一索引允许NULL值(但只能有一个NULL),而主键不允许NULL。
  3. 复合索引(组合索引)
    • 特点:基于多个列的索引,顺序很重要。
    • 适用场景:
      • 查询条件中同时涉及多个列。
      • 举例:为“姓名”和“出生日期”创建复合索引,查询 WHERE 姓名='张三' AND 出生日期='1990-01-01' 会触发索引。
    • 规则:
      • 复合索引的顺序决定了哪些查询能命中索引。
      • 只有查询条件包含最左前缀列(比如复合索引是A,B,C,查询条件包含A或A,B或A,B,C)才能命中索引。
      • 如果查询条件只包含B或C,则无法命中索引。
  4. 位图索引(Bitmap Index)
    • 特点:用位图(二进制数)表示数据是否存在,适合低基数(唯一值很少)的列。
    • 适用场景:
      • 性别(男/女)、状态(启用/禁用)、是否会员等只有几个固定值的列。
    • 优点:对低基数列查询极快,适合数据仓库(DSS)环境。
    • 缺点:不推荐用于高基数列(比如:身份证号),因为位图会变得非常大,反而降低效率。
  5. 函数索引(Function-Based Index)
    • 特点:基于列的函数表达式创建索引。
    • 适用场景:
      • 查询中使用了函数或表达式(如 WHERE UPPER(name) = 'ZhangSan')。
      • 举例:对 UPPER(name) 创建函数索引后,查询会命中索引,而不需要全表扫描。
    • 优点:解决函数导致索引失效的问题。
    • 注意:函数索引需要确保表达式的计算结果稳定(比如不会因数据变化而改变)。