在创建主键约束的同时,Oracle 也会创建一个索引来支持这一点。主键索引与基本索引相比有几个重要区别,即:
- 这里的所有值都保证是唯一的
- 表行中没有空值(形成 PK 的列)
这些原因是您看到的性能差异的关键。使用您的设置,我得到以下查询计划:
--fast version with PK
explain plan for
select count(*)
from
(
select *
from up_data u1
where up_nr = 1
or (up_nr = 0
and pk not in (select pk from up_data where up_nr = 1)
)
) u
/
select * from table(dbms_xplan.display(NULL, NULL,'BASIC +ROWS'));
-----------------------------------------------------
| Id | Operation | Name | Rows |
-----------------------------------------------------
| 0 | SELECT STATEMENT | | 1 |
| 1 | SORT AGGREGATE | | 1 |
| 2 | FILTER | | |
| 3 | INDEX FAST FULL SCAN| PK_UP_DATA | 103K|
| 4 | INDEX UNIQUE SCAN | PK_UP_DATA | 1 |
-----------------------------------------------------
alter table up_data drop constraint pk_up_data;
CREATE INDEX idx_up_data ON up_data(pk, up_nr);
/
--slow version with normal index
explain plan for
select count(*)
from
(
select *
from up_data u1
where up_nr = 1
or (up_nr = 0
and pk not in (select pk from up_data where up_nr = 1)
)
) u
/
select * from table(dbms_xplan.display(NULL, NULL,'BASIC +ROWS'));
------------------------------------------------------
| Id | Operation | Name | Rows |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 |
| 1 | SORT AGGREGATE | | 1 |
| 2 | FILTER | | |
| 3 | INDEX FAST FULL SCAN| IDX_UP_DATA | 103K|
| 4 | INDEX FAST FULL SCAN| IDX_UP_DATA | 1870 |
------------------------------------------------------
最大的区别是快速版本在第二次访问表数据时采用了INDEX UNIQUE SCAN,而不是INDEX FAST FULL SCAN。
来自 Oracle 文档(强调我的):
与索引范围扫描相比,索引唯一扫描必须具有与索引键关联的 0 或 1 个 rowid。当谓词使用相等运算符引用唯一索引键中的所有列时,数据库将执行唯一扫描。索引唯一扫描
在找到第一条记录后立即停止处理,因为不可能有第二条记录。
这种停止处理的优化被证明是这个例子中的一个重要因素。您的查询的快速版本:
- 全面扫描约 103,000 个索引条目
- 对于其中的每一个,在 PK 索引中找到一个匹配的行并停止进一步处理第二个索引
慢速版:
- 全面扫描约 103,000 个索引条目
- 对于其中的每一个,对 103,000 行执行另一次扫描,以查找是否有任何匹配项。
所以要比较所做的工作:
- 使用 PK,我们进行一次快速全扫描,然后对一个索引值进行 103,000 次查找
- 使用普通索引,我们进行一次快速全扫描,然后对 103,000 个索引条目进行 103,000 次扫描 - 工作量增加了几个数量级!
在此示例中,主键的唯一性和索引值的非空性都是获得性能优势所必需的:
-- create index as unique - we still get two fast full scans
drop index index idx_up_data;
create unique index idx_up_data ON up_data(pk, up_nr);
explain plan for
select count(*)
from
(
select *
from up_data u1
where up_nr = 1
or (up_nr = 0
and pk not in (select pk from up_data where up_nr = 1)
)
) u
/
select * from table(dbms_xplan.display(NULL, NULL,'BASIC +ROWS'));
------------------------------------------------------
| Id | Operation | Name | Rows |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 |
| 1 | SORT AGGREGATE | | 1 |
| 2 | FILTER | | |
| 3 | INDEX FAST FULL SCAN| IDX_UP_DATA | 103K|
| 4 | INDEX FAST FULL SCAN| IDX_UP_DATA | 1870 |
------------------------------------------------------
-- now the columns are not null, we see the index unique scan
alter table up_data modify (pk not null, up_nr not null);
explain plan for
select count(*)
from
(
select *
from up_data u1
where up_nr = 1
or (up_nr = 0
and pk not in (select pk from up_data where up_nr = 1)
)
) u
/
select * from table(dbms_xplan.display(NULL, NULL,'BASIC +ROWS'));
------------------------------------------------------
| Id | Operation | Name | Rows |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 |
| 1 | SORT AGGREGATE | | 1 |
| 2 | FILTER | | |
| 3 | INDEX FAST FULL SCAN| IDX_UP_DATA | 103K|
| 4 | INDEX UNIQUE SCAN | IDX_UP_DATA | 1 |
------------------------------------------------------