6

我正在使用一个用作过滤器的索引列,方法是将它放在两个文字值之间。(该列位于索引的第二个位置,实际上使执行速度变慢;我稍后会处理)。

令我困惑的是,Oracle(11.2.0.3.0)根据提供给 to_date 的值和格式字符串的格式使用或忽略所述索引:

这忽略了索引:

SQL> SELECT *
  2  FROM gprs_history_import  gh
  3  WHERE start_call_date_time BETWEEN
  4      to_date('20140610 000000','yyyymmdd hh24miss') AND
  5      to_date('20140610 235959','yyyymmdd hh24miss')
  6  /

Execution Plan
----------------------------------------------------------
Plan hash value: 990804809

--------------------------------------------------------------------------------------------------------------
| Id  | Operation              | Name                | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
--------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |                     |   350 |   219K|   242K  (1)| 00:56:42 |       |       |
|   1 |  PARTITION RANGE SINGLE|                     |   350 |   219K|   242K  (1)| 00:56:42 |    74 |    74 |
|   2 |   PARTITION LIST ALL   |                     |   350 |   219K|   242K  (1)| 00:56:42 |     1 |     3 |
|*  3 |    TABLE ACCESS FULL   | GPRS_HISTORY_IMPORT |   350 |   219K|   242K  (1)| 00:56:42 |   220 |   222 |
--------------------------------------------------------------------------------------------------------------

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

   3 - filter("START_CALL_DATE_TIME"<=TO_DATE(' 2014-06-10 23:59:59', 'syyyy-mm-dd hh24:mi:ss'))

这个确实使用了索引(注意第 4 行中日期部分之后的空格):

SQL> SELECT *
  2  FROM gprs_history_import  gh
  3  WHERE start_call_date_time BETWEEN
  4      to_date('20140610 ','yyyymmdd ') AND
  5      to_date('20140610 235959','yyyymmdd hh24miss')
  6  /

Execution Plan
----------------------------------------------------------
Plan hash value: 464458373

---------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                            | Name                     | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
---------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                     |                          |   350 |   219K|  2795K  (1)| 10:52:15 |       |       |
|*  1 |  FILTER                              |                          |       |       |            |          |       |       |
|   2 |   PARTITION RANGE ITERATOR           |                          |   350 |   219K|  2795K  (1)| 10:52:15 |   KEY |    74 |
|   3 |    PARTITION LIST ALL                |                          |   350 |   219K|  2795K  (1)| 10:52:15 |     1 |     3 |
|   4 |     TABLE ACCESS BY LOCAL INDEX ROWID| GPRS_HISTORY_IMPORT      |   350 |   219K|  2795K  (1)| 10:52:15 |   KEY |   222 |
|*  5 |      INDEX SKIP SCAN                 | GPRS_HISTORY_IMPORT_IDX1 |     1 |       |  2795K  (1)| 10:52:15 |   KEY |   222 |
---------------------------------------------------------------------------------------------------------------------------------

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

   1 - filter(TO_DATE('20140610 ','yyyymmdd ')<=TO_DATE(' 2014-06-10 23:59:59', 'syyyy-mm-dd hh24:mi:ss'))
   5 - access("START_CALL_DATE_TIME">=TO_DATE('20140610 ','yyyymmdd ') AND "START_CALL_DATE_TIME"<=TO_DATE(' 2014-06-10
              23:59:59', 'syyyy-mm-dd hh24:mi:ss'))
       filter("START_CALL_DATE_TIME">=TO_DATE('20140610 ','yyyymmdd ') AND "START_CALL_DATE_TIME"<=TO_DATE(' 2014-06-10
              23:59:59', 'syyyy-mm-dd hh24:mi:ss'))

((1)中的过滤器看起来有点傻,好像Oracle看懂表达式)

同样,这个没有(我删除了尾随空格):

SQL> SELECT *
  2  FROM gprs_history_import  gh
  3  WHERE start_call_date_time BETWEEN
  4      to_date('20140610','yyyymmdd') AND
  5      to_date('20140610 235959','yyyymmdd hh24miss')
  6  /

Execution Plan
----------------------------------------------------------
Plan hash value: 990804809

--------------------------------------------------------------------------------------------------------------
| Id  | Operation              | Name                | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
--------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |                     |   350 |   219K|   242K  (1)| 00:56:42 |       |       |
|   1 |  PARTITION RANGE SINGLE|                     |   350 |   219K|   242K  (1)| 00:56:42 |    74 |    74 |
|   2 |   PARTITION LIST ALL   |                     |   350 |   219K|   242K  (1)| 00:56:42 |     1 |     3 |
|*  3 |    TABLE ACCESS FULL   | GPRS_HISTORY_IMPORT |   350 |   219K|   242K  (1)| 00:56:42 |   220 |   222 |
--------------------------------------------------------------------------------------------------------------

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

   3 - filter("START_CALL_DATE_TIME"<=TO_DATE(' 2014-06-10 23:59:59', 'syyyy-mm-dd hh24:mi:ss'))

在空格周围加上引号会阻止索引被使用。

是什么赋予了?

4

2 回答 2

2

好的 - 我会试一试,这主要是从可用信息中扣除的:

为什么 Oracle 选择不同的执行计划?

在您使用异常日期格式的第二个查询中,优化器似乎不知道结果日期的值是什么。您会看到过滤谓词:

1 - filter(TO_DATE('20140610 ','yyyymmdd ')<=TO_DATE(' 2014-06-10 23:59:59', 'syyyy-mm-dd hh24:mi:ss'))

这意味着优化器甚至不确定第一个日期是否小于第二个日期!这意味着优化器不知道返回的行数,只会使用通用计划而不考虑特定的统计信息。如果您有一个用户定义的函数 xyt() 将返回该范围的日期,那将是相同的。优化器无法知道会产生什么日期值 - 这意味着您获得了一个通用的通用计划,对于指定的任何日期范围都应该是相当不错的。

在第一种和第三种情况下,优化器似乎可以直接了解日期,并且可以通过使用统计信息来猜测日期范围内的行数。因此,虽然第二个查询对优化器来说就像BETWEEN X AND 3这个查询一样BETWEEN 1 AND 3 所以他优化查询计划以预测返回的行数!

奇怪的事情似乎是,查询优化器有奇怪的日期格式这样的问题,可以作为错误/改进请求提交......

但很重要的一点:

  1. 全表扫描不一定是一个糟糕的计划......以及使用索引并不总是更快!
  2. 查询计划中的成本与实际执行时间或性能没有任何直接关系——它是比较相同查询的不同计划的内部衡量标准(因此您无法比较不同查询的成本,例如查询 1,2 和3)

基本上,如果您从表中返回大量行,那么在许多情况下,没有索引访问的全表扫描会快得多,尤其是在某些分区上操作时!- 表扫描将仅访问匹配日期范围的 pertition - 因此仅访问相关日期并返回此分区中的所有行。这比查询每个单行的索引然后通过索引访问提取行要快得多...尝试分析查询 - 分区上的全表扫描应该快 3 倍,而 IO 更少

于 2014-06-27T10:02:51.453 回答
2

优化器或解析器中的错误导致某些日期格式将静态分区修剪降级为动态分区修剪。分区修剪更改导致不同的基数和成本,然后导致计划的许多其他部分发生重大变化。

该答案仅部分解释了问题并包含一些推测。希望它至少能说明问题是什么,不是什么。如果您确实需要完整的解释并想向 Oracle 提交服务请求,这至少是一个很好的起点。

术语和一些背景阅读

静态分区修剪是优化器在编译时确定将使用哪个分区。统计数据是针对每个分区的,从而产生更好的基数估计,从而产生更好的计划。例如,考虑一个按状态分区的表,其中 CANCELED 的分区很小,而 ACTIVE 的分区很大。知道使用哪个分区可以完全改变最优计划的连接顺序和访问方式。 Pstart并且Pstop在使用静态分区修剪时将是数值。

动态分区修剪是指优化器直到运行时才能确定分区。仅从所需的分区中检索数据,但在不知道使用哪个分区的特殊知识的情况下构建执行计划。一些分区统计估计将是所有可用分区的简单平均值。在上面按状态分区的表示例中,小分区和大分区的平均值都不能准确表示。当使用动态分区修剪时,要么PstartPstop将包含这个词。KEY

Oracle® Database VLDB and Partitioning Guide 包括 值得一读的关于数据类型转换的部分。例如,手册中的一个相关引用:

只有正确应用的 TO_DATE 函数才能保证数据库能够唯一确定日期值并将其潜在地用于静态修剪,这对于单分区访问特别有益。

示例架构和数据

这个简单的测试用例演示了这个问题。它还排除了常见的性能问题,例如缺少统计信息。

首先,创建一个包含 2 个分区的示例表,一个大一个小。

create table gprs_history_import(id number, start_call_date_time date)
partition by range (start_call_date_time)
(
    partition p_large values less than (date '2014-06-01'),
    partition p_small values less than (date '2014-07-01')
);

insert into gprs_history_import
select level, date '2014-05-01'
from dual connect by level <= 1000;

insert into gprs_history_import
select level, date '2014-06-01'
from dual connect by level <= 10;

begin
    dbms_stats.gather_table_stats(user, 'GPRS_HISTORY_IMPORT');
end;
/

select count(*) from gprs_history_import partition (p_large); -- 1000
select count(*) from gprs_history_import partition (p_small); --   10

静态到动态会导致糟糕的基数估计

静态基数估计是完美的 1000。第二个日期格式中的额外空间Pstop从 1 变为KEY。该计划从静态分区修剪变为动态分区修剪。动态估计是一个不准确的505,1000和10的平均值

为简单起见,此示例仅显示了错误的基数估计。没有必要显示查询运行缓慢,因为由于许多原因,错误的行估计不可避免地会导致错误的执行计划。

explain plan for select /* static partition pruning */ *
from gprs_history_import
where start_call_date_time < to_date('20140601 000000','yyyymmdd hh24miss');

select * from table(dbms_xplan.display);

Plan hash value: 452971246

--------------------------------------------------------------------------------------------------------------
| Id  | Operation              | Name                | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
--------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |                     |  1000 | 12000 |    16   (0)| 00:00:01 |       |       |
|   1 |  PARTITION RANGE SINGLE|                     |  1000 | 12000 |    16   (0)| 00:00:01 |     1 |     1 |
|   2 |   TABLE ACCESS FULL    | GPRS_HISTORY_IMPORT |  1000 | 12000 |    16   (0)| 00:00:01 |     1 |     1 |
--------------------------------------------------------------------------------------------------------------

explain plan for select /* dybnamic partition pruning */ *
from gprs_history_import
where start_call_date_time < to_date('20140601 ','yyyymmdd ');

select * from table(dbms_xplan.display);


Plan hash value: 2464174375

----------------------------------------------------------------------------------------------------------------
| Id  | Operation                | Name                | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT         |                     |   505 |  6060 |    29   (0)| 00:00:01 |       |       |
|   1 |  PARTITION RANGE ITERATOR|                     |   505 |  6060 |    29   (0)| 00:00:01 |     1 |   KEY |
|*  2 |   TABLE ACCESS FULL      | GPRS_HISTORY_IMPORT |   505 |  6060 |    29   (0)| 00:00:01 |     1 |   KEY |
----------------------------------------------------------------------------------------------------------------

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

   2 - filter("START_CALL_DATE_TIME"<TO_DATE('20140601 ','yyyymmdd '))

日期格式解析问题

现在对为什么查询从静态分区修剪移动到动态分区修剪进行一些猜测。

优化器何时可以使用静态和动态分区并不总是很明显。一般来说,文字允许静态修剪,而变量需要动态修剪。

--#1: Obviously static: It uses an unambiguous ANSI date literal.
select * from gprs_history_import where start_call_date_time = date '2000-11-01';

--#2: Obviously dyanmic: It uses a bind variable.
select * from gprs_history_import where start_call_date_time = :date;

--#3: Probably dynamic: The optimizer cannot always infer the literal value. 
select * from gprs_history_import where start_call_date_time = 
    (select date '2000-11-01' from dual);

--#4: Probably static: FEB is not always valid, but Oracle can figure that out.
select * from gprs_history_import where start_call_date_time = 
    to_date('01-FEB-2000', 'DD-MON-YYYY');

当您考虑围绕案例 #4 的所有性能和国际化问题时,就会清楚解析日期有多么困难。的值to_date('01-FEB-2000', 'DD-MON-YYYY')取决于几个 NLS 参数,例如NLS_DATE_LANGUAGE. 该查询对英语有效,但对德语无效。如果NLS_CALENDAR未设置为,则GREGORIAN即使是全数字日期格式也可能是错误的。该to_date字符串不是绑定值,但也不是明显的文字。

如果计算硬解析,则真实日期文字和格式化字符串之间的差异会更加明显。即使更改了语言,查询 #1 也不会强制进行硬解析,但查询 #4 会。这可以通过运行每个变体、更改语言然后运行select value from v$sesstat natural join v$statname where name = 'parse count (hard)' and sid = userenv('SID');​​.

Oracle 必须在某处有一个变量来表示“这不是绑定变量,但可能会导致基于 NLS 设置的不同计划”。该变量并不总是导致动态分区修剪,但一定有一些错误偶尔会破坏它。

于 2014-06-27T18:59:03.073 回答