在创建主键约束的同时,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。
From the Oracle docs(强调我的):
与索引范围扫描相比,索引唯一扫描必须具有
与索引键关联的 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 |
------------------------------------------------------