Oracle收集对表收集统计信息导致全表扫描直接路径读?

时间:2023-12-22 18:24:50

direct path read深入解析

前言

最近碰到一件很奇葩的事情,因为某条SQL执行缓慢,原因是走了笛卡尔(两组大数据结果集),而且笛卡尔还是NL的一个部分,要循环31M次。

很容易发现是统计信息没有导致的,收集完就好了。

结果过了个51假期回来有人反馈其他的SQL慢了,看了下慢的SQL涉及到的表就刚好是上次收集统计信息的表。

统计问题SQL的历史执行情况,发现时间刚好在那天收集完后开始变慢。咋回事。

后边发现慢的原因是对收集的那两张表做全表扫描时候用了直接路径读(DPR)。暂定为表A和表B。SQL的语法结构是对A,B重复做2次left join,所以慢的SQL中A,B表会被扫描各3次,全部都是DPR。

???收集统计信息会导致DPR???

事件概要

对事情做一个简述,不展开来说,引出研究和得到的结论就行。

  1. SQL变慢,发现时间刚好是收集相关表的统计信息之后变慢的
  2. 收集cursor_awr('&sql_id')可以得到执行计划变过的
  3. 使用hint回退原来的执行计划,还是慢,综合对比无论是新旧执行计划,都走了全表扫描(DPR),statistics_level=all收集详细的执行情况也是发现主要时间花在DRP上(很奇怪就是以前应该也慢才对,毕竟表A、B都很大,而且各3次全表扫)
  4. ashrpti、awrsqrpt相关sql的信息,发现快的时候物理读远远小于慢的时候的物理读
  5. 通过session级别禁用DPR之后,反而更慢了。产生gc类等待,以及"db file sequential read"和"db file scattered read"等。

因此基本可以确认全表扫描导致DPR变慢的,而导致DPR的原因应该就是收集了表的统计信息。

研究过程

首先百度搜索和mos上都找遍了,没有类似的案例。

关于DPR的很容易可以搜索到一些关键的信息,这里自行搜索就行(有些文章的结论就并不完全正确)。

我们从DPR的产生条件出发引出实验内容。

第一,大表是DPR的前置条件,那么什么样的表才算是大表呢?

Oracle有个隐含参数控制的这个阀值,'_small_table_threshold',单位是块,简称为STT。

第二,超过表的VLOT的值,无论是否设置10949,均为发生DPR。(_serial_direct_read的关闭能否阻止尚未验证)。

第三,表已经被缓存在buffer cache中的块占比,超过表50%的话,不会发生DPR。

第四,表的脏块率,超过25%的脏块也不会发生DPR。

为了研究方便,使用sys用户创建视图查询隐含参数。

create or replace view h$parameter
as
select a.ksppinm name,
a.ksppdesc description,
b.ksppstvl session_value,
c.ksppstvl system_value
from x$ksppi a,x$ksppcv b,x$ksppsv c
where a.indx=b.indx and a.indx=c.indx;

在我的环境下(11.2.0.4.0),查询的结果如下:

set line 500
col name for a50
col session_value for a50
col system_value for a50
select name,session_value,system_value from h$parameter where name in
('_db_block_buffers','_very_large_object_threshold','_small_table_threshold','_direct_read_decision_statistics_driven','_statistics_based_srf_enabled'); NAME SESSION_VALUE SYSTEM_VALUE
-------------------------------------------------- -------------------------------------------------- --------------------------------------------------
_db_block_buffers 32830 32830
_small_table_threshold 656 656
_very_large_object_threshold 500 500
_direct_read_decision_statistics_driven TRUE TRUE
_statistics_based_srf_enabled TRUE TRUE

这里关于STT以及VLOT和MTT的计算方法,有个公式

STT = _small_table_threshold
MTT = * _small_table_threshold
VLOT = (_very_large_object_threshold/) * _db_block_buffers

算得的几个具体的数值:

STT = 656

MTT = 3280

VLOT = 164150

那么我们可以构造一张表,循环对表insert一波数据,然后全表扫描看看是否发生DPR,如果发生DPR了,打印出插入了多少行数据。如果没发生DPR,那么truncate表,重新insert知道满足DPR为止。

先构造一张表T:

create table t (id int,name varchar2(2000)) tablespace test;

然后用以下这么个函数实现,函数改造自某个大神,使用sys用户创建(后边的实验都用sys用户)

CREATE OR REPLACE FUNCTION GET_ADR_TRSH(P_STEP  IN NUMBER DEFAULT 1,
P_START IN NUMBER DEFAULT 0,
P_STOP IN NUMBER DEFAULT NULL)
RETURN NUMBER IS
L_PRD NUMBER;
L_CNT NUMBER;
L_BLOCKS NUMBER := 0;
L_START NUMBER := P_START;
BEGIN
EXECUTE IMMEDIATE 'truncate table t';
LOOP
INSERT
INTO T
SELECT level,rpad('a',1800,'+')
FROM DUAL
CONNECT BY LEVEL <= P_STEP + L_START;
COMMIT;
L_BLOCKS := L_BLOCKS + P_STEP + L_START;
L_START := 0;
--EXECUTE IMMEDIATE 'analyze table t delete statistics';
--EXECUTE IMMEDIATE 'analyze table t compute statistics';
EXECUTE IMMEDIATE 'alter system flush buffer_cache';
SELECT /*+ full(t) */
COUNT(*)
INTO L_CNT
FROM T;
SELECT VALUE
INTO L_PRD
FROM V$SEGMENT_STATISTICS
WHERE OWNER = USER
AND OBJECT_NAME = 'T'
AND STATISTIC_NAME = 'physical reads direct';
EXIT WHEN(L_PRD > 0);
END LOOP;
RETURN L_BLOCKS;
END;
/

说明一下:因为我的表结构是name字段2000字节,而块大小为8192字节,因此这个insert的数据每个块刚好只能容纳4行。

再运行以下语句,会得到插入多少行发生DPR的结果。

set serveroutput on
DECLARE
L_TRSH NUMBER;
BEGIN
L_TRSH := GET_ADR_TRSH();
DBMS_OUTPUT.PUT_LINE(L_TRSH);
END;
/

我的环境跑的结果如下:

SYS@zkm> set serveroutput on
SYS@zkm> DECLARE
2 L_TRSH NUMBER;
3 BEGIN
4 L_TRSH := GET_ADR_TRSH();
5 DBMS_OUTPUT.PUT_LINE(L_TRSH);
6 END;
7 /
2489 PL/SQL procedure successfully completed. SYS@zkm> select count(*) from t; COUNT(*)
----------
2489

表示我insert了2489行数据后,全表扫描的话会产生DPR。

SYS@zkm> select dbms_rowid.rowid_relative_fno(rowid) file#,dbms_rowid.rowid_block_number(rowid) block#,count(*) from t group by dbms_rowid.rowid_relative_fno(rowid),dbms_rowid.rowid_block_number(rowid) order by 2;

     FILE#     BLOCK#   COUNT(*)
---------- ---------- ----------
6 131 4
...省略大部分内容...
6 766 4
6 767 4
6 851 1 623 rows selected.

总共623行,代表T表有623个快,由于在第623个块产生已经满足DPR了,因此第623个块insert了一行数据就停止了。

那么,这个和我们前边的大表阀值STT=656有区别,这是怎么回事?

我们知道,表在没有统计信息的时候,dba_tables.blocks这个字段是空的,那么全表扫描的时候,不知道表的块数(姑且用Object_Size表示)怎么和STT=656做比较来确定是否DPR呢?

结合我前言中遇到的问题,我猜测是用的dba_segments.blocks这个值(收集统计信息后走了DPR嘛)。后来找到一篇文章,里边的大佬也是说用的dba_segments.blocks。

但是我在做实验的时候其实是不对的。准确的说,根据我的实验结果,Object_Size应该使用的是L3段头块中"Extent Control Header"下边的#blocks below处的值。

我们接着上边的实验往下验证。

首先确定L3段头块的信息:

SYS@zkm> select HEADER_FILE,HEADER_BLOCK from dba_segments where owner='SYS' and segment_name='T'; 

HEADER_FILE HEADER_BLOCK
----------- ------------
6 130

查看dba_segments.blocks这个值和L3里边"Extent Control Header"下边的#blocks below处的值,我们来做个比较:

SYS@zkm> select blocks from dba_segments where owner='SYS' and segment_name='T';

    BLOCKS
----------
768
SYS@zkm> select value from v$diag_info where name like '%De%';

VALUE
--------------------------------------------------------------------------------
/u01/app/oracle/diag/rdbms/zkm/zkm/trace/zkm_ora_8267.trc SYS@zkm> alter system dump datafile 6 block 130; System altered. SYS@zkm> !more /u01/app/oracle/diag/rdbms/zkm/zkm/trace/zkm_ora_8267.trc
...省略部分内容...
Extent Control Header
-----------------------------------------------------------------
Extent Header:: spare1: 0 spare2: 0 #extents: 21 #blocks: 768
last map 0x00000000 #maps: 0 offset: 2716
Highwater:: 0x01800380 ext#: 20 blk#: 128 ext size: 128
#blocks in seg. hdr's freelists: 0
#blocks below: 748
mapblk 0x00000000 offset: 20
Unlocked
--------------------------------------------------------
Low HighWater Mark :
Highwater:: 0x01800300 ext#: 19 blk#: 128 ext size: 128
#blocks in seg. hdr's freelists: 0
#blocks below: 640
mapblk 0x00000000 offset: 19
Level 1 BMB for High HWM block: 0x01800301
Level 1 BMB for Low HWM block: 0x01800281
--------------------------------------------------------
Segment Type: 1 nl2: 1 blksz: 8192 fbsz: 0
L2 Array start offset: 0x00001434
First Level 3 BMB: 0x00000000
L2 Hint for inserts: 0x01800081
Last Level 1 BMB: 0x01800301
Last Level II BMB: 0x01800081
Last Level III BMB: 0x00000000
Map Header:: next 0x00000000 #extents: 21 obj#: 96513 flag: 0x10000000
Inc # 0
Extent Map
-----------------------------------------------------------------
0x01800080 length: 8
0x01800088 length: 8
0x01800090 length: 8
0x01800098 length: 8
0x018000a0 length: 8
0x018000a8 length: 8
0x018000b0 length: 8
0x018000b8 length: 8
0x018000c0 length: 8
0x018000c8 length: 8
0x018000d0 length: 8
0x018000d8 length: 8
0x018000e0 length: 8
0x018000e8 length: 8
0x018000f0 length: 8
0x018000f8 length: 8
0x01800100 length: 128
0x01800180 length: 128
0x01800200 length: 128
0x01800280 length: 128
0x01800300 length: 128 Auxillary Map
--------------------------------------------------------
Extent 0 : L1 dba: 0x01800080 Data dba: 0x01800083
Extent 1 : L1 dba: 0x01800080 Data dba: 0x01800088
Extent 2 : L1 dba: 0x01800090 Data dba: 0x01800091
Extent 3 : L1 dba: 0x01800090 Data dba: 0x01800098
Extent 4 : L1 dba: 0x018000a0 Data dba: 0x018000a1
Extent 5 : L1 dba: 0x018000a0 Data dba: 0x018000a8
Extent 6 : L1 dba: 0x018000b0 Data dba: 0x018000b1
Extent 7 : L1 dba: 0x018000b0 Data dba: 0x018000b8
Extent 8 : L1 dba: 0x018000c0 Data dba: 0x018000c1
Extent 9 : L1 dba: 0x018000c0 Data dba: 0x018000c8
Extent 10 : L1 dba: 0x018000d0 Data dba: 0x018000d1
Extent 11 : L1 dba: 0x018000d0 Data dba: 0x018000d8
Extent 12 : L1 dba: 0x018000e0 Data dba: 0x018000e1
Extent 13 : L1 dba: 0x018000e0 Data dba: 0x018000e8
Extent 14 : L1 dba: 0x018000f0 Data dba: 0x018000f1
Extent 15 : L1 dba: 0x018000f0 Data dba: 0x018000f8
Extent 16 : L1 dba: 0x01800100 Data dba: 0x01800102
Extent 17 : L1 dba: 0x01800180 Data dba: 0x01800182
Extent 18 : L1 dba: 0x01800200 Data dba: 0x01800202
Extent 19 : L1 dba: 0x01800280 Data dba: 0x01800282
Extent 20 : L1 dba: 0x01800300 Data dba: 0x01800302
-------------------------------------------------------- Second Level Bitmap block DBAs
--------------------------------------------------------
DBA 1: 0x01800081 End dump data blocks tsn: 7 file#: 6 minblk 130 maxblk 130

可以看出dba_segments.blocks为768,而"Extent Control Header"下边的#blocks below处的值为748。

那么其实Object_Size的值是等于748的。

前边提到的T表有623个块意义在当前就显得不是特别重要的,因为Oracle的判断基准以Object_size=748,并不是实际上的623个块。

如何验证Object_size的值是否准确呢?新开一个session(就叫s1会话吧),作如下操作:

SYS@zkm> select value from v$diag_info where name like '%De%';

VALUE
--------------------------------------------------------------------------------
/u01/app/oracle/diag/rdbms/zkm/zkm/trace/zkm_ora_8363.trc SYS@zkm> alter system flush buffer_cache; System altered. SYS@zkm> / System altered. SYS@zkm> / System altered. SYS@zkm> alter session set events 'trace[NSMTIO] disk=medium'; Session altered. SYS@zkm> SELECT /*+ full(t) */ COUNT(*) FROM T; COUNT(*)
----------
2489

查看trc文件内容:

NSMTIO: qertbFetch:[MTT < OBJECT_SIZE < VLOT]: Checking cost to read from caches(local/remote) and checking storage reduction factors (OLTP/EHCC Comp)
NSMTIO: kcbdpc:DirectRead: tsn: 7, objd: 96513, objn: 96511
ckpt: 1, nblks: 748, ntcache: 1, ntdist:0
NSMTIO: Additional Info: VLOT=164150
Object# = 96513, Object_Size = 748 blocks
SqlId = 4x7n0h69zwnuk, plan_hash_value = 2966233522, Partition# = 0

可以直接看到:

Object_Size = 748 blocks

VLOT=164150

这两个值都和前边得到的结果一致。

从其他内容看,我们可以得到判断条件是

当满足[MTT < OBJECT_SIZE < VLOT]的时候,会检查cost(比如cache率,前边提到的第三),本地还是远程之类的等。

实际上,MTT的值为3280不可能<Object_size,MTT在11gR2已经废弃。

大量实验可以得出结论:可以看到在11.2版本下,小于SST不走DirectRead,大于VLOT一定走DirectRead,介于SST和VLOT的表,需要考虑是否为本地表或者远程表

然后这里最后走了DPR了(DirectRead)。

其他信息:

object_id=96511,data_object_id=96513

ntcache: 1表示缓存了一个块

值得一提的是,如果T表存在统计信息,也就是dba_tables.blocks有值的话,Oracle会直接使用该值作为Object_size的值。我们验证下,

新开一个session,收集统计信息之后。

SYS@zkm> analyze table t compute statistics;

Table analyzed.

SYS@zkm> select blocks from dba_tables where owner='SYS' and table_name='T' union all select blocks from dba_segments where owner='SYS' and segment_name='T'; 

    BLOCKS
----------
748 --dba_tables.blocks
768 --dba_segments.blocks

刚好和原来一样,不过你可以尝试truncate表T, 他的值还是用748,但其实为0了。这里实验过程不写出来了。

这个其实可以验证收集统计信息很有可能导致DPR的发生了。

接下来我们验证下前边第三点:

第三,表已经被缓存在buffer cache中的块占比,超过表50%的话,不会发生DPR。

首先,删除统计信息(后边说明原因)

然后用如下语句查询当前表被缓存在buffer cache的块的数量。

SYS@zkm> select INST_ID,sum(NUM_BUF) from x$kcboqh where obj# in (select DATA_OBJECT_ID from dba_objects where owner='SYS' and object_name='T') group by inst_id;

   INST_ID SUM(NUM_BUF)
---------- ------------
1 5 SYS@zkm> analyze table t delete statistics; Table analyzed.

表示实例1中,表T有5个块被缓存。

由于Object_Size = 748,一半为374。因此想办法将缓存的块变为373,看是否发生DPR,然后在缓存多一个块,变为374,看是否发生DPR。

用SELECT /*+ full(t) */ COUNT(*) FROM T where rownum<=xxxx;

由于1个块包含4行,其中373块实际上还要减掉L3号块,因此读取372个块即可,共1488行。但实际上查询的结果有问题,我之前一篇文章有研究过全表扫描下用rownum后如何缓存块,还是测试不够充分。

不过这里暂不做研究,最终我调试出来的rownum可以为1472。

开多另外一个session,将多块读设置为1,禁用动态采样,在这个session级别上禁用DPR。

直接复制的:
set arraysize 5000
alter system flush buffer_cache;
alter session set "_serial_direct_read"=NEVER;
alter session set db_file_multiblock_read_count=1;
SELECT /*+ full(t) dynamic_sampling(t 0) */ COUNT(*) FROM T where rownum<=1476;
select INST_ID,sum(NUM_BUF) from x$kcboqh where obj# in (select DATA_OBJECT_ID from dba_objects where owner='SYS' and object_name='T') group by inst_id;
SYS@zkm> set arraysize 5000
SYS@zkm> alter system flush buffer_cache; System altered. SYS@zkm> alter session set "_serial_direct_read"=NEVER; Session altered. SYS@zkm> alter session set db_file_multiblock_read_count=1; Session altered. SYS@zkm> SELECT /*+ full(t) dynamic_sampling(t 0) */ COUNT(*) FROM T where rownum<=1472; COUNT(*)
----------
1472 SYS@zkm> select INST_ID,sum(NUM_BUF) from x$kcboqh where obj# in (select DATA_OBJECT_ID from dba_objects where owner='SYS' and object_name='T') group by inst_id; INST_ID SUM(NUM_BUF)
---------- ------------
1 373

回到s1会话,执行:

SYS@zkm> SELECT /*+ full(t) */ COUNT(*) FROM T;

  COUNT(*)
----------
2489 SYS@zkm>
SYS@zkm>
SYS@zkm>
SYS@zkm> / COUNT(*)
----------
2489 trc多出来的信息: *** 2020-05-12 11:10:11.356
NSMTIO: qertbFetch:[MTT < OBJECT_SIZE < VLOT]: Checking cost to read from caches(local/remote) and checking storage reduction factors (OLTP/EHCC Comp)
NSMTIO: kcbdpc:DirectRead: tsn: 7, objd: 96513, objn: 96511
ckpt: 1, nblks: 748, ntcache: 373, ntdist:0
NSMTIO: Additional Info: VLOT=164150
Object# = 96513, Object_Size = 748 blocks
SqlId = 4x7n0h69zwnuk, plan_hash_value = 2966233522, Partition# = 0

其中ntcache=373刚好就是我们控制的缓存373。

这里也只走了DPR,那么我们多缓存一个块,是否就不会用DPR了呢?

SYS@zkm> set arraysize 5000
SYS@zkm> alter system flush buffer_cache; System altered. SYS@zkm> alter session set "_serial_direct_read"=NEVER; Session altered. SYS@zkm> alter session set db_file_multiblock_read_count=1; Session altered. SYS@zkm> SELECT /*+ full(t) */ COUNT(*) FROM T where rownum<=1476; COUNT(*)
----------
1476 SYS@zkm> select INST_ID,sum(NUM_BUF) from x$kcboqh where obj# in (select DATA_OBJECT_ID from dba_objects where owner='SYS' and object_name='T') group by inst_id; INST_ID SUM(NUM_BUF)
---------- ------------
1 374

回到s1,

SYS@zkm> SELECT /*+ full(t) */ COUNT(*) FROM T;

  COUNT(*)
----------
2489 trc多出来的信息: *** 2020-05-12 11:11:28.249
NSMTIO: qertbFetch:[MTT < OBJECT_SIZE < VLOT]: Checking cost to read from caches(local/remote) and checking storage reduction factors (OLTP/EHCC Comp)
NSMTIO: kcbdpc:NoDirectRead:[CACHE_READ]: tsn: 7, objd: 96513, objn: 96511
ckpt: 0, nblks: 748, ntcache: 374, ntdist:0
NSMTIO: Additional Info: VLOT=164150
Object# = 96513, Object_Size = 748 blocks
SqlId = 4x7n0h69zwnuk, plan_hash_value = 2966233522, Partition# = 0

当ntcache为Object_size的一半的时候,即表T有一半的块缓存在buffer cache中时候,不会做DPR。

奇怪的是,如果你收集了统计信息,无论cache率多高,都只会做直接路径读。这点尚未搞明白为什么。

这个时候,如果禁用参数"_statistics_based_srf_enabled"=false的话,

再次s1执行观察trc的话,输出会变为如下显示:

*** 2020-05-12 22:37:50.181
NSMTIO: qertbFetch:NoDirectRead:[- STT < OBJECT_SIZE < MTT]:Obect's size: 748 (blocks), Threshold: MTT(3283 blocks),
_object_statistics: enabled, Sage: disabled,
Direct Read for serial qry: enabled(::::::), Ascending SCN table scan: FALSE
flashback_table_scan: FALSE, Row Versions Query: FALSE
SqlId: 4x7n0h69zwnuk, plan_hash_value: 2966233522, Object#: 96513, Parition#: 0

这个时候的Object_size确实就是<MTT的值了。再次设置为true的话,判断cache率有变正常了。

关于这个隐含参数,网络和mos几乎没有资料介绍。

根据实验和网上资料我们可以总结,

1.统计信息的有无只会影响到Object_size,当然可以通过"_direct_read_decision_statistics_driven"控制是否使用统计信息来影响DPR

2.当Object_size < STT的话,一定不走DPR

3.当Object_size > VLOT的话,一定走DPR(未排除参数_serial_direct_read是否影响)

4.当Object_size介于MTT和VLOT之间的时候,会使用成本计算(Checking cost to read from caches(local/remote) and checking storage reduction factors (OLTP/EHCC Comp))去判断是否DPR。

其中,MTT<Object_size实际上不准确(因此才说11gR2MTT是废弃的)。由于要使用成本代价,因此可以认为STT < Object_size < VLOT的时候,使用成本计算。比如包括缓存率的成本等。

5."_direct_read_decision_statistics_driven"默认为true,当设置为false,从观察到的结果上看,MTT被启用了,从上边实验看原因是因为Oracle直接判断- STT < OBJECT_SIZE < MTT了,这个时候无法判断是否成本计算被禁用了。毕竟不再有"MTT < OBJECT_SIZE < VLOT".有时间可以构造出这么一个条件出来观察看看。

参考

http://www.itpub.net/thread-1815281-1-1.html

https://www.talkwithtrend.com/Article/218485

https://www.modb.pro/db/25067

https://blogs.oracle.com/smartscan-deep-dive/revisiting-buffer-cache-very-large-object-threshold-vlot