首页 > 数据库 > Oracle > 等待事件系列(1)--User I/O类型(下)
等待事件系列(1)--User I/O类型(下)
全文可参考:http://www.cnblogs.com/lhrbest/articles/5835420.html
SELECT *
FROM v$event_name
WHERE NAME IN ('db file parallel read');
在V$SESSION_WAIT这个视图里面,这个等待事件有三个参数P1、P2、P3,其中P1为files代表有多少个文件被读取所请求,P2为blocks代表总共有多少个BLOCK被请求,P3为requests代表总共有多少次I/O请求。
db file parallel read物理读等待事件涉及到的数据块均是不连续的,同时可以跨越extent,这点不像db file scattered read。
这是一个很容易引起误导的等待事件,实际上这个等待事件和并行操作(比如并行查询,并行DML)没有关系。这个事件发生在数据库恢复的时候,当有一些数据块需要恢复的时候,Oracle会以并行的方式把他们从数据文件中读入到内存中进行恢复操作。当Oracle从多个数据文件中并行的物理读取多个BLOCK到内存的不连续缓冲中(可能是高速缓存区或者是PGA)的时候可能就会出现这个等待事件。这种并行读取一般出现在恢复操作中或者是从缓冲中预取数据达到最优化(而不是多次从单个BLOCK中读取,buffer prefetch以优化多个单块读)。这个事件表明会话正在并行执行多个读取的需求。注意:在11g之前,这个等待事件发生在数据文件的恢复过程中,但11g中新增了prefetch的特性,所以也可能导致这个等待事件的产生。
如果在等待时间中这个等待事件占的比重比较大,可以按照处理db file sequential read等待事件的方法来处理这个事件。
若是由于prefetch引起的性能问题,我们可以通过添加隐含参数来解决该问题。可以参考blog:http://blog.itpub.net/26736162/viewspace-2123473
set pagesize 9999
set line 9999
col NAME format a40
col KSPPDESC format a50
col KSPPSTVL format a20
SELECT a.INDX,
a.KSPPINM NAME,
a.KSPPDESC,
b.KSPPSTVL
FROM x$ksppi a,
x$ksppcv b
WHERE a.INDX = b.INDX
and lower(a.KSPPINM) IN ('_db_block_prefetch_quota','_db_block_prefetch_limit','_db_file_noncontig_mblock_read_count');
ALTER SYSTEM SET "_db_block_prefetch_quota"=0 SCOPE=SPFILE SID='*';
ALTER SYSTEM SET "_db_block_prefetch_limit"=0 SCOPE=SPFILE SID='*';
ALTER SYSTEM SET "_db_file_noncontig_mblock_read_count"=0 SCOPE=SPFILE SID='*';
SYS@oraESKDB1> set pagesize 9999
SYS@oraESKDB1> set line 9999
SYS@oraESKDB1> col NAME format a40
SYS@oraESKDB1> col KSPPDESC format a50
SYS@oraESKDB1> col KSPPSTVL format a20
SYS@oraESKDB1> SELECT a.INDX,
2 a.KSPPINM NAME,
3 a.KSPPDESC,
4 b.KSPPSTVL
5 FROM x$ksppi a,
6 x$ksppcv b
7 WHERE a.INDX = b.INDX
8 and lower(a.KSPPINM) IN ('_db_block_prefetch_quota','_db_block_prefetch_limit','_db_file_noncontig_mblock_read_count');
INDX NAME KSPPDESC KSPPSTVL
---------- ---------------------------------------- -------------------------------------------------- --------------------
881 _db_block_prefetch_quota Prefetch quota as a percent of cache size 10
883 _db_block_prefetch_limit Prefetch limit in blocks 0
1156 _db_file_noncontig_mblock_read_count number of noncontiguous db blocks to be prefetched 11
SYS@oraESKDB1> ALTER SYSTEM SET "_db_file_noncontig_mblock_read_count"=0 SCOPE=SPFILE SID='*';
System altered.
SYS@oraESKDB1> ALTER SYSTEM SET "_db_block_prefetch_quota"=0 SCOPE=SPFILE SID='*';
System altered.
SYS@oraESKDB1> ALTER SYSTEM SET "_db_block_prefetch_limit"=0 SCOPE=SPFILE SID='*';
System altered.
SYS@oraESKDB1> ALTER SYSTEM SET "_db_file_noncontig_mblock_read_count"=0 SCOPE=SPFILE SID='*';
这个等待事件通常只发生在一种情况下,就是Oracle 更新数据文件头信息时(比如发生Checkpoint)。
当这个等待事件很明显时,需要考虑是不是数据库中的数据文件数量太大,导致Oracle需要花较长的时间来做所有文件头的更新操作(checkpoint)。
SELECT *
FROM v$event_name
WHERE NAME IN ('db file single write');
这个等待事件有三个参数:
l file#: 需要更新的数据块所在的数据文件的文件号。查询文件号的SQL语句是:SELECT * FROM v$datafile WHERE file# = ;
l block#:需要更新的数据块号,如果BLOCK号不是1,则可以通过如下查询查出Oracle正在写入的对象是什么:
SELECT segment_name , segment_type ,
owner , tablespace_name
FROM sys.dba_extents
WHERE file_id =
AND
BETWEEN block_id AND block_id + blocks -1;
l blocks:需要更新的数据块数目(通常来说应该等于1),或Oracle写入file#的数据文件中从BLOCK#开始写入的BLOCK的数量。头一般来说都是BLOCK1,操作系统指定的文件头是BLOCK0,如果BLOCK号大于1,则表明Oracle正在写入的是一个对象而不是文件头。
直接路径读等待事件的3个参数分别是:file#(指绝对文件号)、first block#和block数量。
SELECT * FROM V$EVENT_NAME A WHERE A.NAME = 'direct path read';
这个等待事件有三个参数:
file number: 等待I/O读取请求的文件的绝对文件号
first dba: 等待I/O读取请求的第一个BLOCK号
block cnt: 以first block为起点,总共有多少个连续的BLOCK被请求读取
由参数P1与P2推得访问的数据对象:
select s.segment_name, s.partition_name
from dba_extents s
where between s.block_id and (s.block_id + s.blocks -1) and s.file_id =
直接路径读(direct path read)通常发生在Oracle直接读取数据到PGA时,这个读取不需要经过SGA。这类读取通常在以下情况被使用:
① 大量的磁盘排序IO操作 在排序操作(order by, group by, union, distinct, rollup,合并连接)时,由于PGA中的SORT_AREA_SIZE空间不足,无法在PGA中完成排序,需要利用temp表空间进行排序,当从临时表空间中读取排序结果时,会产生direct path read,从10g开始表现为direct path read temp等待事件。
② 大量的Hash Join操作,利用temp表空间保存hash区。使用HASH连接的SQL语句,将不适合位于内存中的散列分区刷新到临时表空间中。为了查明匹配SQL谓词的行,临时表空间中的散列分区被读回到内存中(目的是为了查明匹配SQL谓词的行),ORALCE会话在direct path read等待事件上等待。
③ SQL语句的并行查询,并行查询从属进程 使用并行扫描的SQL语句也会影响系统范围的direct path read等待事件。在并行执行过程中,direct path read等待事件与从属查询有关,而与父查询无关,运行父查询的会话基本上会在PX Deq:Execute Reply上等待,从属查询会产生direct path read等待事件。
④ 预读操作
⑤ 串行全表扫描(Serial Table Scan),大表的全表扫描,在Oracle11g中,全表扫描的算法有新的变化,根据表的大小、高速缓存的大小等信息,决定是否绕过SGA直接从磁盘读取数据。而10g则是全部通过高速缓存读取数据,称为table scan(large)。11g认为大表全表时使用直接路径读,可能比10g中的数据文件散列读(db file scattered reads)速度更快,使用的latch也更少。
最常见的是第一种情况。在DSS系统中,存在大量的Direct path read是很正常的,但是在OLTP系统中,通常显著的直接路径读都意味着系统应用存在问题,从而导致大量的磁盘排序读取操作。
db file sequential read、db file scattered read、direct path read是常见的集中数据读方式,下图简要描述了这3种方式的读取示意。
大量的direct path read等待时间最可能是一个应用程序问题。
direct path read事件由SQL语句驱动,这些SQL语句执行来自临时的或常规的表空间的直接读取操作。当输入的内容大于PGA中的工作区域时,带有需要排序的函数的SQL语句将排序结果写入到临时表空间中, 临时表空间中的排序顺序串随后被合并,用于提供最终的结果。读取排序结果时, Oracle会话在direct path read等待事件上等待。
对于这一写入等待,我们应该找到I/O操作最为频繁的数据文件(如果有过多的排序操作,很有可能就是临时文件),分散负载,加快其写入操作。
DB_FILE_DIRECT_IO_COUNT初始化参数可能影响direct path read的性能。
直接读取可能按照同步或异步的方式执行,取决于平台和初始化参数disk_asynch_io参数的值。使用异步I/O时,系统范围的等待的事件的统计可能不准确,会造成误导作用。
该事件一般不可能显示为主要的瓶颈,但它实际上也许是就是祸首。由于ORACLE统计等待时间的方式会造成统计的时间量不准确(如:从属查询产生的时间无法进行统计),所以对该事件不应该使用v$session_event视图中的total_wait或time_waited进行估计,应该使用v$sesstat视图中的直接读取操作次数(physical reads direct)进行判断:
select a.NAME,
b.SID,
b.VALUE,
round((sysdate - c.LOGON_TIME) * 24) hours_connected
from vstatnamea,vsesstat b, v$session c
where b.SID = c.SID
and a.STATISTIC# = b.STATISTIC#
and b.VALUE > 0
and a.NAME = 'physical reads direct'
order by b.VALUE;
由direct path read事件产生的原因,我们需要判断该事件正在读取什么段(如:散列段、排序段、一般性的数据文件),由此可判断产生该事件的原因是什么,可使用以下语句进行查询:
SELECT a.event,
a.sid,
c.sql_hash_value hash_vale,
decode(d.ktssosegt,
1,
'SORT',
2,
'HASH',
3,
'DATA',
4,
'INDEX',
5,
'LOB_DATA',
6,
'LOB_INDEX',
NULL) AS segment_type,
b.tablespace_name,
b.file_name
FROM vsessionwaita,dbadatafilesb,vsession c, x$ktsso d
WHERE c.saddr = d.ktssoses(+)
AND c.serial# = d.ktssosno(+)
AND d.inst_id(+) = userenv('instance')
AND a.sid = c.sid
AND a.p1 = b.file_id
AND a.event = 'direct path read'
UNION ALL
SELECT a.event,
a.sid,
d.sql_hash_value hash_value,
decode(e.ktssosegt,
1,
'SORT',
2,
'HASH',
3,
'DATA',
4,
'INDEX',
5,
'LOB_DATA',
6,
'LOB_INDEX',
NULL) AS segment_type,
b.tablespace_name,
b.file_name
FROM v$session_wait a,
dba_temp_files b,
v$parameter c,
v$session d,
x$ktsso e
WHERE d.saddr = e.ktssoses(+)
AND d.serial# = e.ktssosno(+)
AND e.inst_id(+) = userenv('instance')
AND a.sid = d.sid
AND b.file_id = a.p1 - c.VALUE
AND c.NAME = 'db_files'
AND a.event = 'direct path read';
注:如果是从临时文件中读取排序段的会话,则表明SORT_AREA_SIZE或PGA_AGGREGATE_TARGET的设置是不是偏小。如果是从临时文件中读取HASH段的会话,则表明HASH_AREA_SIZE或PAG_AGGREGATE_TARGET的设置是不是偏小。
当direct path read等待事件是由于并行查询造成的(读取的是一般的数据文件而非临时文件),父SQL语句的HASHVALUE与子SQL语句的HASHVALUE不同,可以通过以下SQL查询产生子SQL语句的父SQL语句:
SELECT DECODE(A.QCSERIAL#, NULL, 'PARENT', 'CHILD') STMT_LEVEL,
A.SID,
A.SERIAL#,
B.USERNAME,
B.OSUSER,
B.SQL_HASH_VALUE,
B.SQL_ADDRESS,
A.DEGREE,
A.REQ_DEGREE
FROM VPXSESSIONA,VSESSION B
WHERE A.SID = B.SID
ORDER BY A.QCSID, STMT_LEVEL DESC;
尽量减少I/O请求的次数,初始化参数db_file_direct_io_count用来设置直接读出和写入操作设置最大的IO缓冲区大小,因此能影响direct path read的性能,通过设置初始化参数DB_FILE_DIRECT_IO_COUNT,使得满足DB_BLOCK_SIZE x DB_FILE_DIRECT_IO_COUNT = max_io_size of system,在Oracle8i中默认这个值为64个BLOCK;在Oracle9i中可以设置隐含参数_DB_FILE_DIRECT_IO_COUNT,参数的值也变成了BYTES而不是BLOCK数量了,默认值也变成了1M。
l 使用10046第8层跟踪直接读取操作的ORACLE会话,其中P3参数表明块读取的数量。
l 也可使用strace,truss追踪直接读取或直接写入操作的UNIX进程,从生成的TRACE文件可获得相应的直接IO大小。
l 在第1层使用追踪事件10357,启动执行直接IO操作的会话的调试信息。
注:
>> 1. 如果是Temp文件,则表示该会话正在读取它先前用direct path write操作所创建的临时段,查明使用的是什么类型的临时段,有助于了解会话所做的事情。
SELECT DISTINCT decode(t.ktssosegt,
1,'SORT',
2,'HASH',
3,'DATA',
4,'INDEX',
5,'LOB_DATA',
6,'LOB_INDEX',
'UNDEFINED')
FROM sys.x$ktsso t
WHERE t.inst_id = userenv('instance') AND
t.kssoses = <当前session地址> AND
t.ktssosno =
>> 2. 如果是数据文件,则可能是并行查询从属操作在工作,通过P1值确定数据文件的名称:
select s.NAME from v$datafile s where s.FILE# =
union all
select a.name
from vtempfilea,vparameter b
where b.NAME = 'db_files'
and a.FILE# + b.VALUE =
在Oracle 11g之前,全表扫描使用db file scattered read的方式,将表中的数据块离散的读到Buffer Cache之后,供用户访问和使用,但是如果全表访问的表非常大,则有可能占用大量的Buffer Cache内存,这会导致Buffer Cache中其他数据被老化和挤出内存,而且在这一系列的读取操作中,Oracle引擎需要去判断每一个数据块是否已经存在于内存中,然后还要去请求内存空间,不断使用Cache Buffer Chain和Cache Buffer Lru Chain两个Latch进行判断,在某种程度上会加剧Latch竞争,如果全表访问的数据只是偶尔个别的访问,则占据大量Buffer Cache就显得过于昂贵,在Oracle Database 11g中,一种被称为串行全表扫描(Serial Table Scan)的技术被引入,该特性根据数据块的设置和统计信息等,自动决定是采用Direct Path Read绕过SGA,还是采用常规方式读取,因为这种自动选择,这一特性又被称为自适应直接读(Adaptive Direct Read).这种方式的好处是可以降低Buffer Cache的竞争,但是每次都要发生物理读,若是有多个会话同时去扫描同一张大表,这样会增大IO,也有可能导致系统的问题,而且在读取之前可能需要触发检查点,避免读到旧的映像。
在Oracle Database 11g中有一个新特性,全表扫描可以通过直接路径读的方式来执行(Direct Path Read),这是一个合理的变化,如果全表扫描的大量数据读取是偶发性的,则直接路径读可以避免大量数据对于Buffer Cache的冲击。
当然对于小表来说,Oracle允许通过Buffer Cache来进行全表扫描,因为这可能更快,也对性能影响不大。小表受到隐含参数:_small_table_threshold 影响。如果表大于 5 倍的小表限制,则自动会使用DPR替代FTS。可以设置初始化参数: _serial_direct_read 来禁用串行直接路径读。
当然,Oracle通过一个内部的限制,来决定执行DPR的阈值。
可以通过设置10949事件屏蔽这个特性,返回到Oracle 11g之前的模式上:
altersession setevents '10949 trace name context forever, level 1';
还有一个参数 _very_large_object_threshold 用于设定(MB单位)使用DPR方式的上限,这个参数需要结合10949事件共同发挥作用。10949 事件设置任何一个级别都将禁用DPR的方式执行FTS,但是仅限于小于 5 倍 BUFFER Cache的数据表,同时,如果一个表的大小大于 0.8 倍的 _very_large_object_threshold 设置,也会执行DPR。
这些限定的目标在于:
对于大表的全表扫描,必须通过Direct Path Read方式执行,以减少对于Buffer Cache的冲击和性能影响。但是我们可以通过参数调整来决定执行DPR的上限和下限。
Oracle通过隐含参数_small_table_threshold来界定大表小表的临界,Oracle认为对于大表执行直接路径读取的意义比较大,对于小表通过将其缓存可能受益更大。_small_table_threshold的单位为block。默认为db cache size的2%大小,在实例启动过程中动态决定。11GR2之前,表的大小要是_small_table_threshold参数值的5倍才会采取直接路径读取方式,11GR2后只需要满足_small_table_threshold定义的大小就会采取直接路径读取。
以下的AWR信息是典型的DPR症状:
在11g中,全表扫描可能使用direct path read方式,绕过buffer cache,这样的全表扫描就是物理读了。 在10g中,都是通过gc buffer来读的,所以不存在direct path read的问题。
一个隐含参数_serial_direct_read可以禁用串行直接路径读,11g默认值为auto:
禁用direct path read: _serial_direct_read = false
启用direct path read: _serial_direct_read = true
alter system set "_serial_direct_read"=never scope=both sid='*'; 可以显着减少direct path read
调整数据库参数alter system setevent='10949 trace name context forever, level 1'来关闭“direct path read”(直接路径读)特性,使SQL语句可以从缓存中查询数据,达到降低I/O读取量,使全表扫描的数据从缓存中读取,加快SQL语句运行速度的目的。
SELECT * FROM V$EVENT_NAME A WHERE A.NAME = 'direct path write';
这个等待事件有三个参数:
file number: 要写入的绝对文件号file number,可发现所进行的操作性质(如:排序/并行操作)
first dba: 起始块号first dba
block cnt: 块数block cnt,可发现直接写入IO的大小
由参数P1与P2推得访问的数据对象:
select s.segment_name, s.partition_name
from dba_extents s
where between s.block_id and (s.block_id + s.blocks -1) and s.file_id =
这个等待事件和direct path read 正好相反,是会话将一些数据从PGA中直接写入到磁盘文件(数据文件或临时文件)上,而不经过SGA。这类读取通常在以下情况被使用:
1. 直接路径加载(使用append方式加载数据、CREATE TABLE AS SELECT)
2. 并行DML操作
3. 磁盘排序使用临时表空间排序(内存不足)
最常见的直接路径写,多数因为磁盘排序导致。对于这一写入等待,应该找到I/O操作最为频繁的数据文件(如果有过多的排序操作,很有可能就是临时文件),分散负载,加快其写入操作。
如果系统存在过多的磁盘排序,会导致临时表空间操作频繁,对于这种情况,可以考虑为不同用户分配不同的临时表空间,使用多个临时文件,写入不同磁盘或者裸设备,从而降低竞争提高性能。
direct path write是允许一个会话让一个I/O写请求入队列的同时处理操作系统的I/O。如果会话想确认明显的写是否已经完成就会出现这个等待事件。因为会话需要空的缓存和空的槽位(等待之前的I/O释放),或者是会话需要确认所有的写操作都已经完成。如果没有使用异步I/O,I/O请求会被阻塞直到之前的I/O请求完成后,但是此时不会出现I/O等待,会话稍后重新恢复并加速I/O请求的完成,此时就会出现direct path write等待。因此,对于这个等待事件容易产生两方面的误解:一是认为等待的总的数量不能反映出I/O请求的数量,二是消耗在这个等待事件上的总的时间不能反映出实际的等待时间。这类型的写请求主要是用于直接装载数据的操作(create table as select)、并行的DML操作、不在内存中排序的I/O以及写入没有cache的LOB段操作。
关于该等待事件,以下的几点需要注意:
1. 从PGA写入数据文件,一个会话可以发布多个写入请求和连续的处理。
2. 直接写入可以按同步或异步方式执行,取决于平台和DISK_ASYNC_IO参数的值。
3. 通常用于在数据加载(APPEND提示、CTAS-CREATE TABLE AS SELECT)、并行DML操作时写入到临时段。
4. 在使用异步IO时,direct path write事件产生的等待时间不准确,所以通过v$sesstat视图来获得直接写入次数来评估该事件的影响情况:
SELECT A.NAME,
B.SID,
B.VALUE,
ROUND((SYSDATE - C.LOGON_TIME) * 24) HOURS_CONNECTED
FROM VSTATNAMEA,VSESSTAT B, V$SESSION C
WHERE A.STATISTIC# = B.STATISTIC#
AND B.SID = C.SID
AND B.VALUE > 0
AND A.NAME = 'PHYSICAL WRITES DIRECT';
这个等待事件的等待时间是指等待BLOCK直到明显的I/O请求完成的时间。通常来说,如果不是存在特殊的JOB,一般是不会出现这个等待事件,如果在等待事件中这个等待事件占的比重比较大,可以从如下几个方面来调整:
如果是等待的文件是临时表空间的文件,那么需要查看是否存在大量不合理的磁盘排序,优化相应的存在问题的SQL语句。如果是Oracle9i可以考虑使用自动SQL执行内存管理,Oracle8i的话可以手工的调整各种排序区。
确认异步I/O是否配置正确,异步I/O不会减少这个等待事件的等待时间但是却可以减少会话所消耗的时间。
检查是否存在I/O消耗很严重的SQL语句,如果存在,尝试优化SQL语句减少I/O的消耗。
最后确认一下是否达到了磁盘的I/O极限,如果是,则需要考虑更换更好的硬件设备。
大量的direct path read等待事件最可能是一个应用程序的问题。
注:
>> 1. 如果是Temp文件,则表示该会话正在写入临时表空间,查明使用临时段的类型,有助于了解会话所做的事情。
SELECT DISTINCT decode(t.ktssosegt,
1,'SORT',
2,'HASH',
3,'DATA',
4,'INDEX',
5,'LOB_DATA',
6,'LOB_INDEX',
'UNDEFINED')
FROM sys.x$ktsso t
WHERE t.inst_id = userenv('instance') AND
t.kssoses = <当前session地址> AND
t.ktssosno =
>> 2. 如果是数据文件,则可能正在执行一项直接路径加载操作,通过P1值确定数据文件的名称:
select s.NAME from v$datafile s where s.FILE# =
union all
select a.name
from vtempfilea,vparameter b
where b.NAME = 'db_files'
and a.FILE# + b.VALUE =
为了排序工作在临时区域读写时,等待direct path read temp、direct path write temp事件。oracle 9i为止是通过direct path read、direct path write等待观察的。在Oracle 10g/11g中,为了区分特定的对于临时文件的直接读写操作,Oracle对direct path read/write进行了分离,将这类操作分列出来:
SELECT A.*
FROM V$EVENT_NAME A
WHERE NAME IN ('direct path read temp', 'direct path write temp');
可以看到,现在的direct path read/write temp就是单指对于临时文件的直接读写操作。排序段上的 direct path I/O是在需要排序的数据比排序所分配的PGA内存区大时发生的。因此在排序工作时若大量发生direct path read temp、direct path write temp等待,就可以通过追加分配内存区域而避免等待。
1、应用程序层
检查需要排序的sql语句是否已经最优化。不必要的排序操作会导致CPU浪费、PGA区域浪费、磁盘I/O浪费。从UNION和UNION ALL的性能差异上可以得知,只靠减少不必要的排序操作,也能解决许多问题。
2、oracle内存层
在进程上分配的工作区大小内一次性实现的排序称为One pass sort。与此相反的情况称为Multi pass sort。发生Multi pass sort时,排序工作过程中将排序结果读写到排序段(sort segment)区域,因此发生direct path read temp、direct path write temp等待。如果该等待大量发生,就可以适当提高pga_aggregate_target值,以此消除问题。
oracle在调优指南上推荐如下设定pga_aggregate_target值。
OLTP:pga_aggregate_target=(total_mem * 80%) * 20%
OLAP:pga_aggregate_target=(total_mem * 80%) * 50%
上述的意思是,假设OS本身使用20%左右的内存,OLTP系统上使用剩余内存的20%左右,OLAP系统因为排序工作较多,所以使用剩余内存的50%左右。
结合Oracle 10g的一些特性,来进一步研究一下直接路径读/写与临时文件。
首先在一个session中执行一个能够引发磁盘排序的查询:
tq@CCDB> select sid from v$mystat where rownum <2;
SID
----------
1066
tq@CCDB> select a.table_name,b.object_name,b.object_type
2 from t1 a,t2 b
3 where a.table_name = b.object_name
4 order by b.object_name,b.object_type;
在另外sessoin查询相应等待事件:
tq@CCDB> select event,p1text,p1,p2text,p2,p3text,p3
2 from v$session_wait_history
3 where sid = 1066;
EVENT P1TEXT P1 P2TEXT P2 P3TEXT P3
------------------------ ------------- ------- ------------ ------- ------------ -------
direct path read temp file number 201 first dba 313512 block cnt 31
direct path read temp file number 201 first dba 313481 block cnt 31
direct path read temp file number 201 first dba 386887 block cnt 31
direct path read temp file number 201 first dba 317736 block cnt 31
direct path read temp file number 201 first dba 317193 block cnt 31
direct path read temp file number 201 first dba 316646 block cnt 31
direct path read temp file number 201 first dba 316134 block cnt 31
direct path read temp file number 201 first dba 315622 block cnt 31
direct path read temp file number 201 first dba 315079 block cnt 31
direct path read temp file number 201 first dba 314567 block cnt 31
10 rows selected.
从以上输出可以看到最近10次等待,direct path read temp就是这个查询引起的磁盘排序。注意这里的file number为201。而实际上,通过v$tempfile来查询,临时文件的文件号仅为1:
tq@CCDB> select file#,name from v$tempfile;
FILE# NAME
---------- -----------------------------------------
1 /oracle/oradata/ccdb/ccdb/temp01.dbf
如果通过10046事件跟踪,也可以获得类似的结果:
WAIT #3: nam='direct path write temp' ela= 1 file number=201 first dba=437862 block cnt=31 obj#=112141 tim=1270780
330976998
WAIT #3: nam='direct path write temp' ela= 1 file number=201 first dba=437416 block cnt=31 obj#=112141 tim=1270780
330977070
WAIT #3: nam='direct path read temp' ela= 7 file number=201 first dba=438471 block cnt=31 obj#=112141 tim=12707803
30982214
WAIT #3: nam='direct path read temp' ela= 4 file number=201 first dba=438502 block cnt=31 obj#=112141 tim=12707803
30983765
WAIT #3: nam='direct path read temp' ela= 8 file number=201 first dba=387015 block cnt=31 obj#=112141 tim=12707803
30993872
在Oracle文档中,file#被定义为绝对文件号(The Absolute File Number)。这里的原因何在呢?研究这个问题要先研究一下v$tempseg_usage这个视图,可以从这个视图出发动手研究一下这个对象究竟来自何方。
查询dba_objects视图,发现v$tempseg_usage原来是一个同义词。
sys@CCDB> select object_type from dba_objects where object_name = 'V$TEMPSEG_USAGE';
OBJECT_TYPE
-------------------
SYNONYM
再追本溯源原来v$tempseg_usage是v_$sort_usage的同义词,也就是和v$sort_usage同源。从Oracle 9i开始,Oracle将v$sort_usage视图从文档中移除了,因为这个名称有所歧义,容易使人误解仅记录排序内容,所以v$tempseg_usage视图被引入,用于记录临时段的使用情况:
sys@CCDB> select * from dba_synonyms where synonym_name = 'V$TEMPSEG_USAGE';
OWNER SYNONYM_NAME TABLE_OWNER TABLE_NAME DB_LINK
---------- -------------------- --------------- --------------- ----------
PUBLIC VSORT_USAGE
如果再进一步,可以看到这个视图的构建语句:
sys@CCDB> select view_definition from v$fixed_view_definition
2 where view_name = 'GV$SORT_USAGE';
VIEW_DEFINITION
--------------------------------------------------------------------------------
select x$ktsso.inst_id, username, username, ktssoses, ktssosno, prev_sql_addr, p
rev_hash_value, prev_sql_id, ktssotsn, decode(ktssocnt, 0, 'PERMANENT', 1, 'TEMP
ORARY'), decode(ktssosegt, 1, 'SORT', 2, 'HASH', 3, 'DATA', 4, 'INDEX', 5, 'LOB_
DATA', 6, 'LOB_INDEX' , 'UNDEFINED'), ktssofno, ktssobno, ktssoexts, ktssoblks,
ktssorfno from xktsso,vsession where ktssoses = v$session.saddr and ktssosno
= v$session.serial#
格式化一下,v$sort_usage的创建语句如下:
SELECT x$ktsso.inst_id,username,username,ktssoses,ktssosno,
prev_sql_addr,prev_hash_value,prev_sql_id,ktssotsn,
DECODE (ktssocnt,
0,'PERMANENT',
1,'TEMPORARY'),
DECODE (ktssosegt,
1, 'SORT',
2, 'HASH',
3, 'DATA',
4, 'INDEX',
5, 'LOB_DATA',
6, 'LOB_INDEX',
'UNDEFINED'),
ktssofno,ktssobno,
ktssoexts,ktssoblks,ktssorfno
FROM xktsso,vsession
WHERE ktssoses = vsession.saddrANDktssosno=vsession.serial#;
注意到在Oracle文档中对v$sort_usage字段SEGFILE#的定义为:
SEGFILE# NUMBER File number of initial extent
在视图中,这个字段来自x$ktsso.ktssofno,也就是说这个字段实际上代表的是绝对文件号。那么这个绝对文件号如何与临时文件关联呢?能否与v$tempfile中的file#字段关联呢?
再来看一下v$tempfile的来源,v$tempfile由如下语句创建:
sys@CCDB> select view_definition from v$fixed_view_definition
2 where view_name = 'GV$TEMPFILE';
VIEW_DEFINITION
--------------------------------------------------------------------------------
select tf.inst_id, tf.tfnum, to_number(tf.tfcrc_scn), to_date(tf.tfcrc_tim,'MM/D
D/RR HH24:MI:SS','NLS_CALENDAR=Gregorian'), tf.tftsn, tf.tfrfn, decode(bitand(tf
.tfsta, 2),0,'OFFLINE',2,'ONLINE','UNKNOWN'), decode(bitand(tf.tfsta, 12), 0,'DI
SABLED',4, 'READ ONLY', 12, 'READ WRITE',
'UNKNOWN'), fh.fhtmpfsz*tf.tfbsz, fh.fhtmpfsz, tf.tf
csz*tf.tfbsz,tf.tfbsz, fn.fnnam from xkcctftf,xkccfn fn, x$kcvfhtmp fh whe
re fn.fnfno=tf.tfnum and fn.fnfno=fh.htmpxfil and tf.tffnh=fn.fnnum and tf.tfdu
p!=0 and bitand(tf.tfsta, 32) <> 32 and fn.fntyp=7 and fn.fnnam is not null
格式化v$tempfile如下:
SELECT tf.inst_id,tf.tfnum,TO_NUMBER (tf.tfcrc_scn),
TO_DATE (tf.tfcrc_tim,'MM/DD/RR HH24:MI:SS','NLS_CALENDAR=Gregorian'),
tf.tftsn,tf.tfrfn,
DECODE (BITAND (tf.tfsta, 2), 0, 'OFFLINE', 2, 'ONLINE', 'UNKNOWN'),
DECODE (BITAND (tf.tfsta, 12),
0, 'DISABLED',
4, 'READ ONLY',
12, 'READ WRITE',
'UNKNOWN'),
fh.fhtmpfsz * tf.tfbsz,fh.fhtmpfsz,tf.tfcsz * tf.tfbsz,tf.tfbsz,fn.fnnam
FROM xkcctftf,xkccfn fn, x$kcvfhtmp fh
WHERE fn.fnfno = tf.tfnum
AND fn.fnfno = fh.htmpxfil
AND tf.tffnh = fn.fnnum
AND tf.tfdup != 0
AND BITAND (tf.tfsta, 32) <> 32
AND fn.fntyp = 7
AND fn.fnnam IS NOT NULL;
考察x$kcctf底层表,注意到TFAFN(Temp File Absolute File Number)在这里存在:
sys@CCDB> desc x$kcctf
Name Null? Type
---------------- -------- ----------------
ADDR RAW(8)
INDX NUMBER
INST_ID NUMBER
TFNUM NUMBER
TFAFN NUMBER
TFCSZ NUMBER
TFBSZ NUMBER
TFSTA NUMBER
TFCRC_SCN VARCHAR2(16)
TFCRC_TIM VARCHAR2(20)
TFFNH NUMBER
TFFNT NUMBER
TFDUP NUMBER
TFTSN NUMBER
TFTSI NUMBER
TFRFN NUMBER
TFPFT NUMBER
TFMSZ NUMBER
TFNSZ NUMBER
而这个字段在构建v$tempfile时并未出现,所以不能通过v$sort_usage和v$tempfile直接关联绝对文件号。可以简单构建一个排序段使用,然后来继续研究一下:
sys@CCDB> select username,segtype,segfile#,segblk#,extents,segrfno# from v$sort_usage;
USERNAME SEGTYPE SEGFILE# SEGBLK# EXTENTS SEGRFNO#
------------ --------- ---------- ---------- ---------- ----------
SYS LOB_DATA 201 340361 1 1
看到这里的SEGFILE#=201,而在v$tempfile是找不到这个信息的:
sys@CCDB> select file#,rfile#,ts#,status,blocks from v$tempfile;
FILE# RFILE# TS# STATUS BLOCKS
---------- ---------- ---------- ------- ----------
1 1 3 ONLINE 443520
但是可以从x$kcctf中获得这些信息,v$tempfile.file#实际上来自x$kcctf.tfnum,是临时文件的文件号;而绝对文件号是x$kcctf.tfafn,这个字段才可以与v$sort_usage.segfile#关联:
sys@CCDB> select indx,tfnum,tfafn,tfcsz from x$kcctf;
INDX TFNUM TFAFN TFCSZ
---------- ---------- ---------- ----------
0 1 201 2560
再进一步可以知道,实际上,为了分离临时文件号和数据文件号,Oracle对临时文件的编号以db_files为起点,所以临时文件的绝对文件号应该等于db_files+file#。
db_files参数的缺省值为200:
sys@CCDB> show parameter db_files
NAME TYPE VALUE
------------- -------------- -----------
db_files integer 200
sys@CCDB> select file#,name from v$tempfile;
FILE# NAME
---------- ---------------------------------------------
1 /oracle/oradata/ccdb/ccdb/temp01.dbf
所以在Oracle文档中v$tempfile.file#被定义为The absolute file number是不确切的。
WAITEVENT: "read by other session" Reference Note (文档 ID 732891.1)
当多个进程访问同一个数据块,而此数据块不在内存中,这时会有一个进程将它从磁盘读到内存时,其它读取此数据块进程的状态就是 read by other session;因为Oracle内存不允许多个进程同时读到同一个数据块到内存,其它进程只能等待。
当我们查询一条数据时,Oracle第一次会将数据从磁盘读入 buffer cache。如果有两个或者多个session请求相同的信息,那么第一个session会将这个信息读入buffer cache,其他的session就会出现等待。
SELECT A.*
FROM V$EVENT_NAME A
WHERE NAME IN ('read by other session');
l P1 = file# Absolute File# (AFN) This is the file number of the data file that contains the block that the waiting session wants.
l P2 = block# This is the block number in the above file# that the waiting session wants access to. See Note:181306.1 to determine the tablespace, filename and object for this file#,block# pair.
l P3 = class# Block class
This is the class of block being waited on. In particular:
class 1 indicates a "data block", which could be table or index
class 4 indicates a "segment header"
class >=15 indicate undo blocks
我们可以根据P1和P2参数值获取到等待的对象名称和类型:
SELECT SEGMENT_NAME, SEGMENT_TYPE, OWNER, TABLESPACE_NAME
FROM DBA_EXTENTS
WHERE FILE_ID = FILE#
AND BLOCK#
BETWEEN BLOCK_ID AND BLOCK_ID + BLOCKS - 1;
另外,其实我们也可以根据v$session的ROW_WAIT_OBJ#列获取到等待的对象的名称,SQL如下:
SELECT A.ROW_WAIT_OBJ#,
B.OBJECT_NAME,
A.SQL_ID,
A.SID,
A.BLOCKING_SESSION,
A.EVENT,
A.P1TEXT,
A.P1,
A.P2TEXT,
A.P2,
A.P3TEXT,
A.P3,
A.WAIT_CLASS
FROM V$SESSION A, DBA_OBJECTS B
WHERE A.ROW_WAIT_OBJ# = B.OBJECT_ID
AND A.EVENT='read by other session';
其实 read by other session 是在 Oracle 10g (10.1.0.2 and later) 新引入的一个等待事件,在 10g 以前版本,等待为 buffer busy waits,10g以后做的细分,所以才有了 read by other session。
Oracle官方解释如下:
This event occurs when a session requests a buffer that is currently being read into the buffer cache by another session. Prior to release 10.1, waits for this event were grouped with the other reasons for waiting for buffers under the 'buffer busy wait' event。
此等待事件从侧面也说明了数据库存在读的竞争,所以该等待事件经常会和db file sequential read 和db file scattered read同时出现。
下面是在 Metalink 上的解释:
Solution:
This wait event occurs when we are trying to access a buffer in the buffer
cache but we find that the buffer is currently being read from disk by another user
so we need to wait for that to complete before we can access it. In previous versions, this wait was classified under the "buffer busy waits" event. However, in Oracle 10.1 and higher, the wait time is now broken out into the "read by other session" wait event.
Excessive waits for this event are typically due to several processes repeatedly reading the same blocks, e.g. many sessions scanning the same index or performing full table scans on the same table. Tuning this issue is a matter of finding and eliminating this contention。
一般来说出现这种等待事件是因为多个进程重复的读取相同的 blocks,比如一些session 扫描相同的 index或者在相同的block上执行 full table scan。解决这个等待事件最好是找到并优化相关的SQL语句
1. 如果系统中有这种等待事件,我们可以通过以下SQL查询v$session_wait得到详细信息
SELECT p1 "file#", p2 "block#", p3 "class#"
FROM v$session_wait
WHERE event = 'read by other session';
2. 如果上述查询出的结果是热块造成的,运行如下 SQL,查询出具体对象信息,其实这部分可以直接从 AWR的Segments by Buffer Busy Waits 看出来。
SELECT RELATIVE_FNO, OWNER, SEGMENT_NAME, SEGMENT_TYPE
FROM DBA_EXTENTS
WHERE FILE_ID = &FILE
AND &BLOCK BETWEEN BLOCK_ID AND BLOCK_ID + BLOCKS - 1;
3. 可以通过下面的SQL脚本查询到具体的SQL语句
SELECT HASH_VALUE, SQL_TEXT
FROM V$SQLTEXT
WHERE (HASH_VALUE, ADDRESS) IN
(SELECT A.HASH_VALUE, A.ADDRESS
FROM V$SQLTEXT A,
(SELECT DISTINCT A.OWNER, A.SEGMENT_NAME, A.SEGMENT_TYPE
FROM DBA_EXTENTS A,
(SELECT DBARFIL, DBABLK
FROM (SELECT DBARFIL, DBABLK
FROM X$BH
ORDER BY TCH DESC)
WHERE ROWNUM < 11) B
WHERE A.RELATIVE_FNO = B.DBARFIL
AND A.BLOCK_ID <= B.DBABLK
AND A.BLOCKS > B.DBABLK) B
WHERE A.SQL_TEXT LIKE '%' || B.SEGMENT_NAME || '%'
AND B.SEGMENT_TYPE = 'TABLE')
ORDER BY HASH_VALUE, ADDRESS, PIECE;
4.查看对应 SQL 的执行计划是否最优,必要时可以通过 DBMS_SQLTUNE 包迚行优化,通过SQL_PROFILE 文件稳固执行计划
5.查看表和索引的统计信息是否陈旧,必要时收集统计信息
CREATE TABLE TB_RBOS_20160829_LHR AS SELECT * FROM DBA_OBJECTS;
INSERT INTO TB_RBOS_20160829_LHR SELECT * FROM TB_RBOS_20160829_LHR;
INSERT INTO TB_RBOS_20160829_LHR SELECT * FROM TB_RBOS_20160829_LHR;
INSERT INTO TB_RBOS_20160829_LHR SELECT * FROM TB_RBOS_20160829_LHR;
INSERT INTO TB_RBOS_20160829_LHR SELECT * FROM TB_RBOS_20160829_LHR;
INSERT INTO TB_RBOS_20160829_LHR SELECT * FROM TB_RBOS_20160829_LHR;
COMMIT;
SELECT DISTINCT SID FROM V$MYSTAT;
DECLARE
I NUMBER := 0;
V_STRING VARCHAR2(50) := 'alter system flush buffer_cache';
BEGIN
LOOP
SELECT COUNT(*) INTO I FROM TB_RBOS_20160829_LHR;
EXECUTE IMMEDIATE V_STRING;
END LOOP;
END;
/
SELECT A.SID,A.BLOCKING_SESSION,A.EVENT,A.P1TEXT,A.P1,A.P2TEXT,A.P2,A.P3TEXT,A.P3,A.WAIT_CLASS
FROM V$SESSION A
WHERE SID IN (190, 5, 68);
首先,建表TB_RBOS_20160829_LHR:
SYS@lhrdb> SELECT * FROM V$VERSION;
BANNER
--------------------------------------------------------------------------------
Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production
PL/SQL Release 11.2.0.4.0 - Production
CORE 11.2.0.4.0 Production
TNS for IBM/AIX RISC System/6000: Version 11.2.0.4.0 - Production
NLSRTL Version 11.2.0.4.0 - Production
SYS@lhrdb> CREATE TABLE TB_RBOS_20160829_LHR AS SELECT * FROM DBA_OBJECTS;
Table created.
SYS@lhrdb> INSERT INTO TB_RBOS_20160829_LHR SELECT * FROM TB_RBOS_20160829_LHR;
87145 rows created.
SYS@lhrdb> INSERT INTO TB_RBOS_20160829_LHR SELECT * FROM TB_RBOS_20160829_LHR;
174290 rows created.
SYS@lhrdb> INSERT INTO TB_RBOS_20160829_LHR SELECT * FROM TB_RBOS_20160829_LHR;
348580 rows created.
SYS@lhrdb> INSERT INTO TB_RBOS_20160829_LHR SELECT * FROM TB_RBOS_20160829_LHR;
697160 rows created.
SYS@lhrdb> INSERT INTO TB_RBOS_20160829_LHR SELECT * FROM TB_RBOS_20160829_LHR;
1394320 rows created.
SYS@lhrdb> COMMIT;
Commit complete.
我们开3个session分别清空buffer同时对表TB_RBOS_20160829_LHR做统计操作:
session 1:
SYS@lhrdb> SELECT DISTINCT SID FROM V$MYSTAT;
SID
----------
190
SYS@lhrdb> DECLARE
2 I NUMBER := 0;
3 V_STRING VARCHAR2(50) := 'alter system flush buffer_cache';
4 BEGIN
5 LOOP
6 SELECT COUNT(*) INTO I FROM TB_RBOS_20160829_LHR;
7 EXECUTE IMMEDIATE V_STRING;
8 END LOOP;
9 END;
10 /
session 2:
SYS@lhrdb> SELECT DISTINCT SID FROM V$MYSTAT;
SID
----------
5
SYS@lhrdb> DECLARE
2 I NUMBER := 0;
3 V_STRING VARCHAR2(50) := 'alter system flush buffer_cache';
4 BEGIN
5 LOOP
6 SELECT COUNT(*) INTO I FROM TB_RBOS_20160829_LHR;
7 EXECUTE IMMEDIATE V_STRING;
8 END LOOP;
9 END;
10 /
session 3:
SYS@lhrdb> SELECT DISTINCT SID FROM V$MYSTAT;
SID
----------
68
SYS@lhrdb> DECLARE
2 I NUMBER := 0;
3 V_STRING VARCHAR2(50) := 'alter system flush buffer_cache';
4 BEGIN
5 LOOP
6 SELECT COUNT(*) INTO I FROM TB_RBOS_20160829_LHR;
7 EXECUTE IMMEDIATE V_STRING;
8 END LOOP;
9 END;
10 /
session 4:(监控)
SELECT A.SID,A.BLOCKING_SESSION,A.EVENT,A.P1TEXT,A.P1,A.P2TEXT,A.P2,A.P3TEXT,A.P3,A.WAIT_CLASS
FROM V$SESSION A
WHERE SID IN (190, 5, 68);
由于是11g,满足DPR的特性,所以会走direct path read绕过SGA直接读取数据到PGA的,我们先禁用该特性:
SYS@lhrdb> alter system set "_serial_direct_read"=never scope=both sid='*';
System altered.
禁用之后继续查询:
可以看到等待事件已经变为了read by other session了,当然我们也可以在表上创建索引CREATE INDEX IND_XX_LHR ON TB_RBOS_20160829_LHR(OBJECT_ID) NOLOGGING;然后查询的时候SELECT COUNT(object_id) INTO I FROM TB_RBOS_20160829_LHR;可以走索引,这样的话模拟出来的也是read by other session等待事件了。
我们根据P1和P2参数值获取到访问的对象名称和类型:
SELECT SEGMENT_NAME, SEGMENT_TYPE, OWNER, TABLESPACE_NAME
FROM DBA_EXTENTS
WHERE FILE_ID =1
AND 162048
BETWEEN BLOCK_ID AND BLOCK_ID + BLOCKS - 1;
另外,其实我们也可以根据v$session的ROW_WAIT_OBJ#列获取到等待的对象的名称,SQL如下:
SELECT A.ROW_WAIT_OBJ#,
B.OBJECT_NAME,
A.SQL_ID,
A.SID,
A.BLOCKING_SESSION,
A.EVENT,
A.P1TEXT,
A.P1,
A.P2TEXT,
A.P2,
A.P3TEXT,
A.P3,
A.WAIT_CLASS
FROM V$SESSION A, DBA_OBJECTS B
WHERE A.ROW_WAIT_OBJ# = B.OBJECT_ID
AND SID IN(190,5,68);
SELECT * FROM V$SQL A WHERE A.SQL_ID='97pq51643d7b5';
找到SQL_ID,剩下的就是优化SQL语句了。
SELECT A.*
FROM V$EVENT_NAME A
WHERE NAME IN ('local write wait');
造成此等待事件的原因:
1) 磁盘损坏
2) 若执行TRUNCATE操作很慢,则可能由于表及其表上的索引的初始化值过大,可以通过SQL语句ALTER INDEX IND_BIG_TEMP REBUILD STORAGE (INITIAL 1M);和ALTER TABLE T_BIG MOVE STORAGE (INITIAL 1M);修改其初始化大小
truncates / reduce cache size
降低cache size
Basically 'local write' wait happens (as the name indicates) when the session is waiting for its local (means writes pending because of its own operation) write operation. This could happen typically if the underlying disc has some serious problems (one of the member disk crash in RAID-05 - for example, or a controller failure). That is why I might have said ' you never see this wait in the normal databases!'. You may see this during (rarely) Truncating a large table while most of the buffers of that table in cache. During TRUNCATEs the session has to a local checkpoint and during this process, the session may wait for 'local write' wait. We have not documented this in 'Oracle Wait Interface' as it is very uncommon. May be we can think of adding similar events in the Misc Waits in next edition.
翻译下: 基本上'local write wait' 表示会话在等待自己的写操作。在磁盘发生严重问题时会发生(例如RAID 5的一个磁盘崩溃,或者磁盘控制器错误),这在正常的系统中极少发生,在TRUNCATE一个大表而这个表在缓存中的时候,会话必需进行一个local checkpoint,这个时候会话会等待local session wait. 在开发环境里面truncate一些列表,速度奇慢。看了一下session等待事件 Local Write Wait
local write wait 主要是在dbwr 将脏数据写回dbf过程中产生的。
可以考虑调整dbwr的效率。
另外,从设计上看,可以采用分区表。 把truncate 操作改成drop partition 的操作。
Sorry for the delay in my reply. I was traveling and was in Beirut for
a week and just returned to Bangalore. You have got some valuable advice (esp the one from Jonathan).
Basically 'local write' wait happens (as the name indicates) when the session is waiting for its local (means writes pending because of its own operation) write operation. This could happen typically if the underlying disc has some serious problems (one of the member disk crash in RAID-05 - for example, or a controller failure). That is why I might have said ' you never see this wait in the normal databases!'. You may see this during (rarely) Truncating a large table while most of the buffers of that table in cache. During TRUNCATEs the session has to a local checkpoint and during this process, the session may wait for 'local write' wait.
We have not documented this in 'Oracle Wait Interface' as it is very uncommon. May be we can think of adding similar events in the Misc Waits in next edition.
During TRUNCATEs the session has to a local checkpoint and during this process, the session may wait for
'local write' wait.
果然有local checkpoint

About Me
..........................................................................................................................................................................................................
● 本文作者:小麦苗,只专注于数据库的技术,更注重技术的运用
● 本文在ITpub(http://blog.itpub.net/26736162)、博客园(http://www.cnblogs.com/lhrbest)和个人微信公众号(xiaomaimiaolhr)上有同步更新,推荐pdf文件阅读或博客园地址阅读
● QQ群:230161599 微信群:私聊
● 本文itpub地址:http://blog.itpub.net/26736162/viewspace-2124417/ 博客园地址:http://www.cnblogs.com/lhrbest/articles/5835420.html
● 本文pdf版:http://yunpan.cn/cdEQedhCs2kFz (提取码:ed9b)
● 小麦苗分享的其它资料:http://blog.itpub.net/26736162/viewspace-1624453/
● 联系我请加QQ好友(642808185),注明添加缘由
● 于 2016-08-13 09:00~2016-09-02 19:00 在中行完成
● 【版权所有,文章允许转载,但须以链接方式注明源地址,否则追究法律责任】
..........................................................................................................................................................................................................
长按识别二维码或微信客户端扫描下边的二维码来关注小麦苗的微信公众号:xiaomaimiaolhr,学习最实用的数据库技术。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/28211342/viewspace-2131298/ ,如需转载,请注明出处,否则将追究法律责任。