我试图想出一个示例,表明索引可能会对查询执行时间产生戏剧性(数量级)影响。经过几个小时的反复试验,我仍然在第一个方向。即,即使执行计划显示使用索引,加速也不大。
由于我意识到我最好有一个大表来让索引发挥作用,我编写了以下脚本(使用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);
我是否遗漏了一些区别于索引产生巨大差异的查询的特征?它是什么?
答案 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)
测试用例是一个良好的开端,但需要更多的东西才能获得明显的性能差异:
真实的数据大小。一百万行的两个小值是一个小表。使用一个很小的表,好的和坏的执行计划之间的性能差异可能并不重要。
下面的脚本将使表大小加倍,直到达到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';
收集统计信息。在大型表格更改后始终收集统计信息。优化器除非具有表,列和索引统计信息,否则无法正常工作。
begin
dbms_stats.gather_table_stats(user, 'MANY_STUDENTS');
end;
/
使用提示强制执行好的计划。通常应避免使用优化程序提示。但是为了快速比较不同的计划,他们可以帮助修复糟糕的计划。
例如,这将强制进行全表扫描:
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);
刷新缓存。缓存可能是索引和全表扫描查询背后花费相同时间的主要罪魁祸首。如果表完全适合内存,那么读取所有行的时间可能几乎太小而无法测量。在解析查询或通过网络发送简单结果时,数字可能相形见绌。
此命令将强制Oracle从缓冲区缓存中删除几乎所有内容。这将帮助您测试“冷”系统。 (您可能不希望在生产系统上运行此语句。)
alter system flush buffer_cache;
但是,这不会刷新操作系统或SAN缓存。也许桌子真的适合生产的记忆。如果您需要测试快速查询,可能需要将其放在PL / SQL循环中。
多次交替运行。后台发生了很多事情,比如缓存和其他进程。由于系统上发生了无关的更改,因此很容易得到糟糕的结果。
也许第一次运行需要花费很长时间才能将内容放入缓存中。或者也许在查询之间开始了一些巨大的工作。要避免这些问题,请交替运行这两个查询。运行它们五次,抛出高点和低点,并比较平均值。
例如,将以下语句复制并粘贴五次并运行它们。 (如果使用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';
测试很难。将性能测试结合起来很困难。以上规则只是一个开始。
起初看起来有点矫枉过正。但这是一个复杂的话题。而且我见过很多人,包括我自己,浪费了很多时间根据糟糕的测试来“调整”某些东西。最好现在花更多的时间来得到正确的答案。