6

我有以下用例:

一个表存储来自一个人的更改数据和原始数据。我的查询旨在为每个人只获取一行:更改的数据(如果有的话),否则是原始数据。

我用 100k 行数据和 2k 更改数据填充了表。在我的表上使用主键时,查询运行时间不到半秒。如果我在表上放置索引而不是主键,则查询运行速度非常慢。所以我将使用主键,这一点毫无疑问。

我的问题是:为什么 PK 方法比带索引的方法快得多?

代码在这里:

drop table up_data cascade constraints purge;
/
create table up_data(
 pk integer,
 hp_nr integer,
 up_nr integer,
 ps_flag varchar2(1),
 ps_name varchar2(100)
 -- comment this out and uncomment the index below.
, constraint pk_up_data primary key (pk,up_nr) 
);
/
-- insert some data
insert into up_data
select rownum, 1, 0, 'A', 'tester_' || to_char(rownum) 
from dual 
connect by rownum < 100000;
/
-- insert some changed data
-- change ps_flag = 'B' and mark it with a change number in up_nr
insert into up_data
select rownum, 1, 1, 'B', 'tester_' || to_char(rownum) 
from dual 
connect by rownum < 2000;
/
-- alternative(?) to the primary key
-- CREATE INDEX idx_up_data ON up_data(pk, up_nr);
/

选择语句如下所示:

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

该语句可能是优化的目标,但目前它会保持这种状态。

4

1 回答 1

8

在创建主键约束的同时,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 |                                                                                                                                                                                                                                                       
------------------------------------------------------  
于 2013-06-16T15:44:32.653 回答