【问题标题】:Understanding characteristics of a query for which an index makes a dramatic difference了解索引对其产生显着影响的查询的特征
【发布时间】:2018-05-13 20:46:19
【问题描述】:

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

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

我是否遗漏了一些特征来区分索引对其产生显着影响的查询?这是什么?

【问题讨论】:

  • 查看解释计划 - 它显示了成本。使用正确的索引以及表格的正确统计信息,它会显示最佳路线
  • 注意,不要太看重COST 列。这是一个仅供内部使用的数字,并不是对所涉及的总成本的任意衡量。

标签: oracle indexing rdbms


【解决方案1】:

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

  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. 多次交替运行。后台发生了许多事情,例如缓存和其他进程。很容易得到不好的结果,因为系统上发生了一些不相关的改变。

    也许第一次运行需要很长时间才能将内容放入缓存中。或者,也许在查询之间开始了一些巨大的工作。为避免这些问题,请交替运行这两个查询。运行 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. 测试很困难。很难将体面的性能测试放在一起。以上规则只是一个开始。

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

【讨论】:

  • 感谢您的精彩回复。两个澄清(在两个单独的 cmets 中)。首先,在您在第 3 项(强制全扫描)中使用的示例中,规划器还有其他可用选项吗?我的意思是,我不知道有什么算法比线性搜索更好地在没有一些离线预处理的情况下在随机数数组中查找事物。
  • 二、一般什么时候加索引?我看到了四个选项:(1)第一次设计模式时,在这种情况下,可以(1-a)依赖直觉或(1-b)编写复杂的脚本来模拟预期数据,(2)当一些查询开始在已部署的数据库上运行太慢,在这种情况下,可以 (2-a) 尝试在生产数据库上构建索引或 (2-b) 制作副本(可能具有低隔离级别以在运行期间启用写入访问副本)并尝试副本上的索引。
  • 2) 默认情况下,Oracle 在夜间自动收集统计信息。所以在正常情况下不需要运行dbms_stats.gather_xxx_stats。但是,如果您遇到意外的性能行为或(如前所述)进行较大更改后,您应该使用它。
  • @AlwaysLearning 在这种情况下,只有两个现实的选项可用——全表扫描或索引范围扫描。但是还有许多其他选项可以通过不同的对象和选项启用,例如分区(散列、列表、范围)、快速全索引扫描、散列集群、位图索引、并行性和物化区域图。但是对于一个简单的等式谓词,一个普通的 BTree 索引几乎总是足够好的。
  • @AlwaysLearning 对于索引,根据我的经验,最常见的是结合使用 1a 和 2b,但可能在紧急情况下使用 2a。编写脚本来模拟数据加载很棘手,并且依赖于您对数据库如何工作的直觉,因此只需制作“感觉”正确的索引通常就足够了。而2b一般只是遵循标准部署流程,确保环境协调一致。使用 ONLINE 选项在生产环境中添加索引并检查空间几乎没有风险,因此 2a 并不像最初看起来那样危险。
【解决方案2】:

当数据库不需要遍历表中的每一行来获取结果时,索引真的会大放异彩。所以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')

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

【讨论】:

    猜你喜欢
    • 2019-05-03
    • 2011-04-24
    • 2013-11-21
    • 2019-09-27
    • 1970-01-01
    • 2018-06-10
    • 1970-01-01
    • 2013-07-11
    • 1970-01-01
    相关资源
    最近更新 更多