暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

ORACLE降序查询问题

原创 张程 2020-04-01
1295

原文作者:Jonathan lewis
原文地址:https://jonathanlewis.wordpress.com/2019/01/31/descending-problem/

我以前写过关于降序索引的奇怪之处(例如在ORACLE降序索引BUGORACLE降序索引问题FBI错误重现)。但我刚刚遇到一个例子,我可能必须引入一个不需要存在的降序索引。通常,这是两个Oracle功能产生碰撞的边界。我有一个记录大量客户数据的表,这个表按年记录了一定数量的交易信息。我需要一个查询显示客户的最新交易数据。很自然的,这个表被设计成了按客户ID的HASH分区表。我有一个以customer_id和transaction_date开头的索引列。因此,下面是我的查询,或更准确地说,是客户经过简化并调整后的查询场景:

select  /*+ gather_plan_statistics */
        *
from    (
             select
                    v1.*,
                    rownum rn
             from   (
                             select   /*
                                         no_eliminate_oby
                                         index_rs_desc(t1 (customer_id, transaction_date))
                                      */
                                      t1.*
                             from     t1
                             where    customer_id = 50
                             and      transaction_date >= to_date('1900-01-01','yyyy-mm-dd')
                             order by transaction_date DESC
                ) v1
                where  rownum <= 10 -- > comment to avoid WordPress format issue
         )
where    rn >= 1
;

可以看到SQL中加入了一些HINT,/*+ gather_plan_statistics */允许我在执行时同时收集对应步骤的实际行的资源信息。并且内联视图中的HINT(我已经注释掉),将强制数据库使用customer_id和transaction_date上的索引进行降序扫描。
如果我将t1表创建为一个简单的(非分区)堆表,将得到以下执行计划(我不得不编辑一个“小于或等于”的符号来避免WordPress格式问题)

----------------------------------------------------------------------------------------------------------------
| Id  | Operation                       | Name  | Starts | E-Rows | Cost (%CPU)| A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |       |      1 |        |    14 (100)|     10 |00:00:00.01 |      14 |
|*  1 |  VIEW                           |       |      1 |     10 |    14   (0)|     10 |00:00:00.01 |      14 |
|*  2 |   COUNT STOPKEY                 |       |      1 |        |            |     10 |00:00:00.01 |      14 |
|   3 |    VIEW                         |       |      1 |     10 |    14   (0)|     10 |00:00:00.01 |      14 |
|   4 |     TABLE ACCESS BY INDEX ROWID | T1    |      1 |    340 |    14   (0)|     10 |00:00:00.01 |      14 |
|*  5 |      INDEX RANGE SCAN DESCENDING| T1_I1 |      1 |     10 |     3   (0)|     10 |00:00:00.01 |       4 |
----------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("RN">=1)
   2 - filter(ROWNUM .LE. 10)
   5 - access("CUSTOMER_ID"=50 AND "TRANSACTION_DATE" IS NOT NULL AND "TRANSACTION_DATE">=TO_DATE('
              1900-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))

注意到索引降序扫描正是我所需要的,它只消耗了最小的逻辑读开销。并且从表中只访问了最必要的10行。如果我把T1表创建为一个HASH分区表,且相应索引创建为本地索引后。看一下将发生什么,下面是不加HINT的执行计划:

----------------------------------------------------------------------------------------------------------------------------------------------------------
| Id  | Operation                                      | Name  | Starts | E-Rows | Cost (%CPU)| A-Rows |   A-Time   | Buffers |  OMem |  1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                               |       |      1 |        |   207 (100)|     10 |00:00:00.01 |     138 |       |       |          |
|*  1 |  VIEW                                          |       |      1 |     10 |   207   (1)|     10 |00:00:00.01 |     138 |       |       |          |
|*  2 |   COUNT STOPKEY                                |       |      1 |        |            |     10 |00:00:00.01 |     138 |       |       |          |
|   3 |    VIEW                                        |       |      1 |    340 |   207   (1)|     10 |00:00:00.01 |     138 |       |       |          |
|*  4 |     SORT ORDER BY STOPKEY                      |       |      1 |    340 |   207   (1)|     10 |00:00:00.01 |     138 |  2048 |  2048 | 2048  (0)|
|   5 |      PARTITION HASH SINGLE                     |       |      1 |    340 |   206   (0)|    340 |00:00:00.01 |     138 |       |       |          |
|   6 |       TABLE ACCESS BY LOCAL INDEX ROWID BATCHED| T1    |      1 |    340 |   206   (0)|    340 |00:00:00.01 |     138 |       |       |          |
|*  7 |        INDEX RANGE SCAN                        | T1_I1 |      1 |    340 |     4   (0)|    340 |00:00:00.01 |       3 |       |       |          |
----------------------------------------------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("RN">=1)
   2 - filter(ROWNUM. LE. 10)
   4 - filter(ROWNUM .LE. 10)
   7 - access("CUSTOMER_ID"=50 AND "TRANSACTION_DATE">=TO_DATE(' 1900-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss') AND "TRANSACTION_DATE" IS NOT NULL)

尽管优化器使用本地索引仅做了单分区扫描,但是他并没有采用我们希望的索引降序扫描。尽管他已经采用了合适的索引。因此他用错误的访问顺序获取了满足条件的相关行,然后不得不对他们进行排序,并只保留最大的10行数据。我们看到发生了138个逻辑读(这些都会转换为实际的IO访问,甚至更多的资源开销)。
这是否意味着对表进行分区后优化器无法使用索引降序扫描?或者估算成本过高导致上述问题?下面是带有HINT的执行计划,我们看下优化器走降序扫描时的场景:

----------------------------------------------------------------------------------------------------------------------
| Id  | Operation                             | Name  | Starts | E-Rows | Cost (%CPU)| A-Rows |   A-Time   | Buffers |
----------------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                      |       |      1 |        |   207 (100)|     10 |00:00:00.01 |       8 |
|*  1 |  VIEW                                 |       |      1 |     10 |   207   (1)|     10 |00:00:00.01 |       8 |
|*  2 |   COUNT STOPKEY                       |       |      1 |        |            |     10 |00:00:00.01 |       8 |
|   3 |    VIEW                               |       |      1 |    340 |   207   (1)|     10 |00:00:00.01 |       8 |
|   4 |     PARTITION HASH SINGLE             |       |      1 |    340 |   206   (0)|     10 |00:00:00.01 |       8 |
|   5 |      TABLE ACCESS BY LOCAL INDEX ROWID| T1    |      1 |    340 |   206   (0)|     10 |00:00:00.01 |       8 |
|*  6 |       INDEX RANGE SCAN DESCENDING     | T1_I1 |      1 |    340 |     4   (0)|     16 |00:00:00.01 |       3 |
----------------------------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("RN">=1)
   2 - filter(ROWNUM .LE. 10)
   6 - access("CUSTOMER_ID"=50 AND "TRANSACTION_DATE" IS NOT NULL AND "TRANSACTION_DATE">=TO_DATE('
              1900-01-01 00:00:00', 'syyyy-mm-dd hh24:mi:ss'))

可以看到使用降序扫描后只消耗了8个逻辑读,从表中只访问了10行。但是看到成本值,优化器并没有认为使用降序扫描能有任何的开销降低。回到普通(非分区)表的执行计划,可以看到优化器确实使用了访问更少记录的降序扫描。所以这里的问题是评估成本的问题-我们不得不使用HINT让优化器走索引降序扫描,如果我们希望减少Oracle所做的工作。
你可能会注意到,在索引降序扫描步骤中返回的行数是16而不是10,在普通表查询时没有该问题。我不知道为什么会这样,但是当我将需求更改为20行时,范围扫描返回了31行,当我将其更改为34行时,范围扫描返回了46行,而查询47行是实际返回了61行。可以看到该趋势,索引范围扫描返回的行数似乎为1 + 15 * N。

脚注:
如果将避免添加HINT(或SQL patch)。只需要重新创建一个按transaction_date列降序存储的索引即可。此时优化器将自动选择正确的路径,然后查询恰好返回10行,并且不需要进行任何排序。但是谁想要在不需要的时候创建降序索引呢!
如果您想重现实验,请使用以下脚本创建我的测试数据。

rem
rem     Script:         pt_ind_desc_bug.sql
rem     Author:         Jonathan Lewis
rem     Dated:          Dec 2018
rem     Purpose:        
rem
rem     Last tested 
rem             18.3.0.0
rem             12.2.0.1
rem             12.1.0.2
rem
 
create table t1 (
        customer_id,
        transaction_date,
        small_vc,
        padding 
)
partition by hash(customer_id) partitions 4
nologging
as
with generator as (
        select 
                rownum id
        from dual 
        connect by 
                level <= 1e4 -- > comment to avoid WordPress format issue
)
select
        mod(rownum,128)                         customer_id,
        (trunc(sysdate) - 1e6) + rownum         transaction_date,
        lpad(rownum,10,'0')                     v1,
        lpad('x',100,'x')                       padding
from
        generator       v1,
        generator       v2
where
        rownum <= 1e6 -- > comment to avoid WordPress format issue
;
 
create index t1_i1 on t1(customer_id, transaction_date) 
local 
nologging
;
 
begin
        dbms_stats.gather_table_stats(
                ownname     => null,
                tabname     => 'T1',
                method_opt  => 'for all columns size 1'
        );
end;
/

我已经在12.1.0.2、12.2.0.1和18.3.0.0上运行了该测试-在所有三个版本中,行为都是相同的。
正如客户在阅读该帖子后提醒我的那样,值得指出的是,对于更复杂的SQL,您仍然必须考虑基数和成本计算中的错误,这些错误很容易将优化器推入错误的连接顺序和/或连接方法。因此您是选择提示升序索引还是创建降序索引。评估此类“分页”查询所需的执行计划和优化方式可能会更复杂。

最后修改时间:2020-04-21 09:35:54
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论