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

ORACLE NULL值谓词问题

原创 张程 2020-04-28
907

原文作者:Jonathan lewis
原文地址:https://jonathanlewis.wordpress.com/2018/12/18/null-predicate/

人们不时会问我是否要写另一本关于“基于成本的优化器”的书,我想答案是否定的,因为产品一直在快速增长,不可能跟上发展的步伐,而且总是有越来越多的小细节可能已经存在多年,当有人问我关于一些我以前从未注意到的小细节的问题。
这些“小细节”的难点在于你能花多少时间去弄清楚它们是否重要,以及是否值得写些关于它们的文章。这里有一个小例子来说明我的意思——首先是构造数据集:

rem
rem     Script:         null_filter.sql
rem     Author:         Jonathan Lewis
rem     Dated:          Dec 2018
rem     Purpose:
rem
rem     Last tested
rem             18.3.0.0
rem             12.1.0.2
rem

create table t1
nologging
as
select  *
from    all_objects
where   rownum <= 50000 -- > comment to avoid wordpress format issue
;

insert into t1 select * from t1;
insert into t1 select * from t1;
insert into t1 select * from t1;
commit;

create index t1_i1 on t1(object_type, data_object_id, object_id, created);

begin
        dbms_stats.gather_table_stats(
                ownname     => null,
                tabname     => 'T1',
                cascade     => true,
                method_opt  => 'for all columns size 1'
        );
end;
/

这是一个简单的数据集,只有一个索引。索引唯一有意义的地方是第二列(data_object_id)经常为空。这导致一对非常相似的语句在执行计划中出现了一点差异:

set serveroutput off
alter session set statistics_level = all;

select
        object_name, owner
from
        t1
where
        object_type = 'TABLE'
and     data_object_id = 20002
and     object_id = 20002
and     created > trunc(sysdate - 90)
;

select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

select
        object_name, owner
from
        t1
where
        object_type = 'TABLE'
and     data_object_id is null
and     object_id = 20002
and     created > trunc(sysdate - 90)
;

select * from table(dbms_xplan.display_cursor(null,null,'allstats last'));

在这两个查询的执行计划中,会有多大的差异?当然,“ is null”谓词的副作用是禁用索引基于distinct_keys的“隐式列分组”,但在本例中,我在其中一列上有一个基于范围的谓词,因此Oracle无论如何都不会使用distinct_keys。
当然,有一点是不能使用带null的相等运算符,必须使用“is null”,这可能会产生影响,但如何做到呢?以下是两个执行计划:

----------------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers | Reads  |
----------------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |       |      1 |        |      0 |00:00:00.01 |       3 |      1 |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |      1 |      1 |      0 |00:00:00.01 |       3 |      1 |
|*  2 |   INDEX RANGE SCAN                  | T1_I1 |      1 |      1 |      0 |00:00:00.01 |       3 |      1 |
----------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("OBJECT_TYPE"='TABLE' AND "DATA_OBJECT_ID"=20002 AND "OBJECT_ID"=20002 AND
              "CREATED">TRUNC(SYSDATE@!-90))

-------------------------------------------------------------------------------------------------------
| Id  | Operation                           | Name  | Starts | E-Rows | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                    |       |      1 |        |      0 |00:00:00.01 |       3 |
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| T1    |      1 |      1 |      0 |00:00:00.01 |       3 |
|*  2 |   INDEX RANGE SCAN                  | T1_I1 |      1 |      1 |      0 |00:00:00.01 |       3 |
-------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("OBJECT_TYPE"='TABLE' AND "DATA_OBJECT_ID" IS NULL AND "OBJECT_ID"=20002 AND
              "CREATED">TRUNC(SYSDATE@!-90))
       filter(("OBJECT_ID"=20002 AND "CREATED">TRUNC(SYSDATE@!-90)))

谓词“data_object_id is null”的查询将object_id和sysdate谓词重复作为access谓词和filter谓词。这似乎有点令人惊讶,而且是一个潜在的性能风险。在第一个查询中,查询引擎将非常有效地在正确的位置命中正确的索引叶块,然后顺序扫描,将每个rowid提供给父运算符,直到它到达范围的末尾。
使用“is null”的执行计划,查询引擎在扫描索引叶子节点时,将检查object_id和created的实际值,这将消耗额外的CPU开销,更重要的是,Oracle可能会从object_type='TABLE’和data_object_id is null的第一个索引项开始,并遍历所有为空的索引条目,检查object_id是否正确?
基于这一比较,我们如何证明Oracle并没有从第一个索引条目开始一直走下去,在第一个索引条目中,object_type='TABLE’和data_object_id is null,检查扫描的每个条目,或者换一种说法,Oracle是否真的准确的将索引范围扫描缩减到最小的“路径”去执行,即谓词“OBJECT_ID”=20002 AND “CREATED”>TRUNC(SYSDATE@!-90)作为access谓词?
让我们使用sys-op-lbid()函数计算相关的叶子块数,Oracle在内部使用该函数计算索引中的叶块数。首先我们得到索引对象的id,然后扫描它,看看有多少叶块包含与我们的object_type and data_object_id谓词匹配的条目,并出现在我们的目标值20002之前的索引中:

column object_id new_value m_index_id
 
select
        object_id
from
        user_objects
where
        object_type = 'INDEX'
and     object_name = 'T1_I1'
;
 
select  distinct sys_op_lbid(&m_index_id, 'L', rowid)
from    t1
where   object_type    = 'TABLE'
and     data_object_id is null
and     object_id      < 20002
;
SYS_OP_LBID(159271
------------------
AAAm4nAAFAAACGDAAA
AAAm4nAAFAAACF9AAA
AAAm4nAAFAAACGCAAA
AAAm4nAAFAAACF/AAA
AAAm4nAAFAAACF+AAA
AAAm4nAAFAAACGFAAA
AAAm4nAAFAAACGEAAA
AAAm4nAAFAAACGGAAA
 
8 rows selected.

这告诉我们,在找到object_ID=20002之前,索引中有8个叶块,我们必须对其进行遍历,如果Oracle实际上没有使用access谓词并缩小它正在探测的索引的范围,那么我们将看到8个buffer get,而不是rowsource execution stats中的3个。
一句话:对于多列索引,当列是索引中较靠前的列时,“column is null”和“column=constant”之间的执行计划似乎有所不同,但即使“is null”选项导致某些访问谓词在索引范围内重新显示为filter谓词并产生额外的工作可能并不重要——Oracle仍然在索引范围扫描中使用最小数量的索引叶块。

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

评论