了解索引对其产生巨大影响的查询的特征
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 扫描要好.
测试用例是一个良好的开端,但还需要做一些其他事情才能获得明显的性能差异:
实际数据大小。 两个小值的一百万行很小 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';
收集统计信息。总是在大 table 更改后收集统计信息。除非优化器具有 table、列和索引统计信息,否则它无法很好地完成工作。
begin
dbms_stats.gather_table_stats(user, 'MANY_STUDENTS');
end;
/
使用提示强制制定好计划和坏计划。 通常应避免使用优化器提示。但是为了快速比较不同的计划,它们可能有助于修复错误的计划。
例如,这将强制进行完整的 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);
刷新缓存。 缓存可能是索引和完整 table 扫描查询花费相同时间的罪魁祸首。如果 table 完全适合内存,那么读取所有行的时间可能几乎太短而无法测量。与解析查询或通过网络发送简单结果的时间相比,这个数字可能会相形见绌。
此命令将强制 Oracle 从缓冲区缓存中删除几乎所有内容。这将帮助您测试 "cold" 系统。 (您可能不希望 运行 在生产系统上使用此语句。)
alter system flush buffer_cache;
但是,这不会刷新操作系统或 SAN 缓存。也许 table 真的适合生产时的内存。如果您需要测试快速查询,可能需要将其放入 PL/SQL 循环中。
多个,交替运行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';
测试很难。 将像样的性能测试放在一起很困难。以上规则只是一个开始。
乍一看这似乎有点矫枉过正。但这是一个复杂的话题。而且我已经看到很多人,包括我自己,浪费了很多时间 "tuning" 一些基于糟糕测试的东西。最好现在就多花些时间来获得正确答案。
我想举一个例子来说明索引可以对查询执行时间产生巨大的(数量级)影响。经过数小时的反复试验,我仍然处于第一位。即即使执行计划显示使用索引,提速也不大。
因为我意识到我最好有一个大的 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);
和其他一些。
我看过 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 扫描要好.
测试用例是一个良好的开端,但还需要做一些其他事情才能获得明显的性能差异:
实际数据大小。 两个小值的一百万行很小 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';
收集统计信息。总是在大 table 更改后收集统计信息。除非优化器具有 table、列和索引统计信息,否则它无法很好地完成工作。
begin dbms_stats.gather_table_stats(user, 'MANY_STUDENTS'); end; /
使用提示强制制定好计划和坏计划。 通常应避免使用优化器提示。但是为了快速比较不同的计划,它们可能有助于修复错误的计划。
例如,这将强制进行完整的 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);
刷新缓存。 缓存可能是索引和完整 table 扫描查询花费相同时间的罪魁祸首。如果 table 完全适合内存,那么读取所有行的时间可能几乎太短而无法测量。与解析查询或通过网络发送简单结果的时间相比,这个数字可能会相形见绌。
此命令将强制 Oracle 从缓冲区缓存中删除几乎所有内容。这将帮助您测试 "cold" 系统。 (您可能不希望 运行 在生产系统上使用此语句。)
alter system flush buffer_cache;
但是,这不会刷新操作系统或 SAN 缓存。也许 table 真的适合生产时的内存。如果您需要测试快速查询,可能需要将其放入 PL/SQL 循环中。
多个,交替运行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';
测试很难。 将像样的性能测试放在一起很困难。以上规则只是一个开始。
乍一看这似乎有点矫枉过正。但这是一个复杂的话题。而且我已经看到很多人,包括我自己,浪费了很多时间 "tuning" 一些基于糟糕测试的东西。最好现在就多花些时间来获得正确答案。