【问题标题】:Optimize Oracle Between Date Statement优化 Oracle Between Date 语句
【发布时间】:2012-11-09 18:47:27
【问题描述】:

我有一个 oracle SQL 查询,它选择当天的条目,如下所示:

SELECT   [fields] 
FROM     MY_TABLE T 
WHERE    T.EVT_END BETWEEN TRUNC(SYSDATE) 
                       AND TRUNC(SYSDATE) + 86399/86400
  AND    T.TYPE = 123

EVT_END 字段是DATE 类型,而T.TYPENUMBER(15,0)

我确信随着表数据大小(和持续时间)的增加,日期约束将比类型约束减少结果集的一个更大的因素。 (因为种类非常有限)

所以出现的基本问题是,选择什么索引可以更快地选择当前日期。我特别想知道TRUNC(T.EVT_END) 上的功能索引与T.EVT_END 上的普通索引的优缺点是什么。使用功能索引时,查询看起来像这样:

SELECT   [fields] 
FROM     MY_TABLE T 
WHERE    TRUNC(T.EVT_END) = TRUNC(SYSDATE) 
  AND    T.TYPE = 123

因为其他查询使用提到的日期约束而没有额外的类型选择(或者可能使用其他一些字段),所以多列索引对我没有多大帮助。

谢谢,非常感谢您的提示。

【问题讨论】:

  • 您忘记包含 EVT_END 数据类型。
  • 如果分区是一种选择,那么每日间隔分区可能非常适合。
  • 获取范围内所有日期和时间的安全方法是(伪代码):d >= start AND d < (end + 1 day)。即使d 是结束日期最后一秒的最后一部分,这也有效。

标签: sql oracle indexing between


【解决方案1】:

您的索引应该是 TYPE,EVT_END。

CREATE INDEX PIndex
ON MY_TABLE (TYPE, EVT_END)

优化器计划将首先通过该索引找到 TYPE=123 部分。然后在 TYPE=123 下,它会对 EVT_END 时间戳进行排序,因此它可以在 b-tree 中搜索范围内的第一个日期,并依次遍历这些日期,直到数据超出范围。

【讨论】:

  • 您可能忽略了我在急于快速回答时取消了多列索引资格的部分。我的主要问题是EVT_END 上的索引与TRUNC(EVT_END) 上的索引有何不同。但是,您的回答有助于让我相信普通 B-Tree 用于顺序扫描的可能性。
  • 多列索引没有问题,只是因为其他查询使用了没有TYPE的EVT_END。为什么不在 EVT_END 上创建一个索引,在 TYPE,EVT_END 上创建另一个索引?
  • 函数索引可能(并且它只是一个 MAY)减少整体查询 IO。为什么?聚类因子可以很好地提高。这意味着如果表中的数据不是按 EVT_END 顺序自然排序,您应该会看到 IO 下降。不过,在处理总时间时,IO 的下降可能并不显着。您是否也尝试过使用不使用 TYPE 的查询进行索引(TYPE、EVT_END)?我问的原因是您说它的基数非常低,因此您可能会发现查询使用索引跳过扫描。另一个选项是索引(EVT_END,TYPE)。此查询效率不高,但仍然可以接受。
  • @DazzaL 使用基于函数的索引来减少聚类因子是一个有趣的想法。我猜这个问题的答案涉及比较更好的聚类因子的性能优势与 FBI 更大的缺点。您应该在新答案中扩展您的想法。
  • FBI 根本不会变大。它仍然只存储一个日期,所以每个日期仍然是 7 个字节。
【解决方案2】:

根据上面的查询,功能索引将不提供任何值。对于要使用的功能索引,查询中的谓词需要编写如下:

SELECT [fields] 
FROM MY_TABLE T 
WHERE TRUNC(T.EVT_END) BETWEEN TRUNC(SYSDATE) AND TRUNC(SYSDATE) + 86399/86400
  AND T.TYPE = 123

列 EVT_END 上的功能索引被忽略。在 EVT_END 日期有一个正常的索引会更好。对于要使用的功能索引,条件的左侧必须与功能索引的声明相匹配。我可能会将查询写为:

SELECT [fields] 
FROM MY_TABLE T 
WHERE T.EVT_END BETWEEN TRUNC(SYSDATE) AND TRUNC(SYSDATE+1)
  AND T.TYPE = 123

我会创建以下索引:

CREATE INDEX bla on MY_TABLE( EVT_END )

这是假设您尝试查找在一天内结束的事件。

【讨论】:

  • 注意:如果您使用 TRUNC(T.EVT_END),则不需要 BETWEEN,因为它只会匹配或不匹配 TRUNC(SYSDATE)。
  • @Stu 我相信他的问题已经暗示他会改变谓词:“......围绕 T.EVT_END 移动 TRUNC 函数......”。 (虽然如果对问题进行一些修改以使其更清楚他想要比较的内容可能会有所帮助。)
【解决方案3】:

结果

如果您的索引被缓存,则基于函数的索引表现最佳。如果您的索引未缓存,则基于压缩函数的索引表现最佳。

以下是我的测试代码生成的相对时间。越低越好。您无法比较缓存和非缓存之间的数字,它们是完全不同的测试。

                 In cache      Not in cache
Regular          120           139
FBI              100           138
Compressed FBI   126           100

我不确定为什么 FBI 的表现优于常规索引。 (尽管它可能与您所说的相等谓词与范围有关。您可以看到常规索引在其解释计划中有一个额外的“过滤”步骤。)压缩的 FBI 有一些额外的开销来解压缩块。当一切都已经在内存中时,这少量的额外 CPU 时间是相关的,并且 CPU 等待是最重要的。但是当什么都没有缓存,而 IO 更重要时,压缩后的 FBI 减少的空间就大有帮助了。

假设

这个问题似乎有很多困惑。按照我的阅读方式,您只关心这个特定的查询,并且您想知道基于函数的索引还是常规索引会更快。

我假设您不关心可能从该索引中受益的其他查询、维护索引所花费的额外时间、开发人员是否记得使用它,或者优化器是否选择索引。 (如果优化器没有选择索引,我认为这不太可能,您可以添加提示。)如果这些假设中有任何错误,请告诉我。

代码

--Create tables. 1 = regular, 2 = FBI, 3 = Compressed FBI
create table my_table1(evt_end date, type number) nologging;
create table my_table2(evt_end date, type number) nologging;
create table my_table3(evt_end date, type number) nologging;

--Create 1K days, each with 100K values
begin
    for i in 1 .. 1000 loop
        insert /*+ append */ into my_table1
        select sysdate + i - 500 + (level * interval '1' second), 1
        from dual connect by level <= 100000;

        commit;
    end loop;
end;
/
insert /*+ append */ into my_table2 select * from my_table1;
insert /*+ append */ into my_table3 select * from my_table1;

--Create indexes
create index my_table1_idx on my_table1(evt_end);
create index my_table2_idx on my_table2(trunc(evt_end));
create index my_table3_idx on my_table3(trunc(evt_end)) compress;

--Gather statistics
begin
    dbms_stats.gather_table_stats(user, 'MY_TABLE1');
    dbms_stats.gather_table_stats(user, 'MY_TABLE2');
    dbms_stats.gather_table_stats(user, 'MY_TABLE3');
end;
/

--Get the segment size.
--This shows the main advantage of a compressed FBI, the lower space.
select segment_name, bytes/1024/1024/1024 GB
from dba_segments
where segment_name like 'MY_TABLE__IDX'
order by segment_name;

SEGMENT_NAME     GB
MY_TABLE1_IDX    2.0595703125
MY_TABLE2_IDX    2.0478515625
MY_TABLE3_IDX    1.1923828125


--Test block.
--Uncomment different lines to generate 6 different test cases.
--Regular, Function-based, and Function-based compressed.  Both cached and not-cached.
declare
    v_count number;
    v_start_time number;
    v_total_time number := 0;
begin
    --Uncomment two lines to test the server when it's "cold", and nothing is cached.
    for i in 1 .. 10 loop
        execute immediate 'alter system flush buffer_cache';
    --Uncomment one line to test the server when it's "hot", and everything is cached.
    --for i in 1 .. 1000 loop

        v_start_time := dbms_utility.get_time;

        SELECT COUNT(*)
        INTO   V_COUNT
        --#1: Regular
        FROM   MY_TABLE1 T 
        WHERE  T.EVT_END BETWEEN TRUNC(SYSDATE) AND TRUNC(SYSDATE) + 86399/86400;
        --#2: Function-based
        --FROM   MY_TABLE2 T 
        --WHERE  TRUNC(T.EVT_END) = TRUNC(SYSDATE);
        --#3: Compressed function-based
        --FROM   MY_TABLE3 T 
        --WHERE  TRUNC(T.EVT_END) = TRUNC(SYSDATE);

        v_total_time := v_total_time + (dbms_utility.get_time - v_start_time);
    end loop;

    dbms_output.put_line('Seconds: '||v_total_time/100);
end;
/

测试方法

我每个块至少运行 5 次,在运行类型之间交替(以防我的机器上仅部分时间运行某些东西),抛出高运行时间和低运行时间,然后取平均值。上面的代码没有包含所有这些逻辑,因为它会占用这个答案的 90%。

其他需要考虑的事项

还有很多其他的事情需要考虑。我的代码假设数据以非常索引友好的顺序插入。如果这不是真的,情况将完全不同,因为压缩可能根本没有帮助。

这个问题的最佳解决方案可能是通过分区完全避免它。对于读取相同数量的数据,全表扫描比索引读取快得多,因为它使用多块 IO。但是分区也有一些缺点,比如需要大量资金 需要购买选件和额外的维护任务。例如,提前创建分区,或使用间隔分区(还有一些其他奇怪的问题)、收集统计信息、延迟创建段等。

最终,您需要自己进行测试。但请记住,即使是这样一个简单的选择,测试也很困难。您需要真实的数据、真实的测试和真实的环境。真实的数据比听起来要难得多。使用索引,您不能简单地复制数据并立即构建索引。 create table my_table1 as select * fromcreate index ... 将创建不同的索引,而不是创建表并按特定顺序执行一堆插入和删除。

【讨论】:

    【解决方案4】:

    @S1lence:
    我相信您提出这个问题背后会有相当长的思考时间。而且,我花了很多时间在这里发布我的答案,因为我不喜欢发布任何猜测的答案。
    我想分享我在针对 FBI 的日期列上选择正常索引的网络搜索经验。
    根据我对下面link的理解,如果你肯定要使用TRUNC功能,那么你可以去掉普通索引的选项,作为这个咨询网站空间说:
    即使列可能有索引,trunc 内置函数也会使索引无效,从而导致不必要的 I/O 的次优执行。
    我想这一切都清楚了。如果你肯定要使用TRUNC,你必须和FBI一起去。请让我知道我的回复是否有意义。

    Oracle SQL Tuning with function-based indexes

    干杯,
    拉克什马南 C.

    【讨论】:

      【解决方案5】:

      是否使用基于函数的索引的决定应该由您计划如何编写查询来决定。如果您对日期列的所有查询都将采用TRUNC(EVT_END) 的形式,那么您应该使用 FBI。但是,通常最好只在 EVT_END 上创建索引,原因如下:

      • 它将更加可重用。如果您有查询检查一天中的特定时间,则不能使用 TRUNC。
      • 仅使用日期的索引中会有更多不同的键。如果您在一天中插入了 1,000 个不同的时间,EVT_END 将有 1,000 个不同的键,而 TRUNC(EVT_END) 将只有 1 个(这假设您存储的是时间组件,而不是所有日期的午夜 - 在第二种情况每天都有 1 个不同的密钥)。这很重要,因为索引具有的不同值越多,索引的选择性就越高,优化器使用它的可能性就越大(参见this
      • 聚类因子可能不同,但在使用 trunc 的情况下,它更有可能上升,而不是像其他一些 cmets 中所述的下降。这是因为聚类因子表示索引中值的顺序与数据物理存储的匹配程度。如果您的所有数据都按日期顺序插入,则普通索引将与物理数据具有相同的顺序。但是,TRUNC 一天中的所有时间都将映射到相同的值,因此索引中的行顺序可能与物理数据完全不同。同样,这意味着 trunc 索引不太可能被使用。然而,这将完全取决于您的数据库的插入/删除模式。
      • 开发人员更有可能针对未将 trunc 应用于列的位置编写查询(根据我的经验)。这是否适用于您将取决于您的开发人员和您对已部署 SQL 的质量控制。

      就个人而言,我首先会选择 Marlin 的 TYPE, EVT_END 答案。但是,您需要在您的环境中对此进行测试,看看这如何影响此查询以及使用 TYPE 和 EVT_END 列的所有其他查询。

      【讨论】:

        猜你喜欢
        • 2013-04-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-08-13
        • 2010-10-08
        • 2016-11-04
        相关资源
        最近更新 更多