2

我同意 Tom Kyte 关于全表扫描不是邪恶来源的观点,但只有在表相对较小的情况下。因此,拥有此类表的附加索引是多余的。但是,不应将具有 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 条记录?我同意我的笔记本电脑没有因并发用户超载,表的大小不是很大(包含数字和 varchars),但是,我不喜欢这种魔法,想知道这种行为的原因:)

UPDATE : I just added some dummy value (see below) to invoice_no field to all records - just to increase the size of the table, however, table full scan remains: "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 似乎决定在某个时间点使用某种查询方法,并且即使情况发生变化也不会更新它。我同意这一点,但奇怪的是为什么当我刚刚创建、插入和执行选择时它没有为这个测试用例选择正确的方法。我们是否总是至少在开始时执行 DBMS_STATS.GATHER_TABLE_STATS 来告诉 Oracle 使用最佳查询方法?

4

3 回答 3

2

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

最初运行查询时,表上没有统计信息,因此 Oracle 被迫进行动态采样(请注意查询计划中的注释“用于此语句的动态采样(级别 = 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.

假设 有 255 个或更少的不同值INVOICE_NO,此直方图将使 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”,则需要进行表扫描,但如果绑定值“异常”,则需要进行索引扫描。在 Oracle 10.2 中,Oracle 将进行绑定变量窥视,这意味着当 Oracle 进行硬解析时,它将查看绑定变量的值并生成针对该绑定值进行优化的计划。不幸的是,在 10g 中,每个查询只能有一个计划,因此您一次只能针对两种情况之一获得最佳计划,而您获得的计划将取决于首先遇到哪个绑定值的运气。在 11g 中,您可以获得自适应游标共享,其中 Oracle 为不同的绑定变量值维护多个查询计划,尽管这会引入一些您需要注意的额外复杂性。

哦,顺便说一句,您的提示不起作用,因为您为索引使用了区分大小写的名称。您的提示需要使用区分大小写的索引名称。您还需要使用别名而不是表名

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

这是使用区分大小写的标识符通常是一个主要问题的(许多)原​​因之一。

于 2012-10-15T02:39:25.387 回答
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

于 2012-10-14T21:02:47.193 回答
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

于 2012-10-14T21:02:22.247 回答