解释计划:通过索引varchar2字段从100K记录表中选择不使用索引

时间:2012-10-14 19:44:05

标签: oracle sql-execution-plan

我同意Tom Kyte关于全表扫描不是一个邪恶的source,但只有当一个表相对较小时。因此,拥有这样一个表的附加索引是多余的。但是,具有100.000条记录的表不应该被视为小,但是从这样的表中解释计划显示执行的表全扫描。所以,我在本地安装了Oracle的笔记本电脑上进行了小型实验:

1)首先,创建了my_table:

CREATE TABLE my_table(
  "ID" NUMBER NOT NULL ENABLE, 
  "INVOICE_NO" VARCHAR2(10), 
  CONSTRAINT "test _PK" PRIMARY KEY ("ID")
)

2)然后,为invoice_no列创建索引(因为将使用它进行过滤):

CREATE INDEX "my_table_index1" ON my_table (invoice_no)

3)然后,插入100K记录:

DECLARE
  mod_val NUMBER;
BEGIN
  FOR i IN 1..100000 LOOP
    mod_val := MOD(i,6);
    IF (mod_val = 0) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-110');
    ELSIF (mod_val = 1) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-111');
    ELSIF (mod_val = 2) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-112');
    ELSIF (mod_val = 3) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-113');
    ELSIF (mod_val = 4) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-114');
    ELSIF (mod_val = 5) THEN
      INSERT INTO my_table (ID, INVOICE_NO) VALUES (i, '5570-115');
    END IF; 
  END LOOP;
  COMMIT;
END;

4)然后更新了一条随机记录(仅用于强调选择):

BEGIN
  UPDATE my_table SET INVOICENO = 'exception' WHERE id = 50000;
  COMMIT;
END;

5)然后用解释计划进行选择:

EXPLAIN PLAN FOR
  SELECT * FROM my_table WHERE invoice_no = 'exception';

6)然后抓住统计数据:

 SELECT * FROM TABLE(dbms_xplan.display);

7)并得到了结果:

"PLAN_TABLE_OUTPUT"
"Plan hash value: 3804444429"
" "
"------------------------------------------------------------------------------"
"| Id  | Operation         | Name     | Rows  | Bytes | Cost (%CPU)| Time     |"
"------------------------------------------------------------------------------"
"|   0 | SELECT STATEMENT  |          | 83256 |  1626K|   103   (1)| 00:00:02 |"
"|   1 |  TABLE ACCESS FULL| MY_TABLE | 83256 |  1626K|   103   (1)| 00:00:02 |"
"------------------------------------------------------------------------------"
" "
"Note"
"-----"
"   - dynamic sampling used for this statement (level=2)"

结论:这很奇怪并且闻起来“神奇”,为什么Oracle决定不在invoice_no字段上使用索引并扫描83256条记录?我同意我的笔记本电脑没有超载并发用户,一个表的大小不是很大(包含数字和变量),但是,我不喜欢这种魔法,并且想知道这种行为的原因:)

UPDATE :我刚刚在invoice_no字段中为所有记录添加了一些虚拟值(见下文) - 只是为了增加表的大小,但是,表格全扫描仍然存在:         “aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa”

UPDATE2 :我还执行了分析表但结果是一样的:

ANALYZE TABLE my_table COMPUTE STATISTICS;

UPDATE3 :试图强制使用索引但结果是一样的(可能是错误的语法?):

EXPLAIN PLAN FOR 
  SELECT /*+ INDEX(my_table my_table_index1) */ * FROM my_table t WHERE invoice_no = 'exception'

UPDATE4 :最后,能够“告诉Oracle”使用索引 - 执行新的聚集表统计程序:

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS ( OWNNAME=>user
                             , TABNAME=>'my_table');
END;

以下是解释计划的输出:

"--------------------------------------------------------------------------------------"
"| Id  | Operation                   | Name            | Rows  | Bytes | Cost (%CPU)| Time     |"
"-----------------------------------------------------------------------------------------------"
"|   0 | SELECT STATEMENT            |                 |     1 |   294 |     5   (0)| 00:00:01 |"
"|   1 |  TABLE ACCESS BY INDEX ROWID| MY_TABLE        |     1 |   294 |     5   (0)| 00:00:01 |"
"|*  2 |   INDEX RANGE SCAN          | my_table_index1 |     1 |       |     4   (0)| 00:00:01 |"
"-----------------------------------------------------------------------------------------------"
" "
"Predicate Information (identified by operation id):"
"---------------------------------------------------"
" "
"   2 - access(""INVOICE_NO""='exception')"

因此,似乎Oracle决定在某个时间点使用某种查询方法,即使情况发生变化也不会更新它。我同意这一点,但奇怪的是,当我刚创建,插入和执行select时,为什么它没有为这个测试用例选择正确的方法。我们是否始终至少在开始时执行DBMS_STATS.GATHER_TABLE_STATS告诉Oracle使用最佳查询方法?

3 个答案:

答案 0 :(得分:2)

最初创建表格时,INVOICE_NO只有7个不同的值。因此,默认情况下,Oracle期望针对仅指定INVOICE_NO上的谓词的表的查询将返回大约每7行中的1行(约占行的14.3%),这通常意味着表扫描比索引扫描更有效(确切的截止点将取决于许多不同的参数 - 如果某些系统希望检索15%的行,则完全有可能选择索引扫描。) p>

最初运行查询时,表上没有统计信息,因此Oracle被迫进行动态采样(注意注释"用于此语句的动态采样(level = 2)"在查询计划)。这旨在快速收集优化器的一些基本统计信息。但是,动态采样旨在优化速度而不是精确度,因此统计数据的质量通常不是最佳的。在您的第一个示例中,Oracle估计查询返回83256行(占总数的83​​.2%),这可能意味着它高估了表中的行数并低估了INVOICE_NO列中不同值的数量

你是否使用

收集统计数据
BEGIN
  DBMS_STATS.GATHER_TABLE_STATS ( OWNNAME=>user
                             , TABNAME=>'my_table');
END;

在第4步之后但在第5步之前,假设您没有更改任何DBMS_STATS默认设置,您可能会有更好的统计信息,但您仍然(很可能)会进行表扫描。 Oracle估计会有14286行(7行中的1行)。

SQL> SELECT * FROM TABLE(dbms_xplan.display);

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------
Plan hash value: 3804444429

------------------------------------------------------------------------------
| Id  | Operation         | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |          | 14286 |   195K|   104   (2)| 00:00:02 |
|*  1 |  TABLE ACCESS FULL| MY_TABLE | 14286 |   195K|   104   (2)| 00:00:02 |
------------------------------------------------------------------------------

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

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------

   1 - filter("INVOICE_NO"='exception')

为了获得更好的计划,您需要在INVOICE_NO列上添加直方图。这将告诉Oracle INVOIE_NO中的数据不是均匀分布的,因此某些值(即"异常")比其他列更具选择性。收集统计信息时,您可以指定要在所有索引列上的各列上收集直方图,或者可以指定希望Oracle自动确定哪些列需要直方图(我们将返回到片刻)。如果要强制Oracle在所有索引列上收集直方图,

SQL> exec dbms_stats.gather_table_stats( 'SCOTT', 
                                         'MY_TABLE', 
                                          method_opt => 'FOR ALL INDEXED COLUMNS SIZE 254' );

PL/SQL procedure successfully completed.

假设INVOICE_NO有255个或更少的不同值,这个直方图将让Oracle准确跟踪每个不同值的共同点(如果有超过255个不同的值,那么Oracle将需要组合相邻的值这可能会使您的直方图不准确)。

在默认的Oracle 10.2或11.2安装中,默认的method_opt设置为" FOR ALL COLUMNS SIZE AUTO"。这告诉Oracle收集它确定适合的任何列的直方图。为此,Oracle会查找数据分布严重偏离的列以及该列在谓词中出现的位置。所以早些时候,当我谈论在第4步和第5步之间收集统计数据时,Oracle并没有在INVOICE_NO上收集直方图,因为虽然它知道数据是偏斜的,但它并不知道你要去根据该列查询表。

在步骤7之后,如果您使用完全相同的命令再次收集统计信息

BEGIN
  DBMS_STATS.GATHER_TABLE_STATS ( OWNNAME=>user
                             , TABNAME=>'my_table');
END;

然后Oracle会看到针对MY_TABLE的查询在共享池中的INVOICE_ID上有一个谓词。这将使它意识到INVOICE_NO满足两个条件以获得直方图,所以这一次,它将在INVOICE_NO上收集直方图。这允许优化器意识到您的查询只返回1行并意识到索引扫描将是最有效的计划

SQL> SELECT * FROM TABLE(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 3377519735

-----------------------------------------------------------------------------------------------
| Id  | Operation                   | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                 |     1 |    14 |     4   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| MY_TABLE        |     1 |    14 |     4   (0)| 00:00:01 |
|*  2 |   INDEX RANGE SCAN          | my_table_index1 |     1 |       |     3   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

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

   2 - access("INVOICE_NO"='exception')

所以,好消息是Oracle足够智能,最终可以确定它需要一个直方图,以便为此查询生成最佳计划。坏消息是,如果你通过在表格中填充数据时收集包括直方图在内的统计数据来告诉Oracle,那么在Oracle找出它需要的内容之前,你可能会得到糟糕的计划。

在实际系统中,您通常会在绝大多数查询中使用绑定变量而不是文字。在针对具有直方图的列的查询中使用绑定变量时,会引入一组新问题。如果您的申请中有查询

 SELECT * 
   FROM my_table 
  WHERE invoice_no = :1;

如果你绑定一个值,你会想要一个表扫描" 5570-110"但是如果你绑定一个" exception"的值,你会想要一个索引扫描。在Oracle 10.2中,Oracle将绑定变量peeking,这意味着当Oracle进行硬解析时,它将查看绑定变量的值并生成一个优化该绑定值的计划。不幸的是,在10g中,每个查询只能有一个计划,因此您一次只能为两个案例中的一个获得最佳计划,而您获得的计划将取决于首先遇到绑定值的运气。在11g中,您可以获得自适应游标共享,其中Oracle为不同的绑定变量值维护多个查询计划,尽管这会引入一些您需要注意的额外复杂性。

哦,除此之外,你的提示没有用,因为你的索引使用了区分大小写的名称。您的提示需要使用区分大小写的索引名称。您还需要使用别名而不是表名

SELECT /*+ INDEX(t "my_table_index1") */ * 
  FROM my_table t 
 WHERE invoice_no = 'exception'

这是使用区分大小写的标识符的(很多)原因之一通常是一个主要的痛苦。

答案 1 :(得分:1)

  

我们是否始终至少在开始时执行DBMS_STATS.GATHER_TABLE_STATS告诉Oracle使用最佳查询方法?

不,不一定。

Oracle会自动执行此操作(除非您已将其关闭)。但在默认安装中,它只会每天收集一次统计信息。因此,在大负载之后,统计数据并不是最新的 - 尽管我希望在填充表之后立即使用11.x来使用索引。

因此,每当您更改主要部分数据时,如果您更改了更多表格,最好运行dbms_stats.gather_table_stats()甚至dbms_stats.gather_schema_stats()

“一天一次”策略适用于大多数工作负载,但如果您有更快的更改条件,则可能需要调整Oracle计算统计信息的参数。

有关详细信息,请参阅手册:http://docs.oracle.com/cd/E11882_01/server.112/e16638/stats.htm#g49431

答案 2 :(得分:1)

我的测试得到了这个,没有任何统计信息更新:计划:

SELECT STATEMENT ALL_ROWS:费用:1字节:20基数:1

TABLE ACCESS BY INDEX ROWID TABLE SYS.MY_TABLE:费用:1字节:20基数:1

INDEX RANGE SCAN INDEX SYS.my_table_index1:费用:1基数:1