Skip to content

Oracle 索引与优化器

Oracle 索引与优化器的重点不是背“B-tree 快、Bitmap 慢”,而是理解:

Oracle 怎么通过统计信息估算行数和成本,为什么通过索引拿到 ROWID 后还要访问表块,为什么同一条 SQL 可能因为数据分布、绑定变量、统计信息变化而走完全不同的计划。

学习目标

学完这一页,你要能掌握:

  1. B-tree、Bitmap、函数索引、组合索引分别适合什么场景。
  2. ROWID 为什么是 Oracle 索引定位表行的关键。
  3. INDEX RANGE SCANINDEX UNIQUE SCANTABLE ACCESS FULLTABLE ACCESS BY INDEX ROWID 分别代表什么。
  4. 优化器为什么依赖统计信息、直方图和基数估算。
  5. 绑定变量为什么能减少硬解析,又为什么可能带来计划不适配。
  6. 商业 OLTP 系统如何给资产、订单、报表查询设计索引。

索引的本质

没有索引时,Oracle 需要扫描表块,然后逐行判断条件。

mermaid
flowchart TD
    A["没有合适索引"] --> B["读取大量表块"]
    B --> C["逐行判断 where 条件"]
    C --> D["过滤结果"]
    D --> E["排序或 Join"]

有索引时,Oracle 先在索引结构中定位 key,再通过 ROWID 找表行。

mermaid
flowchart TD
    A["where asset_no = :assetNo"] --> B["访问 B-tree 索引"]
    B --> C["Root Block"]
    C --> D["Branch Block"]
    D --> E["Leaf Block<br/>key + ROWID"]
    E --> F["TABLE ACCESS BY INDEX ROWID"]
    F --> G["读取表块中的完整行"]

索引能减少读块数量,但也带来写入维护成本。更新索引列时,索引也要变;索引越多,DML 成本越高。

常见索引类型

索引适合场景不适合场景
B-tree Index高选择性字段、范围查询、排序、OLTP 主力索引低基数字段大范围统计不一定最佳
Bitmap Index低基数字段、数据仓库、少更新多统计高并发 OLTP 高频更新
Function-based Indexupper(name)trunc(date) 等表达式查询表达式不稳定或 SQL 写法不统一
Composite Index多条件过滤、排序列顺序错误会收益很低
Unique Index业务唯一性和精确定位非唯一业务不能强加

B-tree 索引和 ROWID

Oracle B-tree 索引通常由 root、branch、leaf block 组成。leaf block 保存 key 和 ROWID。

mermaid
flowchart TD
    A["Root Block"] --> B["Branch Block"]
    B --> C["Leaf Block"]
    C --> D["Key"]
    C --> E["ROWID"]
    E --> F["Data Block"]
    F --> G["Row"]

ROWID 可以理解为表中行的物理定位信息。通过 ROWID,Oracle 可以快速找到对应数据块和行位置。

常见计划:

text
INDEX UNIQUE SCAN UK_ASSET_NO
TABLE ACCESS BY INDEX ROWID MEDICAL_ASSET

含义是:先通过唯一索引找到 ROWID,再回表拿完整列。

组合索引怎么设计

医疗资产列表页:

sql
select id, asset_no, asset_name, status
from medical_asset
where hospital_id = :hospital_id
  and status = :status
order by created_at desc
fetch first 20 rows only;

推荐:

sql
create index idx_asset_hospital_status_time
on medical_asset(hospital_id, status, created_at);

设计原因:

  1. hospital_id 是租户或机构过滤边界,优先缩小范围。
  2. status 是等值条件,继续缩小范围。
  3. created_at 服务排序和分页。
  4. 高频列表页比低频管理查询更值得专门建索引。

错误示例:

sql
create index idx_bad_asset_time_status
on medical_asset(created_at, status);

如果业务每次都按医院查,这个索引不能很好利用医院过滤,可能扫描大量时间范围数据。

Bitmap 索引为什么慎用于 OLTP

Bitmap 索引适合低基数字段,例如性别、是否有效、少量状态。

mermaid
flowchart TD
    A["status 字段只有少量值"] --> B["Bitmap 保存值到行集合的位图"]
    B --> C["多个条件可做位运算"]
    C --> D["数据仓库统计很快"]

但在 OLTP 高频更新中,它的问题是:

  1. 一个 Bitmap 片段可能对应多行。
  2. 更新一行可能影响一段位图。
  3. 并发更新不同记录也可能争用相同位图片段。
  4. 锁冲突和维护成本比普通 B-tree 更明显。

所以:

场景建议
交易表状态频繁变化优先 B-tree
报表库、数仓表、低更新可以考虑 Bitmap
低基数字段但高并发更新不要只因“基数低”就建 Bitmap

函数索引

如果 SQL 对列做函数,普通索引可能用不上。

sql
select id, asset_no
from medical_asset
where upper(asset_no) = upper(:asset_no);

可以建立函数索引:

sql
create index idx_asset_no_upper
on medical_asset(upper(asset_no));

注意:

  1. SQL 表达式必须和索引表达式匹配。
  2. 函数索引也要维护,写入有成本。
  3. 如果可以在入库时统一大小写,可能比查询时函数处理更简单。

优化器怎么选计划

Oracle 成本优化器不是凭感觉选计划,而是根据对象统计信息估算不同路径成本。

mermaid
flowchart TD
    A["SQL 文本"] --> B["解析和语义检查"]
    B --> C["查询转换"]
    C --> D["读取表、列、索引统计信息"]
    D --> E["估算选择率和基数"]
    E --> F["枚举访问路径和 Join 顺序"]
    F --> G["计算成本"]
    G --> H["选择成本较低计划"]

统计信息包括:

信息作用
表行数判断全表扫描成本
块数判断需要读多少数据块
列 NDV估算等值条件选择率
直方图处理数据倾斜
索引层级和叶子块估算索引访问成本
聚簇因子判断索引顺序和表块分布是否接近

收集统计信息:

sql
begin
  dbms_stats.gather_table_stats(
    ownname => user,
    tabname => 'MEDICAL_ASSET',
    cascade => true,
    method_opt => 'for all columns size auto'
  );
end;
/

执行计划怎么看

sql
explain plan for
select id, asset_no, asset_name
from medical_asset
where hospital_id = 1001
  and status = 'USED'
order by created_at desc;

select * from table(dbms_xplan.display);

真实执行后的计划更有价值:

sql
select * from table(
  dbms_xplan.display_cursor(null, null, 'allstats last')
);

常见节点:

节点含义排查重点
TABLE ACCESS FULL全表扫描大表高频 SQL 是否不可接受
INDEX UNIQUE SCAN唯一索引精确定位通常很好
INDEX RANGE SCAN索引范围扫描范围过大仍慢
TABLE ACCESS BY INDEX ROWID根据 ROWID 回表回表次数太多会慢
NESTED LOOPS嵌套循环外层行数大时风险高
HASH JOIN哈希连接PGA/TEMP 压力
SORT ORDER BY排序是否可用索引避免排序

绑定变量与计划稳定性

绑定变量好处:

sql
select id, asset_no
from medical_asset
where hospital_id = :hospital_id
  and status = :status;
好处说明
SQL 文本稳定更容易复用游标
减少硬解析降低 CPU 和 Shared Pool 压力
降低注入风险参数和 SQL 结构分离

但如果数据倾斜明显,比如大医院有 1000 万资产,小医院只有 100 条,复用同一个计划可能不适合所有参数。处理方向包括统计信息、直方图、SQL Plan Baseline、拆 SQL、必要时提示或重写。

商业项目索引设计

场景查询特点索引建议
资产详情asset_no = ?唯一 B-tree
医院资产列表hospital_id + status + created_at组合 B-tree
审批待办assignee + status + created_at组合 B-tree
低更新报表状态统计状态低基数、大量聚合数仓可考虑 Bitmap
大表按日期归档时间范围扫描分区 + 本地索引
大小写不敏感查询upper(code)函数索引或规范化字段

可运行 Demo

sql
create table medical_asset (
  id number primary key,
  asset_no varchar2(64) not null,
  hospital_id number not null,
  status varchar2(20) not null,
  asset_name varchar2(128) not null,
  created_at date default sysdate not null,
  constraint uk_medical_asset_no unique(asset_no),
  constraint ck_medical_asset_status
    check (status in ('IDLE', 'USED', 'REPAIR', 'SCRAPPED'))
);

create sequence seq_medical_asset start with 1 increment by 1;

create index idx_asset_hospital_status_time
on medical_asset(hospital_id, status, created_at);

insert into medical_asset(id, asset_no, hospital_id, status, asset_name)
values(seq_medical_asset.nextval, 'A001', 1001, 'USED', 'CT-001');

commit;

explain plan for
select id, asset_no, asset_name
from medical_asset
where hospital_id = 1001
  and status = 'USED'
order by created_at desc;

select * from table(dbms_xplan.display);

观察:

  1. 是否使用 idx_asset_hospital_status_time
  2. 是否出现 TABLE ACCESS BY INDEX ROWID
  3. 如果返回列很多,回表是否可能成为瓶颈。

常见坑

后果正确做法
只看有没有索引回表多仍然慢看执行计划和实际行数
高频更新字段建 Bitmap锁竞争严重OLTP 优先 B-tree
函数包裹索引列普通索引失效函数索引或字段规范化
统计信息过期计划选错定期收集统计信息
绑定变量一律认为好数据倾斜下计划不适配结合直方图和计划管理
组合索引列顺序随便放过滤和排序无法充分利用按高频 SQL 设计

面试标准回答

text
Oracle 常见索引有 B-tree、Bitmap、函数索引和组合索引。B-tree 索引由 root、branch、leaf block 组成,leaf block 保存 key 和 ROWID,查询时先通过索引定位 ROWID,再访问表数据块。Bitmap 索引适合低基数字段和少更新的数据仓库统计,不适合高并发 OLTP 频繁更新。Oracle 优化器基于统计信息、选择率、基数和成本选择执行计划,统计信息不准、数据倾斜、绑定变量复用不合适都可能导致计划错误。排查时要看执行计划中的访问路径、回表、Join、排序和估算行数。

追问:

  1. ROWID 在索引访问里起什么作用?
  2. Bitmap 索引为什么不适合 OLTP?
  3. 绑定变量为什么既能优化又可能带来问题?
  4. 统计信息不准会导致什么执行计划问题?

关联知识点

知识点说明
Oracle 从零到生产级掌握主学习线
核心全过程原理SQL 执行、解析、内存、事务全链路
事务与锁锁、一致性读、Undo/Redo
性能排查等待事件、AWR/ASH、慢 SQL