知识库✳

教程与手册

全部教程
  1. 01Oracle 函数手册
  2. 02性能优化手册
  3. 03Linux Shell
  4. 04Oracle 学习与演练
  5. 05网络基础学习
  6. 06数据治理与管理
返回个人网站
甲Oracle 学习教程从 SQL 到 PL/SQL 与运维
首页课程目录搜索演练场
知识库

已完成 0 / 22 章

模块一 · 入门与准备

  • 01Oracle 数据库体系结构
  • 02环境安装与连接方式
  • 03SQL*Plus 与常用工具

模块二 · SQL 查询基础

  • 04SELECT 查询基础
  • 05过滤与排序
  • 06单行函数
  • 07多表连接
  • 08分组聚合与 HAVING
  • 09子查询

模块三 · 数据与对象

  • 10DML 与事务控制
  • 11表与数据类型
  • 12约束
  • 13索引与执行计划
  • 14视图、序列与同义词

模块四 · PL/SQL 编程

  • 15PL/SQL 块与变量
  • 16流程控制与游标
  • 17存储过程与函数
  • 18触发器
  • 19异常处理

模块五 · 运维与进阶

  • 20用户与权限管理
  • 21表空间与存储管理
  • 22备份与恢复
❖

Oracle 学习教程

返回个人网站知识库

示例代码基于 Oracle AI Database 26ai / 23ai,查询示例使用官方 HR 示例方案。

以 Next.js 16 · Tailwind CSS v4 · HeroUI v3 装帧

13
  1. 首页
  2. 课程目录
  3. 索引与执行计划

第 13 章◆高级◆建议 60 分钟◆模块三 · 数据与对象

索引与执行计划

索引为什么有时不生效?学会看懂执行计划,才能定位真正的性能瓶颈。

学完这一章,你应该能

  • 理解 B-Tree 索引结构与最左前缀原则
  • 会用 EXPLAIN PLAN 和 DBMS_XPLAN 看执行计划
  • 识别索引失效的常见写法
  • 知道什么时候该收集统计信息

13.1索引基本原理

B-Tree 索引把索引列排序后建成树结构,让数据库可以用很少的 I/O 定位到目标行,而不必扫描整表。代价是每次 DML 都要维护索引,所以索引不是越多越好。

  • 适合建索引:高选择性(不同值多)的列、常用于 WHERE/JOIN/ORDER BY 的列
  • 不适合:性别这类只有几个值的列(此时位图索引可能更合适)
  • 复合索引遵循最左前缀:索引 (a, b) 能用于 a 或 a+b,不能单独用于 b
  • 索引列上使用函数或隐式类型转换会导致索引失效
创建各类索引
-- 普通索引:employees.department_id 在 HR 方案里已有 emp_department_ix,
-- 直接再建会报 ORA-01408,所以换个没有索引的列演示
CREATE INDEX idx_emp_salary ON employees (salary);

-- 复合索引:注意列顺序,把等值条件列放前面
CREATE INDEX idx_emp_dept_job ON employees (department_id, job_id);

-- 唯一索引:email 上已有 emp_email_uk,这里用组合列演示
CREATE UNIQUE INDEX uk_emp_email_dept ON employees (email, department_id);

-- 函数索引:解决「列上用函数导致索引失效」
CREATE INDEX idx_emp_hire_year ON employees (TRUNC(hire_date, 'YYYY'));

-- 查看索引信息
SELECT index_name, index_type, uniqueness, status
FROM   user_indexes WHERE table_name = 'EMPLOYEES';

只作示例阅读:5 行写入限制下只开放单独创建普通空表;CTAS、删表、清空表、改结构、触发器和存储程序不允许执行。请在独立学习数据库中练习。

13.2看懂执行计划

  • TABLE ACCESS FULL:全表扫描,行数大且无可用索引时出现
  • INDEX RANGE SCAN:索引范围扫描,通常是我们想要的
  • INDEX UNIQUE SCAN:唯一索引等值查询,效率最高
  • NESTED LOOPS:小结果集驱动大表时效果好
  • HASH JOIN:大表之间连接,通常出现在数据仓库场景
  • 看计划先看预估行数(Rows)和实际行数差别,差得多说明统计信息过期
生成并查看执行计划
EXPLAIN PLAN FOR
SELECT e.last_name, d.department_name
FROM   employees e
JOIN   departments d ON d.department_id = e.department_id
WHERE  e.department_id = 60;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

-- 更实用:查看真实执行统计(需要实际执行语句)
SELECT /*+ GATHER_PLAN_STATISTICS */
       e.last_name, d.department_name
FROM   employees e
JOIN   departments d ON d.department_id = e.department_id
WHERE  e.department_id = 60;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));

只作示例阅读:受限模式只开放查询、常规增删改及单独创建空表。请在独立学习数据库中练习。

13.3索引失效的常见写法

这些写法会让索引用不上
-- ✗ 列上使用函数
SELECT * FROM employees WHERE TO_CHAR(hire_date, 'YYYY') = '2005';
-- ✓ 改成范围条件,或建函数索引
SELECT * FROM employees
WHERE  hire_date >= DATE '2005-01-01' AND hire_date < DATE '2006-01-01';

-- ✗ 隐式类型转换(employee_id 是 NUMBER,传字符串)
SELECT * FROM employees WHERE employee_id = '100';
-- ✓ 传正确类型
SELECT * FROM employees WHERE employee_id = 100;

-- ✗ 前导通配符无法用索引
SELECT * FROM employees WHERE last_name LIKE '%mith';
-- ✓ 后置通配符可以用索引
SELECT * FROM employees WHERE last_name LIKE 'Sm%';

-- ✗ 对索引列做运算
SELECT * FROM employees WHERE salary * 12 > 100000;
-- ✓ 把运算移到右边
SELECT * FROM employees WHERE salary > 100000 / 12;
在你的专用沙箱里执行打开演练场

13.4统计信息

收集统计信息
-- 表与索引一起收集
BEGIN
  DBMS_STATS.GATHER_TABLE_STATS(
    ownname          => USER,
    tabname          => 'EMPLOYEES',
    cascade          => TRUE,       -- 连带收集索引统计
    estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
    degree           => 4
  );
END;
/

-- 查看统计信息的新鲜度
SELECT table_name, num_rows, last_analyzed, stale_stats
FROM   user_tab_statistics
WHERE  table_name = 'EMPLOYEES';

只作示例阅读:5 行写入限制下不执行任意 PL/SQL、循环写入或动态 SQL;只开放字面量 DBMS_OUTPUT 打印。请在独立学习数据库中练习。

提示 优化器靠统计信息估算代价。数据量发生大幅变化后没重新收集统计信息,是「昨天还很快今天突然变慢」的常见原因。

练习

3 题 · 先自己写,再展开对照

参考答案

一般不能按最左前缀利用,因为索引按 department_id 先排序,
单独用 job_id 无法直接定位。

个别情况下优化器会走 INDEX SKIP SCAN(跳跃扫描),把前导列的不同值
当作多个子索引来扫,但这依赖统计信息和数据分布,不能指望。

稳妥做法:如果确实经常单独按 job_id 查询,就再建一个以 job_id
为前导列的索引。

自评要点

  • 说出最左前缀原则
  • 知道 SKIP SCAN 存在但不可依赖

参考答案

因为函数作用在了索引列上,索引里存的是 hire_date 的原值,
优化器无法直接用它定位,只能全表扫描。

改法一:改写成范围条件,让索引可用
WHERE hire_date >= DATE '2005-01-01' AND hire_date < DATE '2006-01-01'

改法二:建函数索引,把表达式的值也索引起来
CREATE INDEX idx_emp_hire_year ON employees (TRUNC(hire_date, 'YYYY'));

自评要点

  • 识别出函数作用于索引列
  • 给出范围条件改写
  • 提到函数索引

参考答案

方式一:只看优化器预估的计划(不真正执行)
EXPLAIN PLAN FOR
SELECT ... ;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

方式二:看真实执行统计(会真正执行语句)
SELECT /*+ GATHER_PLAN_STATISTICS */ ... ;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));

排查性能问题时优先用方式二,因为预估行数与实际行数差得多,
往往说明统计信息过期。

自评要点

  • EXPLAIN PLAN + DBMS_XPLAN.DISPLAY
  • GATHER_PLAN_STATISTICS + DISPLAY_CURSOR('ALLSTATS LAST')
  • 知道要看预估行数与实际行数的差异
← 上一章

约束

下一章 →

视图、序列与同义词