了解索引对其产生巨大影响的查询的特征

Understanding characteristics of a query for which an index makes a dramatic difference

我想举一个例子来说明索引可以对查询执行时间产生巨大的(数量级)影响。经过数小时的反复试验,我仍然处于第一位。即即使执行计划显示使用索引,提速也不大。

因为我意识到我最好有一个大的 table 索引来发挥作用,所以我编写了以下脚本(使用 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);

和其他一些。

我看过 post 并且认为我的查询满足那里回复中所述的要求。但是,none 我尝试的查询在建立索引后显示出显着的改进:create index myindex on many_students(city);

我是否遗漏了一些特征,这些特征可以区分索引对其产生巨大影响的查询?这是什么?

当数据库不需要遍历 table 中的每一行来获取结果时,索引才真正发挥作用。所以 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 扫描 (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 来查找 table 的符合我们条件的 rowid。然后,它转到 table 本身 (TABLE ACCESS BY INDEX ROWID) 并仅查找我们需要的行并且可以高效地这样做,因为它有一个 rowid。

更好的是,如果您恰好要查找完全在索引中的内容,扫描甚至不必返回到基础 table。索引就够了:

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')

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

测试用例是一个良好的开端,但还需要做一些其他事情才能获得明显的性能差异:

  1. 实际数据大小。 两个小值的一百万行很小 table。对于这么小的 table,好的和坏的执行计划之间的性能差异可能并不重要。

    下面的脚本将使 table 大小加倍,直到达到 6400 万行。在我的机器上大约需要 20 分钟。 (为了让它更快,对于更大的尺寸,你可以制作 table 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. 收集统计信息。总是在大 table 更改后收集统计信息。除非优化器具有 table、列和索引统计信息,否则它无法很好地完成工作。

    begin
        dbms_stats.gather_table_stats(user, 'MANY_STUDENTS');
    end;
    /
    
  3. 使用提示强制制定好计划和坏计划。 通常应避免使用优化器提示。但是为了快速比较不同的计划,它们可能有助于修复错误的计划。

    例如,这将强制进行完整的 table 扫描:

    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. 刷新缓存。 缓存可能是索引和完整 table 扫描查询花费相同时间的罪魁祸首。如果 table 完全适合内存,那么读取所有行的时间可能几乎太短而无法测量。与解析查询或通过网络发送简单结果的时间相比,这个数字可能会相形见绌。

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

    alter system flush buffer_cache;
    

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

  5. 多个,交替运行s。后台发生了很多事情,比如缓存和其他进程。很容易得到不好的结果,因为系统上发生了不相关的更改。

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

    例如,将以下语句复制并粘贴五次并运行。 (如果使用 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. 测试很难。 将像样的性能测试放在一起很困难。以上规则只是一个开始。

    乍一看这似乎有点矫枉过正。但这是一个复杂的话题。而且我已经看到很多人,包括我自己,浪费了很多时间 "tuning" 一些基于糟糕测试的东西。最好现在就多花些时间来获得正确答案。