有一次经历让我印象深刻,那是要对一张大表上一个核心索引进行重建,表的大小几十个G是有的。当时预估了时间(全表扫描+排序)应该十几分钟可以搞定,但 是让我吃惊的是整个重建过程足足搞了快1个小时。不停的查看重新索引会话的等待事件,大多数时候都是db file sequential read,零星的能看到几次db file scattered read,虽然指定的多块读大小为128,但是观察到的p3参数小的可怜。导致索引重建慢的原因不难分析,是由于这个表的不少数据被缓存进了buffer cache中,虽然设置的多块读参数为128,但是由于总是无法读取到符合要求的连续的数据块而让性能大打折扣,这里所谓的符合要求指的是读取的数据块都 不在buffer cache之中。例如:
上面的12个块为一个extent,假设都不在buffer cache中的话,一次物理IO就可以完成,但是由于部分块被cache,而导致总共需要6次的物理IO才行。因此在做全表扫描的情况下,表里的数据一部 分被cache并不见得总是好事。当时我在想,如果可以绕过buffer cache,直接路径读取就好了。11G以后,ORACLE终于推出了这个功能,在全表扫描的时候,如果ORACLE认为这个表足够大,就会启用直接路径读取,而不需要像11G之前,必须先把数据加载到buffer cache。
采用direct path read完成读取的条件
1)表大于_small_table_threshold的参数值设置
Oracle通过隐含参数_small_table_threshold来界定大表小表的临界,Oracle认为对于大表执行直接路径读取的意义比较大, 对于小表通过将其缓存可能受益更大。_small_table_threshold的单位为block。默认为db cache size的2%大小,在实例启动过程中动态决定。11GR2之前,表的大小要是_small_table_threshold参数值的5倍才会采取直接路 径读取方式,11GR2后只需要满足_small_table_threshold定义的大小就会采取直接路径读取。我们可以简单的看一下11GR2下的 一个测试:
create tablespace test datafile '+DG_DATA/test/test.dbf' size 64m segment space management manual;
create table t (v varchar2(100)) pctused 1 pctfree 99 tablespace test;
show parameter small
NAME TYPE VALUE
------------------------------------ ---------------------- ------------------------------
_small_table_threshold integer 3000
CREATE OR REPLACE FUNCTION GET_ADR_TRSH(P_STEP IN NUMBER,
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 /*+ append */
INTO T
SELECT RPAD('*', 100, '*')
FROM DUAL
CONNECT BY LEVEL <= P_STEP + L_START;
COMMIT;
L_BLOCKS := L_BLOCKS + P_STEP + L_START;
L_START := 0;
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 OR L_BLOCKS > NVL(P_STOP, L_BLOCKS));
END LOOP;
RETURN L_BLOCKS - P_STEP;
END;
/
set serveroutput on
DECLARE
L_TRSH NUMBER;
BEGIN
L_TRSH := GET_ADR_TRSH(10, 1500, 4000);
DBMS_OUTPUT.PUT_LINE(L_TRSH);
END;
/
3000
以上的代码大家可以研究一下,我不做详细说明了,大体的意思是不断的增加表大小,观察增加到表上的block有多少时,会采取直接路径读取。我们测试的结 果是3000,刚好和我们定义的_small_table_threshold参数值完全吻合。11GR1的话,经过测试是 _small_table_threshold的5倍的时候才会出现直接路径读取。
但是表的大小并不仅仅是唯一决定是否采用直接路径读取的因素,还有其他两个因素:脏块的比例、表中数据被cache的比例
2)表上的脏块小于表总block数的25%
下面的代码通过不断增加表的脏块数,当达到表脏块数的25%的时候,不在发生直接路径读取事件了。
CREATE OR REPLACE FUNCTION GET_DIRTY_TRSH(P_STEP IN NUMBER,
P_START IN NUMBER DEFAULT 0,
P_STOP IN NUMBER DEFAULT NULL)
RETURN NUMBER IS
L_TRSH NUMBER := 0;
L_PRD NUMBER := 0;
L_CNT NUMBER := 0;
L_START NUMBER := P_START;
BEGIN
EXECUTE IMMEDIATE 'alter system flush buffer_cache';
LOOP
L_TRSH := L_TRSH + P_STEP + L_START;
UPDATE T SET V = V WHERE ROWNUM <= L_TRSH;
COMMIT;
L_START := 0;
SELECT /*+ full(t) */
COUNT(*)
INTO L_CNT
FROM T;
SELECT VALUE
INTO L_CNT
FROM V$SEGMENT_STATISTICS
WHERE OWNER = USER
AND OBJECT_NAME = 'T'
AND STATISTIC_NAME = 'physical reads direct';
EXIT WHEN L_CNT = L_PRD OR L_TRSH > NVL(P_STOP, L_TRSH);
L_PRD := L_CNT;
END LOOP;
RETURN L_TRSH;
END;
/
DECLARE
L_TRSH NUMBER;
BEGIN
L_TRSH := GET_DIRTY_TRSH(1, 350, 4000);
DBMS_OUTPUT.PUT_LINE(L_TRSH);
END;
/
739
我们看到脏块的比例大约达到表块的25%(共3000个表块)的时候,直接路径读消失了。
3)表中的块被cache的比例小于50%的时候
CREATE OR REPLACE FUNCTION GET_CACHED_TRSH(P_START IN NUMBER DEFAULT 0,
P_STEP IN NUMBER DEFAULT 1)
RETURN NUMBER IS
CURSOR L_CUR IS
SELECT /*+ index(t i_t) */
v
FROM T WHERE v='****************************************************************************************************';
L_V VARCHAR2(100);
L_TRSH NUMBER := 0;
L_PRD NUMBER := 0;
L_CNT NUMBER := 0;
L_START NUMBER := P_START;
BEGIN
EXECUTE IMMEDIATE 'alter system flush buffer_cache';
OPEN L_CUR;
LOOP
FOR I IN 1 .. P_STEP + L_START LOOP
FETCH L_CUR
INTO L_V;
END LOOP;
L_TRSH := L_TRSH + P_STEP + L_START;
L_START := 0;
SELECT /*+ full(t) */
COUNT(*)
INTO L_CNT
FROM T;
SELECT VALUE
INTO L_CNT
FROM V$SEGMENT_STATISTICS
WHERE OWNER = USER
AND OBJECT_NAME = 'T'
AND STATISTIC_NAME = 'physical reads direct';
EXIT WHEN L_CNT = L_PRD OR L_CUR%NOTFOUND;
L_PRD := L_CNT;
END LOOP;
CLOSE L_CUR;
RETURN L_TRSH;
END;
/
create index i_t on t (v);
DECLARE
L_TRSH NUMBER;
BEGIN
L_TRSH := GET_CACHED_TRSH(500, 1);
DBMS_OUTPUT.PUT_LINE(L_TRSH);
END;
/
1497
在接近50%(共3000个表块)的表块被cache的时候,直接路径读消失了。
11GR1 | 11GR2 | 备注 | |
块阀值 | _small_table_threshold*5 | _small_table_threshold | 统计信息里记录的表的block数目(11GR2)。 超过此阀值后。 |
Block cache阀值 | 表块的50% | 表块的50% | 少于此阀值 |
脏块阀值 | 表块的25% | 表块的25% | 少于此阀值 |
满足以上条件时,Oracle会进行直接路径读取。
Oracle为直接路径读取设置的三个“门槛”,非常的合理:
第 一个阀值:表大小,太小的表从direct path read中的获益太小。但是特别需要引起你的警惕,如果表上存在统计信息,那么ORACLE会采取表的统计信息中记录的block与 _small_table_threshold的设定值来做比较,而不是表的真实大小(dba_segments中记录的值)。这可能导致一些不是你预期 的情况发生。如果你的统计信息与表的真实情况差异很大,那么你应该仔细考虑可能发生什么样的结果。如果你的表没有统计信息,ORACLE会依据表的真实大 小来决定是否进行direct path read。
第二个阀值:脏块阀值,由于direct path read需要出发一个段的检查点,因此脏块太多,刷新脏块可能会导致IO繁忙
第 三个阀值:表在内存里的cache率,如果cache率很高,那么还是走传统路径更快。direct path read的出现,需要让ORACLE公司的开发人员设计一个单独的结构来存储每个表有多少数据是脏数据,有多少数据被cache。不过这个结构目前还并未 暴露给我们查询。在flush buffer cache后,这个结构被清空。(flush shared_pool并不会被清空)
当你预期一个查询应该会走direct path read,但是却走了传统的路径扫描的时候,应该检查是否违背了这三个前置条件中的一个或几个。不过很多时候,当一个查询“应该”走direct path read但是却没有的时候,你往往无能为力,你能在数据库运行过程中修改表的数据的cache比例吗?不能!你能修改表的脏数据的比例吗?往往也不能,通 过修改表的统计信息中的表块数,可以满足第一个条件,但是如果不满足其他两个条件,依然不能有效。你可以通过flush buffer cache来满足后两个条件,但往往在生产环境中不被允许。我曾经在个人微博(新浪微博:魏兴华-DBA)发布过一个关于索引创建走不上direct path read的微博,现在终于想明白了,不满足第三个条件,表被cache的内容需要低于50%。
direct path read的优势
- 我认为非常重要的,参照我文章开头举得例子,采用直接路径读取后,总能保证读取的块数是多块读参数设置的大小,提高了读取的效率
- 大大的降低了对于latch的使用,进而避免了可能导致的latch竞争(cbc latch等)
- 降低了全表扫描对buffer cache的冲击
- 降低了buffer pin的开销,有可能降低buffer busy waits等相关等待
如何控制direct path read的开关
有两种方法来启用、禁用这个特性。event 10949或者设置隐含参数_serial_direct_read。两种方式都可以在session或system级别设置。
- event 10949设置后,可以禁用direct path read。
每次测试前为了避免脏块和缓冲块对测试的影响,都先将buffer cache清空。我们在session级别测试,system级别的测试方法是一样的,这里就不再赘述。
SQL> alter system flush buffer_cache;
System altered.
SQL> ALTER session SET EVENTS '10949 TRACE NAME CONTEXT FOREVER';
Session altered.
SQL> execute snap_events.start_snap
PL/SQL procedure successfully completed.
SQL> select count(*) from t;
COUNT(*)
----------
10090944
SQL> set serveroutput on
SQL> execute snap_events.end_snap
---------------------------------------------------------
SID: 2929:DLSP - oracle
Session Events - 08-Sep 10:58:50
Interval:- 18 seconds
---------------------------------------------------------
Event Waits Time_outs Csec Avg Csec Max Csec
----- ----- --------- ---- -------- --------
Disk file operations I/O 17 0 0 .012 0
db file scattered read 1,115 0 96 .086 1
SQL*Net message to client 6 0 0 .000 0
SQL*Net message from client 6 0 1,323 220.444 2,350
PL/SQL procedure successfully completed.
SQL> alter system flush buffer_cache;
System altered.
SQL> ALTER session SET EVENTS '10949 TRACE NAME CONTEXT off';
Session altered.
SQL> execute snap_events.start_snap
PL/SQL procedure successfully completed.
SQL> select count(*) from t;
COUNT(*)
----------
10090944
SQL> execute snap_events.end_snap
---------------------------------------------------------
SID: 2929:DLSP - oracle
Session Events - 08-Sep 10:59:56
Interval:- 8 seconds
---------------------------------------------------------
Event Waits Time_outs Csec Avg Csec Max Csec
----- ----- --------- ---- -------- --------
db file sequential read 1 0 0 .024 0
direct path read 1113 0 300 .232 1
SQL*Net message to client 5 0 0 .001 0
SQL*Net message from client 5 0 445 89.034 2,673
我们通过snap_events脚本获取了会话执行语句前后等待事件的差值,启用10949事件后,执行了db file scattered read,关闭10949事件后,以direct path read方式进行了表扫描。
2.通过设置隐含参数_serial_direct_read来设置是否启用direct path read,同样我们只在session级别做测试,system级别可以由读者自己来完成。
SQL>alter session set "_serial_direct_read"=false;
Session altered.
SQL> execute snap_events.start_snap
PL/SQL procedure successfully completed.
SQL> select count(*) from t;
COUNT(*)
----------
10090944
SQL>execute snap_events.end_snap
SQL> ---------------------------------------------------------
SID: 2641:DLSP - oracle
Session Events - 08-Sep 10:49:46
Interval:- 9 seconds
---------------------------------------------------------
Event Waits Time_outs Csec Avg Csec Max Csec
----- ----- --------- ---- -------- --------
Disk file operations I/O 6 0 0 .001 0
db file sequential read 1 0 0 .030 1
db file scattered read 1,115 0 154 .138 4
SQL*Net message to client 5 0 0 .000 0
SQL*Net message from client 5 0 228 45.510 1,912
PL/SQL procedure successfully completed.
SQL> alter session set "_serial_direct_read"=auto;
Session altered.
SQL> alter system flush buffer_cache;
System altered.
SQL> execute snap_events.start_snap
PL/SQL procedure successfully completed.
SQL> select count(*) from t;
COUNT(*)
----------
10090944
SQL>execute snap_events.end_snap
SQL> ---------------------------------------------------------
SID: 2641:DLSP - oracle
Session Events - 08-Sep 10:50:21
Interval:- 10 seconds
---------------------------------------------------------
Event Waits Time_outs Csec Avg Csec Max Csec
----- ----- --------- ---- -------- --------
db file sequential read 1 0 0 .028 1
direct path read 1112 0 42 .164 0
SQL*Net message to client 5 0 0 .000 0
SQL*Net message from client 5 0 730 146.016 1,912
PL/SQL procedure successfully completed.
_serial_direct_read设置为false后,系统采取了传统的扫描路径,出现了db file scattered read等待时间,_serial_direct_read为true后,采用了direct path read的等待事件。
direct path read可能的副作用
- 会发生段一级的检查点(后面详细介绍),因此在查询真正开始执行前,会做这个额外的准备工作。而且可能会造成IO抖动,因为要写脏数据。
- 如果你的表需要频繁的全表扫描读取,还是用传统的读取方式比较好。
- 在MOS中搜索direct path read,会发现它可能会导致多次的延迟块清除(后面详细介绍)
直接路径读为什么要发生检查点?
Oracle的一致性读取代表的是:读取到的数据是查询开始时刻的数据,在你对一个大表发出查询前,首先需要发生一个段级别的检查点来保证这个段上的脏数据已经被刷新到了磁盘,如果不这样做会发生什么情况?
举个例子就会很容易明白:查询发生时,某条记录的id值在磁盘上的值为5,内存中的值为6。如果不发生段一级的检查点就开 始直接路径读取,那么进程在读取到这个记录所在的数据块后发现,块上的scn是小于查询scn的,读取是安全的,因此直接把5作为结果返回给用户。完全违 背了数据库(块)的一致性读取。因此Oracle在直接路径读取之前,都会发生一个段一级的检查点来保证一致性的读取。通过10046很容易看到发生的段 级别的检查点。
PARSING IN CURSOR #4573489040 len=22 dep=0 uid=45 oct=3 lid=45 tim=1735265030575 hv=2763161912 ad='70000036058fd08' sqlid='cyzznbykb509s'
select count(*) from t
END OF STMT
PARSE #4573489040:c=60,e=101,p=0,cr=0,cu=0,mis=0,r=0,dep=0,og=1,plh=1842905362,tim=1735265030574
EXEC #4573489040:c=51,e=84,p=0,cr=0,cu=0,mis=0,r=0,dep=0,og=1,plh=1842905362,tim=1735265030719
WAIT #4573489040: nam='SQL*Net message to client' ela= 9 driver id=1650815232 #bytes=1 p3=0 obj#=-1 tim=1735265030759
WAIT #4573489040: nam='reliable message' ela= 89 channel context=504403173615444552 channel handle=504403173661108944 broadcast message=504403173662264424 obj#=-1 tim=1
735265031335
WAIT #4573489040: nam='enq: KO - fast object checkpoint' ela= 10064 name|mode=1263468550 2=65588 0=1 obj#=-1 tim=1735265041430
说明:
1)直接路径读取如果有必要,也会去构造CR块,不过这里的CR块是在PGA中构造的,而不是在buffer cache中构造的。
2)如果表上没有任何的脏数据,并不会触发段上的检查点,这一点也很容易用10046跟踪出来,因此如果你想通过10046跟踪到段检查点,需要保证这个表上在内存中有脏数据。
延迟块清除与直接路径读
由于直接路径读取是发生在进程的PGA中的,如果读取过程中,ORACLE发现一些块没有做块清除,会在PGA中进行延迟 块清除的操作,但是这个清除的操作并不会记录日志,且被清除过的块并不会被刷回到磁盘。正是由于延迟清除过的块不被写回到磁盘,因此如果有比较多的进程来 进行直接路径读取,就会导致各个进程反复的进行块清除的操作,一定程度上浪费了CPU资源。(我们下面的测试用到的 snap_my_stats包,可以采样两次会话的统计信息差值,来获取两次采样间的信息增量)
1)我们先看看传统路径扫描下,延迟块清除的情况
update t set object_id=1 where rownum<10000;
另开一个session对buffer_cache进行刷新。
SQL> commmit;
禁用direct path read
SQL> alter session set "_serial_direct_read"=false;
Session altered.
SQL> execute snap_my_stats.start_snap
PL/SQL procedure successfully completed.
SQL> select count(*) from t;
COUNT(*)
----------
10090944
SQL> execute snap_my_stats.end_snap
SQL> ---------------------------------
Session stats - 08-Sep 11:51:00
Interval:- 11 seconds
---------------------------------
Name Value
---- -----
redo size 9,764
cleanouts only - consistent read gets 135
immediate (CR) block cleanout applications 135
commit txn count during cleanout 135
cleanout - number of ktugct calls 135
PL/SQL procedure successfully completed.
可以看到,相关延迟块清除的工作,都有了相应的指标,而且清除过程中产生了redo。我们来看看第二次读取还会不会发生延迟块清除。
SQL> execute snap_my_stats.start_snap
PL/SQL procedure successfully completed.
SQL> select count(*) from t;
COUNT(*)
----------
10090944
SQL> execute snap_my_stats.end_snap
SQL> ---------------------------------
Session stats - 08-Sep 11:51:24
Interval:- 3 seconds
---------------------------------
Name Value
---- -----
DB time 343
non-idle wait count 5
consistent gets 140,109
consistent gets from cache 140,109
PL/SQL procedure successfully completed.
没有任何延迟块清除的统计信息了
采用传统的路径读取,延迟块清除只需要进行一次,后续如果再有会话读取,不需要再做任何有关块清除的工作。
2)直接路径扫描下延迟块清除的情况
准备工作略(update,flush buffer cache,commit)
SQL> alter session set "_serial_direct_read"=auto;
Session altered.
SQL> execute snap_my_stats.start_snap
PL/SQL procedure successfully completed.
SQL> select count(*) from t;
COUNT(*)
----------
10090944
SQL>execute snap_my_stats.end_snap
SQL> ---------------------------------
Session stats - 08-Sep 11:53:39
Interval:- 4 seconds
---------------------------------
Name Value
---- -----
cleanouts only - consistent read gets 135
immediate (CR) block cleanout applications 135
commit txn count during cleanout 135
cleanout - number of ktugct calls 135
PL/SQL procedure successfully completed.
第一次读取发生了延迟块清除。但是由于redo size 没发生任何变化,因此没有结果显示。
SQL> execute snap_my_stats.start_snap
PL/SQL procedure successfully completed.
SQL> select count(*) from t;
COUNT(*)
----------
10090944
SQL>execute snap_my_stats.end_snap
SQL> ---------------------------------
Session stats - 08-Sep 11:53:52
Interval:- 3 seconds
---------------------------------
Name Value
---- -----
cleanouts only - consistent read gets 135
immediate (CR) block cleanout applications 135
commit txn count during cleanout 135
cleanout - number of ktugct calls 135
PL/SQL procedure successfully completed.
第二次查询依然会进行延迟块清除,一定程度上多消耗了CPU时间。
参考至:
http://afatkulin.blogspot.com/2009/01/11g-adaptive-direct-path-reads-what-is.html
http://afatkulin.blogspot.com/2012/07/serial-direct-path-reads-in-11gr2-and.html
http://www.itpub.net/thread-1815281-1-1.html
http://blog.tanelpoder.com/2012/09/03/optimizer-statistics-driven-direct-path-read-decision-for-full-table-scans-_direct_read_decision_statistics_driven/
如有错误,欢迎指正
邮箱:czmcj@163.com