【问题标题】:How to tune a range / interval query in Oracle?如何在 Oracle 中调整范围/间隔查询?
【发布时间】:2014-03-06 14:10:21
【问题描述】:

我有一张桌子A,间隔(COL1, COL2)

CREATE TABLE A (
  COL1 NUMBER(15) NOT NULL,
  COL2 NUMBER(15) NOT NULL,
  VAL1 ...,
  VAL2 ...
);
ALTER TABLE A ADD CONSTRAINT COL1_BEFORE_COL2 CHECK (COL1 <= COL2);

保证间隔是“独占的”,即它们永远不会重叠。换句话说,这个查询不会产生任何行:

SELECT *
FROM (
  SELECT
    LEAD(COL1, 1) OVER (ORDER BY COL1) NEXT,
    COL2
  FROM A
)
WHERE COL2 >= NEXT;

目前在(COL1, COL2) 上有一个索引。现在,我的查询如下:

SELECT /*+FIRST_ROWS(1)*/ *
FROM A
WHERE :some_value BETWEEN COL1 AND COL2
AND ROWNUM = 1

这对于:some_value 的低值表现良好(A 中的数百万条记录不到一毫秒),因为它们对索引非常有选择性。但是对于 :some_value 的高值,由于访问谓词的选择性较低,它的性能很差(几乎是一秒钟)。

执行计划对我来说似乎不错。由于现有索引已经完全覆盖了谓词,我得到了预期的INDEX RANGE SCAN

------------------------------------------------------
| Id  | Operation                    | Name | E-Rows |
------------------------------------------------------
|   0 | SELECT STATEMENT             |      |        |
|*  1 |  COUNT STOPKEY               |      |        |
|   2 |   TABLE ACCESS BY INDEX ROWID| A    |      1 |
|*  3 |    INDEX RANGE SCAN          | A_PK |        |
------------------------------------------------------

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

   1 - filter(ROWNUM=1)
   3 - access("VAL2">=:some_value AND "VAL1"<=:some_value)
       filter("VAL2">=:some_value)

3 中,很明显,访问谓词仅对:some_value 的低值具有选择性,而对于较高的值,过滤操作会“启动”索引。

无论:some_value 的值如何,有什么方法可以普遍提高这个查询的速度吗?如果需要进一步规范化,我可以完全重新设计表格。

【问题讨论】:

    标签: sql oracle oracle11g


    【解决方案1】:

    您的尝试很好,但遗漏了一些关键问题。

    让我们慢慢开始吧。我假设COL1 上有一个索引,实际上我不介意COL2 是否也包含在其中。

    由于您对数据的限制(尤其是非重叠),您实际上只希望行 before COL1&lt;= 的行有一些值....[- -休息一下--] 由COL1订购的

    这是classic Top-N query

    select *
      FROM ( select *
              from A
             where col1 <= :some_value
             order by col1 desc
           )
     where rownum <= 1;
    

    请注意,您必须使用ORDER BY 来获得明确的排序顺序。由于WHEREORDER BY 之后应用,您现在还必须将top-n 过滤器包装在外部查询中。

    这几乎完成了,我们实际上也需要过滤COL2 的唯一原因是过滤掉根本不属于该范围的记录。例如。如果 some_value 为 5 并且您拥有此数据:

      COL1 | COL2
         1 |  2
         3 |  4   <-- you get this row 
         6 | 10
    

    如果COL2 为 5,则该行结果是正确的,但不幸的是,在这种情况下,您的查询的正确结果是 [empty set]。这就是我们需要像这样过滤COL2 的唯一原因:

    select *
      FROM ( select *
               FROM ( select *
                        from A
                       where col1 <= :some_value
                       order by col1 desc
                    )
              where rownum <= 1
            )
      WHERE col2 >= :some_value;
    

    你的方法有几个问题:

    • 缺少ORDER BY - 与rownum 过滤器有关的危险!
    • 应用 Top-N 子句(rownum 过滤器)过早。如果结果没有怎么办?数据库读取索引直到结束,rownum (STOPKEY) 永远不会启动。
    • 优化器故障。使用 between 谓词,我的 11g 安装并没有想到按降序读取索引,所以它实际上是从头 (0) 向上读取它,直到找到匹配的 COL2 值——或者——COL1 超出范围。

    .

    COL1 | COL2
       1 |  2   ^
       3 |  4   |      (2) go up until first match.
                +----- your intention was to start here
       6 | 10
    

    实际发生的是:

      COL1 | COL2
         1 |  2   +----- start at the beginning of the index
         3 |  4   |      Go down until first match.      
                  V
         6 | 10
    

    看看我的查询的执行计划:

    ------------------------------------------------------------------------------------------
    | Id  | Operation                       | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
    ------------------------------------------------------------------------------------------
    |   0 | SELECT STATEMENT                |        |     1 |    26 |     4   (0)| 00:00:01 |
    |*  1 |  VIEW                           |        |     1 |    26 |     4   (0)| 00:00:01 |
    |*  2 |   COUNT STOPKEY                 |        |       |       |            |          |
    |   3 |    VIEW                         |        |     2 |    52 |     4   (0)| 00:00:01 |
    |   4 |     TABLE ACCESS BY INDEX ROWID | A      | 50000 |   585K|     4   (0)| 00:00:01 |
    |*  5 |      INDEX RANGE SCAN DESCENDING| SIMPLE |     2 |       |     3   (0)| 00:00:01 |
    ------------------------------------------------------------------------------------------
    

    注意INDEX RANGE SCAN **DESCENDING**

    最后,为什么我没有在索引中包含COL2?这是一个单一的 row-top-n 查询。您最多可以保存一个表访问(不管上面的行估计是什么!)如果您希望在大多数情况下找到一行,那么您无论如何都需要转到表中的其他列(可能)所以你不会节省任何东西,只会消耗空间。包含COL2 只会在您查询不返回任何内容时提高性能!

    相关:

    【讨论】:

    • +1 将您的品牌重命名为“使用索引 Lukas”。那是量身定制的支持:-)。我还测试了双重嵌套的ROWNUM 过滤,但我会再次测试。有很多分析步骤。也许,我在途中做错了什么......这里的关键实际上可能是将COL2排除在索引之外
    • @LukasEder 如果你有可以展示你的尝试,我们可以看到找到问题。 COL2 应该不是问题。
    • 虽然此查询的性能似乎与公认的答案一样好,但我有点担心 E-Rows 值在我的查询的实际计划中相当偏离。 Thomas Köhne's answer 似乎有一个更优化的计划,在整个查询中正确的 E-Rows 值为 1(除非我遗漏了什么)。此外,关于查询区间的互斥性,它的可读性非常强。不过还是谢谢!提醒 Oracle CBO 利用ROWNUM 语义的能力始终很重要!
    【解决方案2】:

    我认为,因为范围不相交,您可以将 col1 定义为主键并执行如下查询:

    SELECT *
      FROM    a
           JOIN
              (SELECT MAX (col1) AS col1
                 FROM a
                WHERE col1 <= :somevalue) b
           ON a.col1 = b.col1;
    

    如果您必须添加的范围之间存在差距:

    Where col2 >= :somevalue
    

    作为最后一行。

    执行计划:

    SELECT STATEMENT  
     NESTED LOOPS  
      VIEW  
       SORT AGGREGATE 
        FIRST ROW  
         INDEX RANGE SCAN (MIN/MAX) PKU1
      TABLE ACCESS BY INDEX A
       INDEX UNIQUE SCAN PKU1
    

    【讨论】:

    • 这是一个非常聪明的解决方案。我想知道@MarkusWinand 对此有何看法……?
    • 智能解决方案,我自己想到的。但我认为它不太可能胜过@MarkusWinand 的(或者它们都显示相同的结果)。能发个方案对比一下吗?我猜应该有INDEX RANGE SCANINDEX UNIQUE SCAN
    • 我已经在我的帖子中添加了执行计划。
    • 这确实是一个非常狡猾的解决方案。我可以重现这个计划。鉴于范围是互斥的,这个解决方案似乎是最优的并且非常易读。谢谢!
    • @LukasEder 正如我所说的“虽然它很可能被缓存了”。物理IO不会受到太大影响,但是逻辑IO会比需要高一倍,因为它需要遍历树两次。主要是关于CPU和锁定。将 CPU 使用率减少一半通常是一件好事;)特别是如果您正在使用的数据库在此基础上发生更改...
    【解决方案3】:

    也许将此堆表更改为 IOT 表会提供更好的性能。

    【讨论】:

    • 不是我的情况。现有索引完全涵盖了谓词,所以我很擅长 INDEX RANGE SCAN... 我想我应该用该信息更新我的问题
    • 嗯,在你的位置,我会尝试使用散列分区全局索引。
    • 嗯,我之前考虑过分区,但没有考虑哈希分区。您能否详细说明如何(以及为什么)使用散列分区优化?
    【解决方案4】:

    我没有生成示例数据来对此进行测试,但您可能想尝试一下。

    ALTER TABLE A ADD COL3 NUMBER(15);
    
    UPDATE A SET COL3 = COL2 - COL1;
    

    在 COL3 上创建索引。

    SELECT /*+FIRST_ROWS(1)*/ *
    FROM A
    WHERE :some_value < COL3
    AND ROWNUM = 1;
    

    【讨论】:

    • 这不会返回正确的结果。示例::some_value = 5COL1 = 4COL2 = 6(:some_value BETWEEN COL1 AND COL2) IS TRUECOL3 = 2(:some_value &lt; COL3) IS FALSE
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-05-14
    • 2019-05-26
    • 1970-01-01
    • 2020-01-25
    • 2014-06-09
    • 1970-01-01
    相关资源
    最近更新 更多