了解索引与其产生巨大差异的查询的特征

时间:2018-05-13 20:46:20

标签: oracle indexing rdbms

我试图想出一个示例,表明索引可能会对查询执行时间产生戏剧性(数量级)影响。经过几个小时的反复试验,我仍然在第一个方向。即,即使执行计划显示使用索引,加速也不大。

由于我意识到我最好有一个大表来让索引发挥作用,我编写了以下脚本(使用Oracle 11g Express):

CREATE TABLE many_students (
  student_id NUMBER(11),
  city       VARCHAR(20)
);

DECLARE
  nStudents    NUMBER := 1000000;
  nCities      NUMBER := 10000;
  curCity      VARCHAR(20);
BEGIN
  FOR i IN 1 .. nStudents LOOP
    curCity := ROUND(DBMS_RANDOM.VALUE()*nCities, 0) || ' City';
    INSERT INTO many_students
    VALUES (i, curCity);
  END LOOP;
  COMMIT;
END;

然后我尝试了很多查询,例如:

select count(*) 
from many_students M 
where M.city = '5467 City'; 

select count(*) 
from many_students M1
join many_students M2 using(city);

以及其他一些。

我看过this帖子并认为我的查询符合回复中所述的要求。但是,在构建索引后,我尝试过的查询都没有显示出显着的改进:create index myindex on many_students(city);

我是否遗漏了一些区别于索引产生巨大差异的查询的特征?它是什么?

2 个答案:

答案 0 :(得分:3)

当数据库不需要转到表中的每一行来获取结果时,索引真的很棒。所以COUNT(*)不是最好的例子。以此为例:

alter session set statistics_level = 'ALL';
create table mytable as select * from all_objects;
select * from mytable where owner = 'SYS' and object_name = 'DUAL';

---------------------------------------------------------------------------------------
| Id  | Operation         | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |         |      1 |        |    300 |00:00:00.01 |      12 |
|   1 |  TABLE ACCESS FULL| MYTABLE |      1 |  19721 |    300 |00:00:00.01 |      12 |
---------------------------------------------------------------------------------------

因此,在这里,数据库执行全表扫描(TABLE ACCESS FULL),这意味着它必须访问数据库中的每一行,这意味着它必须从磁盘加载每个块。很多I / O.优化器猜测它会找到15000行,但我知道只有一行。

与此相比:

create index myindex on mytable( owner, object_name );
select * from mytable where owner = 'SYS' and object_name = 'JOB$';
select * from table( dbms_xplan.display_cursor( null, null, 'ALLSTATS LAST' ));

----------------------------------------------------------------------------------------------------------
| Id  | Operation                   | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
----------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |         |      1 |        |      1 |00:00:00.01 |       3 |      2 |
|   1 |  TABLE ACCESS BY INDEX ROWID| MYTABLE |      1 |      2 |      1 |00:00:00.01 |       3 |      2 |
|*  2 |   INDEX RANGE SCAN          | MYINDEX |      1 |      1 |      1 |00:00:00.01 |       2 |      2 |
----------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("OWNER"='SYS' AND "OBJECT_NAME"='JOB$')

这里,因为有一个索引,它会INDEX RANGE SCAN找到符合我们条件的表格的rowid。然后,它转到表本身(TABLE ACCESS BY INDEX ROWID),只查找我们需要的行,并且可以有效地执行,因为它有一个rowid。

更好的是,如果您正在寻找完全在索引中的内容,则扫描甚至不必返回基表。索引就足够了:

select count(*) from mytable where owner = 'SYS';
select * from table( dbms_xplan.display_cursor( null, null, 'ALLSTATS LAST' ));

------------------------------------------------------------------------------------------------
| Id  | Operation         | Name    | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |         |      1 |        |      1 |00:00:00.01 |      46 |     46 |
|   1 |  SORT AGGREGATE   |         |      1 |      1 |      1 |00:00:00.01 |      46 |     46 |
|*  2 |   INDEX RANGE SCAN| MYINDEX |      1 |   8666 |   9294 |00:00:00.01 |      46 |     46 |
------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("OWNER"='SYS')

因为我的查询涉及所有者列并且包含在索引中,所以它永远不需要返回基表来查找任何内容。因此索引扫描就足够了,然后它会进行聚合来计算行数。这种情况有点不完美,因为索引是(所有者,object_name)而不仅仅是所有者,但它肯定比在主表上进行全表扫描更好。

答案 1 :(得分:3)

测试用例是一个良好的开端,但需要更多的东西才能获得明显的性能差异:

  1. 真实的数据大小。一百万行的两个小值是一个小表。使用一个很小的表,好的和坏的执行计划之间的性能差异可能并不重要。

    下面的脚本将使表大小加倍,直到达到6400万行。我的机器大约需要20分钟。 (为了使其更快,对于更大的尺寸,您可以制作表格nologging并在插页中添加/*+ append */提示。

    --Increase the table to 64 million rows.  This took 20 minutes on my machine.
    insert into many_students select * from many_students;
    insert into many_students select * from many_students;
    insert into many_students select * from many_students;
    insert into many_students select * from many_students;
    insert into many_students select * from many_students;
    insert into many_students select * from many_students;
    commit;
    
    --The table has about 1.375GB of data.  The actual size will vary.
    select bytes/1024/1024/1024 gb from dba_segments where segment_name = 'MANY_STUDENTS';
    
  2. 收集统计信息。在大型表格更改后始终收集统计信息。优化器除非具有表,列和索引统计信息,否则无法正常工作。

    begin
        dbms_stats.gather_table_stats(user, 'MANY_STUDENTS');
    end;
    /
    
  3. 使用提示强制执行好的计划。通常应避免使用优化程序提示。但是为了快速比较不同的计划,他们可以帮助修复糟糕的计划。

    例如,这将强制进行全表扫描:

    select /*+ full(M) */ count(*) from many_students M where M.city = '5467 City';
    

    但您还需要验证执行计划:

    explain plan for select /*+ full(M) */ count(*) from many_students M where M.city = '5467 City';
    select * from table(dbms_xplan.display);
    
  4. 刷新缓存。缓存可能是索引和全表扫描查询背后花费相同时间的主要罪魁祸首。如果表完全适合内存,那么读取所有行的时间可能几乎太小而无法测量。在解析查询或通过网络发送简单结果时,数字可能相形见绌。

    此命令将强制Oracle从缓冲区缓存中删除几乎所有内容。这将帮助您测试“冷”系统。 (您可能不希望在生产系统上运行此语句。)

    alter system flush buffer_cache;
    

    但是,这不会刷新操作系统或SAN缓存。也许桌子真的适合生产的记忆。如果您需要测试快速查询,可能需要将其放在PL / SQL循环中。

  5. 多次交替运行。后台发生了很多事情,比如缓存和其他进程。由于系统上发生了无关的更改,因此很容易得到糟糕的结果。

    也许第一次运行需要花费很长时间才能将内容放入缓存中。或者也许在查询之间开始了一些巨大的工作。要避免这些问题,请交替运行这两个查询。运行它们五次,抛出高点和低点,并比较平均值。

    例如,将以下语句复制并粘贴五次并运行它们。 (如果使用SQL * Plus,请先运行set timing on。)我已经这样做了,并在每行之前发布了评论中的时间。

    --Seconds: 0.02, 0.02, 0.03, 0.234, 0.02
    alter system flush buffer_cache;
    select count(*) from many_students M where M.city = '5467 City';
    
    --Seconds: 4.07, 4.21, 4.35, 3.629, 3.54
    alter system flush buffer_cache;
    select /*+ full(M) */ count(*) from many_students M where M.city = '5467 City';
    
  6. 测试很难。将性能测试结合起来很困难。以上规则只是一个开始。

    起初看起来有点矫枉过正。但这是一个复杂的话题。而且我见过很多人,包括我自己,浪费了很多时间根据糟糕的测试来“调整”某些东西。最好现在花更多的时间来得到正确的答案。