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

Oracle数据库的跟踪和分析方法

原创 由迪 2020-10-02
5014

在今天的技术领域,DevOps已经成为最热门的话题之一,DevOps是开发和运维一体化的实践趋势,也是运维掌握一定的开发能力,推动和协助开发进行适应高效运维的渐进变革。
在我的技术生涯中,对Oracle数据库的接触最多,感受也最深。如果说要将最值得推荐的技能展示给大家,那么我想推荐的就是Oracle跟踪方法。事实上,通过跟踪能够实现的也正是不断了解、接近开发的思路和方法,从而有助于运维中的问题诊断、排查和解决。
我在一个活动上分享过一段话,摘录在这里,作为我这一章内容的开始。
“早上我听到一句话印象深刻,叫“隐藏的权利感”,我想把这句话应用到数据库,表达一下我的观点。
Oracle数据库,虽然是一个商用数据库不开源,但是它又是非常开放的一个产品,Oracle几乎所有的内部操作,不管是调优的过程还是数据库的各种内部操作,都是可跟踪解析的。比如Oracle数据库的启动和关闭过程,全程是可跟踪的。它的启动关闭会解析成多少个递归操作,我们全都可以跟踪出来。
所以我们做Oracle DBA的工作时,面对任何事情我们都会非常有信心。Oracle开放了各种接口,方法和手段给我们,只要我们去分析研究,就能够把一个问题的Root Cause找出来,接近Root Cause就离解决问题不远了。
一个数据库只有变得更加开放接口,更加开放DeBug功能的,才能让我们在研究这个数据库的时候也可以找到更多的乐趣。我觉得这里面找到的乐趣就是我讲的,是隐藏的权利感。就是我不动声色,但是我知道我在处理接触这个数据库的时候,我有非常强的把控力,我能撼动和解决几乎所有的问题。我觉得这一点对于技术人员是非常重要的。”
Oracle数据库的这些基本跟踪方法,伴随着我的技术成长和排忧解难的职业历程,以下详细的通过案例进行解析。
4.1 SQL_TRACE及10046事件
最常用的跟踪方式是通过初始化参数SQL_TRACE或者设置10046事件。首先从文档了解一下SQL_TRACE基本介绍,以使大家能对这个工具有所了解,并熟悉其使用方法。
4.2 SQL_TRACE说明
先来关注一下Oracle 12c官方文档对SQL_TRACE的说明,如表4-1所示。
表4-1
参 数 类 型 布 尔 型
默认值 false
修改方式 ALTER SESSION ,ALTER SYSTEM
PDB修改 Yes
取值范围 true | false

通过设置SQL_TRACE可以启用或禁用SQL 跟踪工具,设置SQL_trace为true可以收集信息用于性能优化或问题诊断;DBMS_SYSTEM包也可以用于实现类似的功能。
以下警告在不同版本的文档中几乎没有任何变化。
警告 设置初始化参数SQL_TRACE为true会对整个实例产生严重的性能影响,所以在产品环境中如非必要,确保不要设置这个参数。如果只是对特定的session启用跟踪,可以使用ALTER SESSION或DBMS_SYSTEM.SET_SQL_TRACE_IN_SESSION来设置。如果必须在数据库级启用SQL_TRACE,你需要保证以下条件以最小化性能影响。
(1)至少保证有25%的CPU idle。
(2)为USER_DUMP_DEST分配足够的空间。
(3)条带化磁盘以减轻IO负担。
在12c中文档中提示:不再支持SQL_TRACE参数,推荐使用DBMS_MONITOR和DBMS_SESSION包来替代其功能,该参数作为向后兼容而保留,其原有功能仍然存在。而事实上,DBA的工作中,SQL_TRACE很少被使用,更多的是10046事件。
自Oracle 10g开始,SQL_TRACE参数才成为动态参数,可以在全局动态启用,在实践中除了研究目的,很少需要如此在全局设置。
SQL> alter system set SQL_trace=true;
System altered.

大多数时候我们使用SQL_trace跟踪当前会话,通过跟踪当前会话可以发现当前操作的后台递归活动(这在研究数据库新特性时尤其有效),研究SQL执行,发现后台错误等。
在session级启用和停止SQL_trace方式如下所示。
SQL> alter session set SQL_trace=true; --启用当前session的跟踪:
SQL> select count() from dba_users; --此时的SQL操作将被跟踪
COUNT(
)

    34
复制

SQL> alter session set SQL_trace=false; --结束跟踪
SQL> select value from v$diag_info where name=‘Default Trace File’;

在很多时候需要跟踪其他用户的进程,而不是当前用户,这可以通过Oracle提供的系统包DBMS_SYSTEM的SET_SQL_TRACE_IN_SESSION过程来完成,该过程调用提供三个参数。
PROCEDURE SET_SQL_TRACE_IN_SESSION
Argument Name Type In/Out Default?


SID NUMBER IN
SERIAL# NUMBER IN
SQL_TRACE BOOLEAN IN

通过查询vsession可以获得会话的sid、serial#等信息,获得会话信息之后就可以设置跟踪。 SQL> select sid,serial#,username from vsession
2 where username is not null;
SID SERIAL# USERNAME


     8       2041  SYS
     9        437  EYGLE
复制

SQL> exec dbms_system.set_SQL_trace_in_session(9,437,true)
SQL> exec dbms_system.set_SQL_trace_in_session(9,437,false)

DBMS_SYSTEM包功能非常强大,是重要的跟踪手段之一。
SQL_TRACE还可以通过如下方式针对特定的SQL启用跟踪,指定SQL_ID的SQL会被按照指定条件跟踪其执行过程,这在分析特定SQL时非常有效。
ALTER SYSTEM SET EVENTS ‘SQL_trace [SQL:&&SQL_id] bind=true, wait=true’;
4.2.1 DBMS_SYSTEM跟踪案例
以下这个案例是我走上DBA道路第一个帮助朋友解决的问题。这个问题的解决过程,就是利用自己获得的已知知识,通过思考、跟踪去发现问题,并给出解决方案,对我的成长意义非凡。
这个应用是一个后台新闻发布系统,前端展现是一个网站。JAVA开发,通过中间件连接池连接数据库。当时系统症状是访问新闻页极其缓慢,后台发布管理具有同样的问题。通常需要数十秒才能返回。
处理这个问题时,通过前台或者应用代码去分析会变得十分复杂,我想到的第一个办法就是启用跟踪,然后通过分析跟踪文件找出瓶颈所在。诊断时间在晚上,在无集中用户访问的情况下,让用户在前台进行相关页面的访问,同时进行进程跟踪。
首先通过查询vsession视图,获取进程信息。 SQL> select sid,serial#,username from vsession where username is not null;
SID SERIAL# USERNAME


     7        284 IFLOW
    11        214 IFLOW
    12        164 SYS
    16       1042 IFLOW
复制

然后对相应的应用会话启用SQL_trace跟踪如下。
SQL> exec dbms_system.set_SQL_trace_in_session(7,284,true)
SQL> exec dbms_system.set_SQL_trace_in_session(11,214,true)
SQL> exec dbms_system.set_SQL_trace_in_session(16,1042,true)

应用执行一段时间后,关闭SQL_trace。
SQL> exec dbms_system.set_SQL_trace_in_session(7,284,false)
SQL> exec dbms_system.set_SQL_trace_in_session(11,214,false)
SQL> exec dbms_system.set_SQL_trace_in_session(16,1042,false)

找到跟踪生成的跟踪文件,然后通过Oracle提供的格式化工具——tkprof对trace文件进行格式化处理,筛查其中消耗时间部分。通过检查,发现类似以下语句的是可疑的。


select auditstatus,categoryid,auditlevel from
categoryarticleassign a,category b where b.id=a.categoryid and articleId=
20030700400141 and auditstatus>0

call count cpu elapsed disk query current rows


Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 1 0.81 0.81 0 3892 0 1


total 3 0.81 0.81 0 3892 0 1


这里的查询显然是根据articleId进行新闻读取的。但是注意到逻辑读有3892,这是较高的一个数字,这个内容引起了我的注意。
接下来的类似查询跟踪得到的执行计划显示,全表访问被执行。
select auditstatus,categoryid from
categoryarticleassign where articleId=20030700400138 and categoryId in (‘63’,
‘138’,‘139’,‘140’,‘141’,‘142’,‘143’,‘144’,‘168’,‘213’,‘292’,‘341’’-1’)
call count cpu elapsed disk query current rows


Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 1 4.91 4.91 0 2835 7 1


total 3 4.91 4.91 0 2835 7 1

Rows Row Source Operation


  1 TABLE ACCESS FULL CATEGORYARTICLEASSIGN
复制

登录数据库,检查相应表结构,看是否存在有效的索引,以下输出中的IDX_ARTICLEID是基于ARTICLEID创建的,但是在以上查询中都没有被用到。
SQL> select index_name,table_name,column_name from user_ind_columns
2 where table_name=upper(‘categoryarticleassign’);
INDEX_NAME TABLE_NAME COLUMN_NAME


IDX_ARTICLEID CATEGORYARTICLEASSIGN ARTICLEID
IND_ARTICLEID_CATEG CATEGORYARTICLEASSIGN ARTICLEID
IND_ARTICLEID_CATEG CATEGORYARTICLEASSIGN CATEGORYID
IDX_SORTID CATEGORYARTICLEASSIGN SORTID
PK_CATEGORYARTICLEASSIGN CATEGORYARTICLEASSIGN ARTICLEID
PK_CATEGORYARTICLEASSIGN CATEGORYARTICLEASSIGN CATEGORYID
PK_CATEGORYARTICLEASSIGN CATEGORYARTICLEASSIGN ASSIGNTYPE

检查下表结构。
SQL> desc categoryarticleassign
Name Null? Type


CATEGORYID NOT NULL NUMBER
ARTICLEID NOT NULL VARCHAR2(14)
ASSIGNTYPE NOT NULL VARCHAR2(1)
AUDITSTATUS NOT NULL NUMBER
SORTID NOT NULL NUMBER
UNPASS VARCHAR2(255)

分析表结构发现了问题所在,因为ARTICLEID是个字符型数据,查询中给入的articleId= 20030700400141 是一个数字值,Oracle执行查询时需要将ARTICLEID转换为数字和给定值进行比较,发生潜在的数据类型转换,这就导致了索引不能被采用,产生了全表扫描的执行计划,在客户的系统中大量类似如下的SQL在通过全表扫描产生大量的IO操作。
SQL> select auditstatus,categoryid
2 from categoryarticleassign where articleId=20030700400132;
AUDITSTATUS CATEGORYID


      9         94                                     
      0        383                                     
      0        695                                     
复制

Execution Plan

0 SELECT STATEMENT Optimizer=CHOOSE (Cost=110 Card=2 Bytes=38)
1 0 TABLE ACCESS (FULL) OF ‘CATEGORYARTICLEASSIGN’ (Cost=110 Card=2 Bytes=38)

在这里很有必要解释一下Oracle数据库的数据转换,以下一段内容主要来自官方文档的介绍(依据Oracle 12c版本,本章末有一节附上了详细的译文)。
通常一个表达式不能包含不同的数据类型,例如一个表达式不能计算5x10之后再加上’JAMES’,数字和字符无法进行联合的计算。然而,Oracle支持显示和隐式的数据类型转换,可以将一种数据类型转换为另一种,从而使得某些表达式的运算可以正确执行。显示转换是指通过函数明确指定的数据类型转换,而隐式转换则指未明确指定,依赖Oracle自动进行的数据类型转换。
基于以下几个原因,Oracle推荐使用显示类型转换而不是依赖隐式转换。
(1)使用显示转换使得SQL含义更容易被理解。
(2)隐式数据类型转换会产生负面的性能影响,尤其是当列值被转换成其他常量的数据类型时。
(3)隐式转换的行为依赖每次转换时的环境如数据库参数设置等,其行为可能多变而不确定,例如将datetime值隐式转换为VARCHAR2,这个转换的格式和NLS_DATE_FORMAT 参数有关,可能产生不可预期的结果。
(4)隐式转换的算法会因数据库版本而不同,而显示转换则可以预期。
(5)如果隐式转换发生在索引列,则Oracle可能用不到索引而影响性能。
在现实的开发环境中,绝大多数隐式转换是开发者无意中引入的,但却导致了大量的性能问题。这个案例就处于这样的场景之中。
要知道,在字符和数字进行比对时,Oracle总是将字符转换为数字进行比对。解决本例问题的方法很简单,只须在传入参数两侧各增加一个单引号使其作为字符传入,即可解决这个问题。重新测试类似的查询,可以发现Query模式逻辑读降低为2,占用CPU时间也大大减少。


select unpass from
categoryarticleassign where articleid=‘20030320000682’ and categoryid=‘113’

call count cpu elapsed disk query current rows


Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 1 0.00 0.00 0 2 0 0


total 3 0.00 0.00 0 2 0 0

Rows Row Source Operation


  0  TABLE ACCESS BY INDEX ROWID CATEGORYARTICLEASSIGN 
  1   INDEX RANGE SCAN (object id 3080)
复制

至此,这个问题得到了完满的解决。但是关于隐式转换的故事还远远没有结束。
在从事数据库工作多年之后,我们发现大多数DBA仍然在面对我们10多年前面对的同样问题,仍然频繁地在解决系统中的隐式转换、索引失效、全表扫描问题。重复的工作就必须找出更高效的、自动化的手段去消解,这也是今天DevOps时代的要义之一。
我们的理念就是,从遇到的问题总结规则,聚规则而成规范,由规范衍生工具,通过工具替代人力。
如果从以上案例总结规范,至少有以下两条需要开发去注意改进。
(1)使用绑定变量——绑定变量可以减少硬解析,带来性能上的改进,这是最基本的开发实践。
(2)避免隐式转换——隐式转换可能带来索引失效影响性能,也会产生不可预期的程序效果,应当尽量避免。
一个DBA如果能够从实践中不断积累、提炼、上升,那么就能够在企业技术架构中承载更重要的使命和职责。
以上整个问题的改善,事实上就是在运维Ops的过程中,发现开发Dev中存在的问题,如果能够进而通过运维去促进开发,提升运维,这就是DevOps的使命和范畴了。
在现实环境中,我们在很多系统中都看到,大量的性能问题都是由于简单的疏忽导致的,而且由于问题的隐蔽性等,这些问题一旦在线上爆发出来,会给诊断优化带来相当的难度,同时会影响业务的正常运行,所以完善的规范和良好的编码对于一个系统来说是至关重要的。DevOps在Oracle数据库开发中的最佳实践至少可以包括以下两点。
(1)基于运维的开发培训——通过运维中的实践总结,将规则方法推进到开发端,持续改进开发质量。
(2)优化前置的SQL审核——将事后救火变更为事前审核,在开发测试阶段发现和解决问题。
治本永远强于治标。
4.2.2 系统递归调用的跟踪
很多时候在进行数据库DDL操作时,Oracle会在后台将这些操作转换为一系列的DML和查询操作。如果其中的某个步骤出现异常,通常其错误提示不容易判定问题所在。这时候跟踪的作用就体现出来。
以下错误是在一次DROP USER中出现的,单从类似这样的提示来看,很多时候是没有丝毫用处的。
ORA-00604: error occurred at recursive SQL level 1
ORA-00942: table or view does not exist .

关于 recursive SQL 错误,我们有必要做个简单说明。
当我们发出一条简单的DDL命令以后,Oracle数据库要在后台解析这条命令,并转换为Oracle数据库的一系列后台操作。这些后台操作统称为递归SQL。
比如create table这样一条简单的DDL命令,Oracle数据库在后台,实际上要把这个命令转换为对于obj、tab、col$等底层表的插入操作。对于drop table操作,则是在这些系统表中进行反向删除操作。
在面对这样的问题时,通过SQL_trace进行后台跟踪,就可以进一步了解Oracle数据库的后台操作,找出问题点。
SQL> alter session set SQL_trace=true;
SQL> drop user wapcomm;
ORA-00604: error occurred at recursive SQL level 1
ORA-00942: table or view does not exist .
SQL> alter session set SQL_trace=false;

格式化(使用tkprof)跟踪文件后,我们获得以下输出(摘录部分)。


The following statement encountered a error during parse:
DELETE FROM SDO_GEOM_METADATA_TABLE WHERE SDO_OWNER = ‘WAPCOMM’
Error encountered: ORA-00942


drop user wapcomm


delete from user_history$
where
user# = :1 -----后台的递归删除操作…

Rows Row Source Operation


  1  DELETE USER_HISTORY$ 
  1   TABLE ACCESS FULL USER_HISTORY$ 
复制

declare
stmt varchar2(200);
BEGIN
if dictionary_obj_type = ‘USER’ THEN
stmt := ‘DELETE FROM SDO_GEOM_METADATA_TABLE ’ ||
’ WHERE SDO_OWNER = ‘’’ || dictionary_obj_name || ‘’’ ';
EXECUTE IMMEDIATE stmt;
end if;
end;

call count cpu elapsed disk query current rows


Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 2 0
Fetch 0 0.00 0.00 0 0 0 0


total 2 0.00 0.00 0 0 2 0


使用TKPROF格式化以后,Oracle把错误信息首先呈现出来。ORA-00942错误是由于SDO_GEOM_METADATA_TABLE表/视图不存在所致,问题由此可以定位。对于这一类的错误,定位问题以后解决的方法就要依据具体问题原因而定了。同时可以通过Oracle的官方支持站点MOS(My Oracle Support)去定位是否Bug引起的,是否已经有明确的解决方案。对于本例以关键字SDO_GEOM_METADATA_TABLE检索可以确认这是一个Bug,并且给出了解决方案。
Problem Description

The Oracle Spatial Option has been installed and you are encountering the following errors while trying to drop a user, who has no spatial tables, connected as SYSTEM:

ERROR at line 1:
ORA-00604: error occurred at recursive SQL level 1
ORA-00942: table or view does not exist
ORA-06512: at line 7

A 942 error trace shows the failing SQL statement as:
DELETE FROM SDO_GEOM_METADATA_TABLE WHERE SDO_OWNER = ‘

Solution Description

(1) Create a synonym for SDO_GEOM_METADATA_TABLE under SYSTEM which points to
MDSYS.SDO_GEOM_METADATA_TABLE.
(2) Now the user can be dropped connected as SYSTEM.

对于本例,为MDSYS.SDO_GEOM_METADATA_TABLE创建一个同义词即可解决,是相对简单的情况。这是一个Oracle早期版本的问题,只是这样的分析方法应当被DBA掌握。
跟踪的目标就是,找出根源,解决问题。只要能够找出问题的根源,或者接近根源,那么距离解决问题也就不远了。我推荐的提问方式也是,当遇到问题时,通过跟踪、日志分析更进一步,而不是只依据浅层的表象提出。
4.3 10046与等待事件
10046事件是Oracle提供的内部跟踪事件,是对SQL_TRACE的增强,通过10046可以通知Oracle内核执行SQL_TRACE类的跟踪操作。如果我们需要获得更多的跟踪信息,就需要用到10046事件,而在实际工作中最常用的就是10046事件。
自11g开始,10046获得了更多的增强,包括明确的在设置中指定SQL_trace,类似’SQL_trace wait=false, bind=true’这样的设定。
在常规的使用方法下,10046事件的以下几个跟踪级别最为常用(后面的参数是11g的新设置中可用的参数设定)。
1 - 启用标准的SQL_TRACE功能,等价于SQL_trace
4 - Level 1 加上绑定值(bind values) [ bind=true ]
8 - Level 1 + 等待事件跟踪 [ wait=true ]
12 - Level 1 + Level 4 + Level 8

从11g开始增加了以下两个跟踪级别。
16 – 为每次SQL执行生成STAT信息输出 [ plan_stat=all_executions ]
32 – 不转储执行统计信息 [ plan_stat=never ]

从 11.2.0.2开始增加以下级别。
64 – 自适应的STAT转储 [ plan_stat=adaptive ]

我们知道,在进行数据库问题诊断及性能优化时,经常需要查询的几个重要视图包括vsession_wait、vsystem_event等,这些视图中主要记录的就是等待事件。
通过调整以降低等待,是提高性能的一个方法。这些等待事件来自所有数据库操作,对于不同进程的等待可以通过动态性能视图vsession_wait等来查询;对于数据库全局等待可以通过vsystem_event等视图来获得。
同样的,通过10046事件对具体session进行跟踪,可以获得每个session的执行情况及等待事件、统计信息等详细信息,输出到外部跟踪文件以进行分析。
类似SQL_trace,10046事件可以在全局设置,也可以在session级设置。在全局设置,可以在参数文件中增加Event参数,此设置对所有用户的所有进程生效,包括后台进程,所以同样除了研究目的应当很少被使用。
event=“10046 trace name context forever,level 12”

通过alter session的方式修改,需要alter session的系统权限。
SQL> alter session set events ‘10046 trace name context forever, level 12’; – 启用跟踪
SQL> alter session set events ‘10046 trace name context off’; --停止跟踪

11g之后的可选设置方式类似。
alter session set events ‘SQL_trace wait=true’;

对其他用户session设置事件跟踪,同样可以通过DBMS_SYSTEM的SET_EV过程来实现。
PROCEDURE SET_EV
Argument Name Type In/Out Default?


SI BINARY_INTEGER IN
SE BINARY_INTEGER IN
EV BINARY_INTEGER IN
LE BINARY_INTEGER IN
NM VARCHAR2 IN

其中的参数SI、SE来自vsession视图。 查询获得需要跟踪的session信息。 SQL> select sid,serial#,username from vsession where username is not null;
SID SERIAL# USERNAME


     8       2041 SYS
     9        437 EYGLE
复制

执行跟踪。
SQL> exec dbms_system.set_ev(9,437,10046,8,’’);
PL/SQL procedure successfully completed.

结束跟踪。
SQL> exec dbms_system.set_ev(9,437,10046,0,’’);
PL/SQL procedure successfully completed.

基于会话级别跟踪产生的文件,可以通过查询V$DIAG_INFO视图,找到跟踪文件的名称和位置信息,查看其中的内容。
以下通过一个简单的测试来看看10046事件的作用。
SQL> create table eygle as select * from dba_objects;
SQL> select file_id,block_id,blocks from dba_extents where segment_name=‘EYGLE’;

FILE_ID BLOCK_ID BLOCKS


     1      21601          8
     1      21609          8
     1      21617          8
     1      21625          8
     1      21633          8
     1      23433          8
     1      23441          8
     1      23449          8
     1      23457          8
     1      23465          8
复制

SQL> alter session set events ‘10046 trace name context forever,level 12’;
SQL> select count() from eygle; --表上无索引,所以此处引发一次全表扫描
COUNT(
)

  6207
复制

SQL> alter session set events ‘10046 trace name context off’;

检查一下Oracle生成的跟踪文件,比如关注一下等待事件 – db file scattered read (注意,自11g开始,由于Serial Table Scan的特性引入,你可能观察到全表扫描以Direct Path Read方式执行)。
[oracle@eygle udump]$ cat rac1_ora_20695.trc |grep scatt
WAIT #1: nam=‘db file scattered read’ ela= 11657 p1=1 p2=21602 p3=7
WAIT #1: nam=‘db file scattered read’ ela= 1363 p1=1 p2=21609 p3=8
WAIT #1: nam=‘db file scattered read’ ela= 1297 p1=1 p2=21617 p3=8
WAIT #1: nam=‘db file scattered read’ ela= 1346 p1=1 p2=21625 p3=8
WAIT #1: nam=‘db file scattered read’ ela= 1313 p1=1 p2=21633 p3=8
WAIT #1: nam=‘db file scattered read’ ela= 6226 p1=1 p2=23433 p3=8
WAIT #1: nam=‘db file scattered read’ ela= 1316 p1=1 p2=23441 p3=8
WAIT #1: nam=‘db file scattered read’ ela= 1355 p1=1 p2=23449 p3=8
WAIT #1: nam=‘db file scattered read’ ela= 1320 p1=1 p2=23457 p3=8
WAIT #1: nam=‘db file scattered read’ ela= 884 p1=1 p2=23465 p3=5

大家注意这里的等待事件’db file scattered read’,意味着这里使用了全表扫描来访问数据.其中 p1,p2,p3分别代表了文件号、起始数据块块号和读取数据块的数量。
各参数的含义也可以从vevent_name视图中获得。 SQL> select name,PARAMETER1 p1,PARAMETER2 p2,PARAMETER3 p3 2 from vevent_name where name=‘db file scattered read’;
NAME P1 P2 P3


db file scattered read file# block# blocks

在数据库内部,这些等待时间最后都会累计到vsystem_event动态性能视图中,是数据库性能诊断的一个重要参考。 SQL> select event,time_waited from vsystem_event
2 where event=‘db file scattered read’;
EVENT TIME_WAITED


db file scattered read 51

这里我们有必要提到另外一个相关的初始化参数db_file_multiblock_read_count,这个参数代表Oracle在执行全表扫描时每次IO操作可以读取的数据块的数量。
在前面的测试中,db_file_multiblock_read_count参数设置为16,由于extent大小为8个block,Oracle的一次IO操作不能跨越extent,所以前面的全表扫描每次只能读取8个block,进行了10次IO读取。
看一下进一步的测试。
SQL> create tablespace eygle datafile ‘/dev/raw/raw2’ size 100M
2 extent management local uniform size 256K;
SQL> alter table eygle move tablespace eygle;
SQL> select file_id,block_id,blocks from dba_extents where segment_name=‘EYGLE’;
FILE_ID BLOCK_ID BLOCKS


     4          9         32
     4         41         32
     4         73         32
复制

SQL> show parameter read_count
NAME TYPE VALUE


db_file_multiblock_read_count integer 16
SQL> alter session set events ‘10046 trace name context forever,level 12’;
SQL> select count() from eygle;
COUNT(
)

  6207	
复制

SQL> alter session set events ‘10046 trace name context off’;
SQL> select value TRACE_FILE from v$diag_info where name=‘Default Trace File’;
TRACE_FILE

/opt/oracle/admin/rac/udump/rac1_ora_20912.trc

[oracle]$ cat /opt/oracle/admin/rac/udump/rac1_ora_20912.trc |grep scatt
WAIT #1: nam=‘db file scattered read’ ela= 12170 p1=4 p2=10 p3=16
WAIT #1: nam=‘db file scattered read’ ela= 2316 p1=4 p2=26 p3=15
WAIT #1: nam=‘db file scattered read’ ela= 2454 p1=4 p2=41 p3=16
WAIT #1: nam=‘db file scattered read’ ela= 2449 p1=4 p2=57 p3=16
WAIT #1: nam=‘db file scattered read’ ela= 2027 p1=4 p2=73 p3=13

可以看到此时Oracle只需要5次IO操作就完成了全表扫描。通常较大的db_file_multiblock_ read_count设置可以加快全表扫描的执行。但是需要注意的是,增大db_file_multiblock_read_ count参数的设置,会使全表扫描的成本降低,在CBO优化器下可能会使Oracle更倾向于使用全表扫描而不是索引访问,一般建议使用默认值。
4.3.1 通过跟踪理解数据库的初始化
在DBA的职业生涯中,会面临众多的挑战,其中最重要的一种情况是数据库无法启动,所以深入理解Oracle数据库的初始化非常重要。通过Oracle的跟踪手段,可以帮助我们获取这些知识,在我的学习过程中,一直在不断地通过跟踪去研究熟悉的或不熟悉的特性和功能,从而加深自己对于数据库的理解。
对于Oracle数据库的初始化,我最初的思考是:数据库的核心信息都是存放在数据文件当中的,但是当数据库尚未打开之前,Oracle是无法获得这部分数据的。那么Oracle是怎样完成这个从数据文件到内存的初始化过程的呢?
首先通过以下步骤对数据库的OPEN过程进行跟踪,研究获得的跟踪文件。
SQL> startup mount;
SQL> alter session set events=‘10046 trace name context forever,level 12’;
SQL> alter database open;

以上通过10046跟踪获得一个跟踪文件,跟踪文件里将记录从mount到open的过程中,Oracle所执行的后台操作。可以通过tkprof工具对跟踪文件进行格式化,使得其中的信息更便于阅读。首先我们来参考跟踪文件的前面部分(我的研究首先从Oracle 9i开始,逐渐推演到Oracle 12c,研究不同版本的引导过程方法完全相同),这是第一个对象的创建。
create table bootstrap$
( line# number not null,
obj# number not null,
SQL_text varchar2(4000) not null)
storage (initial 50K objno 56 extents (file 1 block 520))

注意:在这一步骤中,实际上Oracle是在内存中创建bootstrap$的结构,然后从数据文件中读取数据到内存中,完成第一次初始化。在9i中,读取的位置是文件1的377块,自从11g之后变更为文件1的520块。注意此处的file 1 block 520子句是内部语句,意味这这些对象的存储位置是固定的,该语法对用户创建对象是不可用的。
从数据库的创建脚本 ORACLE_HOME/rdbms/admin/SQL.bsq 文件中,可以获得bootstrap表的初始创建语句,直至12c这些定义未曾变化(在12c中SQL.bsq分解为一系列的bsq文件,dcore.bsq中记录了下面这段代码)。
create table bootstrap$
( line# number not null, /* statement order id /
obj# number not null, /
object number /
SQL_text varchar2(“M_VCSZ”) not null) /
statement /
storage (initial 50K) /
to avoid space management during IOR I /
// /
“//” required for bootstrap */

接下来从数据库中查询一下,file 1 block 520 上存储的是什么对象。
SQL> select segment_name,file_id,block_id
2 from dba_extents where block_id=520 and file_id=1;
SEGMENT_NAME FILE_ID BLOCK_ID


BOOTSTRAP$ 1 520

File 1 Block 520开始存放的正是Bootstrap对象。继续查看Trace文件的内容,Oracle进一步执行的是如下操作。 select line#, SQL_text from bootstrap where obj# != :1

在创建并从数据文件中装载了bootstrap的内容之后,Oracle开始递归的从该表中读取信息,加载数据。那么bootstrap中记录的是什么信息呢?
在数据库中,bootstrap是一张实际存在的系统表。 SQL> desc bootstrap
Name Null? Type


LINE# NOT NULL NUMBER
OBJ# NOT NULL NUMBER
SQL_TEXT NOT NULL VARCHAR2(4000)

来看一下这张表的具体内容。
SQL> select * from bootstrap$ where rownum <5;
LINE# OBJ# SQL_TEXT


-1    -1 8.0.0.0.0
 0     0 CREATE ROLLBACK SEGMENT SYSTEM STORAGE (  INITIAL 112K NEXT 1024K MINE
         XTENTS 1 MAXEXTENTS 32765 OBJNO 0 EXTENTS (FILE 1 BLOCK 9))
 8     8 CREATE CLUSTER C_FILE#_BLOCK#("TS#" NUMBER,"SEGFILE#" NUMBER,"SEGBLOCK
         #" NUMBER) PCTFREE 10 PCTUSED 40 INITRANS 2 MAXTRANS 255 STORAGE (  IN
         ITIAL 24K NEXT 1024K MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0
         OBJNO 8 EXTENTS (FILE 1 BLOCK 73)) SIZE 225
 9     9 CREATE INDEX I_FILE#_BLOCK# ON CLUSTER C_FILE#_BLOCK# PCTFREE 10 INITR
         ANS 2 MAXTRANS 255 STORAGE (  INITIAL 64K NEXT 1024K MINEXTENTS 1 MAXE
         XTENTS 2147483645 PCTINCREASE 0 OBJNO 9 EXTENTS (FILE 1 BLOCK 81))
复制

以上输出只显示了表中的4条记录,大家可以自行研究一下其他记录的内容。从这些语句中可以看出,bootstrap$ 中实际上是记录了一些数据库系统基本对象的创建语句。Oracle通过bootstrap进行引导,进一步创建相关的重要对象,从而启动了数据库。 如果向前追溯,可以继续考察一下bootstrap的创建过程。查看一下创建数据库的脚本,可以发现数据库在创建过程中最先运行的是一个叫做CreateDB.SQL的脚本。这个脚本发出CREATE DATABASE的命令,具体类似如下的例子。
CREATE DATABASE eygle
MAXINSTANCES 1 MAXLOGHISTORY 1 MAXLOGFILES 5 MAXLOGMEMBERS 3 MAXDATAFILES 100
DATAFILE ‘/opt/oracle/oradata/eygle/system01.dbf’
SIZE 250M REUSE AUTOEXTEND ON NEXT 10240K MAXSIZE UNLIMITED EXTENT MANAGEMENT LOCAL
DEFAULT TEMPORARY TABLESPACE TEMP TEMPFILE ‘/opt/oracle/oradata/eygle/temp01.dbf’
SIZE 40M REUSE AUTOEXTEND ON NEXT 640K MAXSIZE UNLIMITED
UNDO TABLESPACE “UNDOTBS1” DATAFILE ‘/opt/oracle/oradata/eygle/undotbs01.dbf’
SIZE 200M REUSE AUTOEXTEND ON NEXT 5120K MAXSIZE UNLIMITED
CHARACTER SET ZHS16GBK NATIONAL CHARACTER SET AL16UTF16
LOGFILE GROUP 1 (’/opt/oracle/oradata/eygle/redo01.log’) SIZE 10240K,
GROUP 2 (’/opt/oracle/oradata/eygle/redo02.log’) SIZE 10240K,
GROUP 3 (’/opt/oracle/oradata/eygle/redo03.log’) SIZE 10240K;
exit;

在这个创建过程中,Oracle会隐含的调用$ORACLE_HOME/rdbms/admin/SQL.bsq脚本,用于创建数据字典。这个文件的位置受到一个隐含的初始化参数 (_init_SQL_file )的控制。
SQL> @GetParDescrb.SQL
Enter value for par: init_SQL
NAME VALUE DESCRIB


_init_SQL_file ?/rdbms/admin/SQL.bsq File containing SQL statements to execute upon database creation

如果在创建过程中,Oracle无法找到SQL.bsq文件,则数据库创建将会出错。我们可以测试一下移除SQL.bsq文件,再看这样一个数据库创建过程。
SQL> startup nomount;
SQL> @CreateDB.SQL
CREATE DATABASE eygle
*
ERROR at line 1:
ORA-01092: ORACLE instance terminated. Disconnection forced

此时日志中会记录以下信息。
Fri Aug 18 15:45:49 2006
Errors in file /opt/oracle/admin/eygle/udump/eygle_ora_3632.trc:
ORA-01501: CREATE DATABASE failed
ORA-01526: error in opening file ‘?/rdbms/admin/SQL.bsq’
ORA-07391: sftopn: fopen error, unable to open text file.
Error 1526 happened during db open, shutting down database
USER: terminating instance due to error 1526

这就是SQL.bsq文件在数据库创建过程中的作用。那么在数据库的引导过程中,又该如何去定位bootstrap$的位置呢?
这就不得不提到了SYSTEM表空间了。在系统表空间文件头存在一个重要的数据结构root dba,我们可以通过转储数据文件头获得这个信息,从生成的trace文件中,我们可以获得以下信息(Oracle 12c环境信息摘录)。
V10 STYLE FILE HEADER:
Compatibility Vsn = 202375680=0xc100200
Db ID=2903506423=0xad0ffdf7, Db Name=‘PRODCDB’
Activation ID=0=0x0
Control Seq=60695=0xed17, File size=103680=0x19500
File Number=1, Blksiz=8192, File Type=3 DATA
Tablespace #0 - SYSTEM rel_fn:1
Creation at scn: 0x0000.00000007 07/07/2014 05:38:57
Backup taken at scn: 0x0000.00000000 01/01/1988 00:00:00 thread:0
reset logs count:0x35f7cd7a scn: 0x0000.0018531f
prev reset logs count:0x32cc9b67 scn: 0x0000.00000001
recovered at 03/02/2016 12:59:30
status:0x2004 root dba:0x00400208 chkpt cnt: 941 ctl cnt:940

root dba仅在SYSTEM表空间的文件头存在,用于定位数据库引导的bootstrap$信息。Root dba存储的是用16进制表示的二进制数,其中包含10位的文件号以及22位的数据块号,将0x00400208转换为二进制就是0000 0000 0100 0000 0000 0010 0000 1000,前10位为1,代表文件号为1,后22位转换为10进制为520,代表数据文件1上的520号数据块。
当然在数据库中无须如此复杂,Oracle提供工具用于数据块及文件号的转换。
SQL> variable file# number
SQL> execute :file#:=dbms_utility.data_block_address_file(to_number(‘400208’,‘xxxxxxx’));
PL/SQL procedure successfully completed.
SQL> variable block# number
SQL> execute :block#:=dbms_utility.data_block_address_block(to_number(‘400208’,‘xxxxxxx’))
PL/SQL procedure successfully completed.
SQL>print file#
FILE#

     1
复制

SQL> print block#
BLOCK#

   520
复制

现在可以全面的来回顾一下数据库的内部引导过程,通过10046事件可以跟踪一下数据库的打开过程,使用前面曾经提到过的步骤。
oracle@enmocoredb admin]$ SQLplus / as sysdba
SQL*Plus: Release 12.2.0.0.3 Production on Thu Aug 4 15:26:49 2016
Copyright © 1982, 2016, Oracle. All rights reserved.
SQL> shutdown immediate;
SQL> startup mount;
Database mounted.
SQL> alter session set events=‘10046 trace name context forever,level 12’;
SQL> alter database open;
SQL> shutdown immediate;

从跟踪文件(以下跟踪文件来自Oracle 12.2版本)中我们可以获得以下重要信息。

PARSING IN CURSOR #0x7fdf6c93ae70 len=19 dep=0 uid=0 oct=35 lid=0 tim=2699657623431 hv=1907384048 ad=‘0x61ce6470’ SQLid=‘a01hp0psv0rrh’
alter database open
END OF STMT
PARSE #0x7fdf6c93ae70:c=2999,e=2961,p=0,cr=0,cu=0,mis=1,r=0,dep=0,og=1,plh=0,tim=2699657623426
WAIT #0x7fdf6c93ae70: nam=‘db file sequential read’ ela= 12 file#=1 block#=520 blocks=1 obj#=-1 tim=2699658571220

PARSING IN CURSOR #0x7fdf6c938d48 len=188 dep=1 uid=0 oct=1 lid=0 tim=2699658571988 hv=4006182593 ad=‘0x61c4bd28’ SQLid=‘32r4f1brckzq1’
create table bootstrap$ (
line# number not null,
obj# number not null,
SQL_text varchar2(4000) not null)
storage (initial 50K objno 59 extents (file 1 block 520))
END OF STMT
PARSE #0x7fdf6c938d48:c=0,e=658,p=0,cr=0,cu=0,mis=1,r=0,dep=1,og=4,plh=0,tim=2699658571988
WAIT #0x7fdf6c938d48: nam=‘PGA memory operation’ ela= 13 p1=65536 p2=2 p3=0 obj#=-1 tim=2699658572091
EXEC #0x7fdf6c938d48:c=0,e=215,p=0,cr=0,cu=0,mis=0,r=0,dep=1,og=4,plh=0,tim=2699658572285
CLOSE #0x7fdf6c938d48:c=0,e=3,dep=1,type=0,tim=2699658572352

PARSING IN CURSOR #0x7fdf6c938d48 len=65 dep=1 uid=0 oct=3 lid=0 tim=2699658572771 hv=1762642493 ad=‘0x61c4a500’ SQLid=‘aps3qh1nhzkjx’
select line#, SQL_text from bootstrap$ where obj# not in (:1, :2)
END OF STMT
PARSE #0x7fdf6c938d48:c=0,e=404,p=0,cr=0,cu=0,mis=1,r=0,dep=1,og=4,plh=0,tim=2699658572770
BINDS #0x7fdf6c938d48:

Bind#0
oacdty=02 mxl=22(22) mxlc=00 mal=00 scl=00 pre=00
oacflg=08 fl2=1000001 frm=00 csi=00 siz=24 off=0
kxsbbbfp=7fdf6c938900 bln=22 avl=02 flg=05
value=59
Bind#1
oacdty=02 mxl=22(22) mxlc=00 mal=00 scl=00 pre=00
oacflg=08 fl2=1000001 frm=00 csi=00 siz=24 off=0
kxsbbbfp=7fdf6c9388d0 bln=24 avl=06 flg=05
value=4294967295
EXEC #0x7fdf6c938d48:c=1000,e=817,p=0,cr=0,cu=0,mis=1,r=0,dep=1,og=4,plh=867914364,tim=2699658573677
WAIT #0x7fdf6c938d48: nam=‘db file sequential read’ ela= 9 file#=1 block#=520 blocks=1 obj#=59 tim=2699658573738
WAIT #0x7fdf6c938d48: nam=‘PGA memory operation’ ela= 9 p1=65536 p2=1 p3=0 obj#=59 tim=2699658573827
WAIT #0x7fdf6c938d48: nam=‘db file scattered read’ ela= 19 file#=1 block#=521 blocks=3 obj#=59 tim=2699658573931

从等待事件上可以明确看到,单块读读取了文件1的第520个数据块,这也正是引导块的定位过程。
WAIT #0x7fdf6c93ae70: nam=‘db file sequential read’ ela= 12 file#=1 block#=520 blocks=1 obj#=-1 tim=2699658571220

了解了SYSTEM表空间的重要作用,也就可以理解,为什么系统表空间的文件头损坏,或者如果启动对象的数据块损坏后,Oracle数据库就将无法启动。
我们曾经见过很多案例,很多用户的数据库运行在非归档模式下,又没有备份,最后当SYSTEM表空间出现故障后,数据库就无法打开了,这是最为严重的情况,通常是没有办法恢复数据的。
所以我们经常反复建议,SYSTEM表空间极其重要,备份重于一切,希望通过我们的不断呼吁,数据库的安全能够更加引起重视,用户的数据能够更加安全。
数据库的引导过程还可以通过GDB工具在Linux、UNIX上进行跟踪,分步骤来观察这个启动过程,以下输出可以帮助读者进一步了解这些内部操作。
首先将数据库启动到Mount状态,找到进程SPID。
SQL> startup mount;
ORACLE instance started.
Database mounted.
SQL> select spid from vprocesswhereaddrin(selectpaddrfromvprocess where addr in (select paddr from vsession where sid=(select distinct sid from v$mystat));
SPID

1518

然后通过gdb跟踪这个进程。
localhost:~ oracle$ gdb $ORACLE_HOME/bin/oracle 1518
GNU gdb 6.3.50-20050815 (Apple version gdb-1518) (Sat Feb 12 02:52:12 UTC 2011)
Attaching to program: `/oracle/product/10.2.0/bin/oracle’, process 1518.
Reading symbols for shared libraries .+++++++++++ done
0x00007fff80616984 in read ()
(gdb)

然后跟踪两个内部指令。
(gdb) break kcrf_commit_force
Breakpoint 1 at 0x1025a2d4c
(gdb) break kqlobjlod
Breakpoint 2 at 0x1006c78b4

此时执行数据库OPEN操作会被挂起。
SQL> alter database open;

然后重新开启一个SQL*Plus进程,查询此时数据库加载的ROWCACHE对象。
SQL> select parameter,count,gets from v$rowcache where count!=0;

no rows selected

然后继续执行,我们看到在第三个步骤之后,数据库加载了一个ROW Cache对象。
(gdb) c
Continuing.

Breakpoint 1, 0x00000001025a2d4c in kcrf_commit_force ()
(gdb) c
Continuing.

Breakpoint 1, 0x00000001025a2d4c in kcrf_commit_force ()
(gdb) c
Continuing.

Breakpoint 2, 0x00000001006c78b4 in kqlobjlod ()

SQL> select parameter,count,gets from v$rowcache where count!=0;
PARAMETER COUNT GETS


dc_objects 1 1

这个对象是什么呢?
SQL> select address,cache_name,existent,lock_mode,saddr,substr(key,1,40) keystr from v$rowcache_parent;

ADDRESS CACHE_NAME E LOCK_MODE SADDR KEYSTR


00000001942E9080 dc_objects N 3 0000000194782EB0 000000000A00424F4F5453545241502400000000

解析其KEY值,正是bootstrap,这就是数据库初始化时加载的第一个对象。 SQL> select dump('BOOTSTRAP’,16) from dual;
DUMP(‘BOOTSTRAP$’,16)

Typ=96 Len=10: 42,4f,4f,54,53,54,52,41,50,24

然后数据库将递归查询该对象中的数据,向内存中加载其他对象。更进一步。
(gdb) c
Continuing.
Breakpoint 2, 0x00000001006c78b4 in kqlobjlod ()

ADDRESS CACHE_NAME E LOCK_MODE SADDR KEYSTR


00000001942E30D8 dc_tablespaces N 0 00 0000000000000000000000000000000000000000
00000001942DE9B8 dc_rollback_segments Y 0 00 0000000000000000000000000000000000000000
00000001942E9080 dc_objects Y 0 00 000000000A00424F4F5453545241502400000000
00000001942DE2D0 dc_objects N 3 0000000194782EB0 000000000600435F4F424A230000000000000000
00000001942E3340 dc_object_ids Y 0 00 3800000000000000000000000000000000000000

这里可以看到数据库加载了回滚段信息,首先加载的是SYSTEM的回滚段,转储Row Cache信息之后,就可以看到这些详细的内容。
SQL> ALTER SESSION SET EVENTS ‘immediate trace name row_cache level 10’;

这里得到的BUCKET 37包含了回滚段信息。
BUCKET 37:
row cache parent object: address=0x1942de9b8 cid=3(dc_rollback_segments)
hash=5fed2a24 typ=9 transaction=0x0 flags=000000a6
own=0x1942dea88[0x1942dea88,0x1942dea88] wat=0x1942dea98[0x1942dea98,0x1942dea98] mode=N
status=VALID/INSERT/-/FIXED/-/-/-/-/-
data=
00000000 00000000 00000001 00000009 59530006 4d455453 00000000 00000000
00000000 00000000 00000000 00000000 00000003 00000000 00000000 00000000
00000000 00000000 00000000 00000000
BUCKET 37 total object count=1
ROW CACHE HASH TABLE: cid=4 ht=0x192b2e8a8 size=256

其中53595354454d正是SYSTEM回滚段。
SQL> select dump(‘SYSTEM’,16) from dual;
DUMP(‘SYSTEM’,16)

Typ=96 Len=6: 53,59,53,54,45,4d

而另外一个BUCKET上正是BOOTSTRAP$对象。
BUCKET 43170:
row cache parent object: address=0x1942e9080 cid=8(dc_objects)
hash=f3d1a8a1 typ=11 transaction=0x0 flags=000000a6
own=0x1942e9150[0x1942e9150,0x1942e9150] wat=0x1942e9160[0x1942e9160,0x1942e9160] mode=N
status=VALID/INSERT/-/FIXED/-/-/-/-/-
set=0, complete=TRUE
data=
00000000 4f42000a 5453544f 24504152 00000000 00000000 00000000 00000000
00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000038 00000001 00000038 066f7802 030d1411
11066f78 78030d14 1411066f 0001030d 00000000 00000000 00000000 00000000
00000000 00000000
BUCKET 43170 total object count=1

这就是数据库启动过程中,BOOTSTRAP的加载与引导过程。由上面的讨论我们可以知道bootstrap表的重要,如果bootstrap$表发生损坏,数据库将无法启动。
4.3.2 远程支持之10046事件
通过10046的跟踪可以快速获得完整的后台信息,在远程支持时尤其有效。以下一个案例来自朋友发来的案例请求,他们的优化工具在客户的环境中执行某个SQL查询时,需要10分钟时间才能出结果,这是无法接受的,而同样的查询在其他环境上都可以快速的获得输出结果。
首先我请朋友获得了一个10046跟踪文件,通过tkprof格式化之后,这个SQL的输出结果展现出来。一段时间消耗代码如下所示。

该段SQL的Elapsed时间超过了600秒,逻辑读也非常高,对于一个优化工具来说显然是不可接受的。
接下来的跟踪文件中显示了SQL的执行计划(仅保留了核心部分)。
Rows Row Source Operation


1935557 VIEW
1935557 UNION-ALL PARTITION
1935557 FILTER
1935557 NESTED LOOPS
2863 TABLE ACCESS BY INDEX ROWID USER2863INDEXUNIQUESCANIUSER1(objectid44)1935557TABLEACCESSFULLOBJ 2863 INDEX UNIQUE SCAN I_USER1 (object id 44) 1935557 TABLE ACCESS FULL OBJ

以上执行计划中,最可疑的部分是对于OBJ$的全表扫描,这个环节的行数返回有1、935、557行。通过NL执行,首先怀疑这里的执行计划选择错误,如果选择索引,执行性能肯定会有极大的不同。
可是10046的跟踪事件显示的信息有限不能够准确定位错误的原因,我请朋友通过10053事件来生成一个执行计划的跟踪,10053使用极为简便,通过如下方式就可以捕获SQL的解析过程。
alter session set events ‘10053 trace name context forever,level 1’;
explain plan for <select_query>;

举例如下所示。
SQL> alter session set events ‘10053 trace name context forever,level 1’;
Session altered.

SQL> explain plan for select count(*) from obj$;
Explained.

然后在udump目录下就可以找到10053生成的跟踪文件,在该文件中,找到了查询相关表的统计信息,其中OBJ的信息如下所示,其中CDN(CarDiNality)指表中包含的记录数量,此处显示OBJ表中有24万左右的记录,使用了2941个数据块。


Table stats Table: OBJ$ Alias: SYS_ALIAS_1
TOTAL :: CDN: 245313 NBLKS: 2941 AVG_ROW_LEN: 79
Column: OWNER# Col#: 3 Table: OBJ$ Alias: SYS_ALIAS_1
NDV: 221 NULLS: 0 DENS: 4.5249e-03 LO: 0 HI: 259
NO HISTOGRAM: #BKT: 1 #VAL: 2
– Index stats
INDEX NAME: I_OBJ1 COL#: 1
TOTAL :: LVLS: 1 #LB: 632 #DK: 245313 LB/K: 1 DB/K: 1 CLUF: 4184
INDEX NAME: I_OBJ2 COL#: 3 4 5 12 13 6
TOTAL :: LVLS: 2 #LB: 1904 #DK: 245313 LB/K: 1 DB/K: 1 CLUF: 180286
INDEX NAME: I_OBJ3 COL#: 15
TOTAL :: LVLS: 1 #LB: 19 #DK: 2007 LB/K: 1 DB/K: 1 CLUF: 340
_OPTIMIZER_PERCENT_PARALLEL = 0


而在前面的10046跟踪信息中,显示OBJ包含大约200万条记录,这是非常巨大的不同,对于USER表,显示具有2863条记录,而统计信息中显示仅有253条记录。


Table stats Table: USER$ Alias: U
TOTAL :: CDN: 253 NBLKS: 16 AVG_ROW_LEN: 82
Column: USER# Col#: 1 Table: USER$ Alias: U
NDV: 253 NULLS: 0 DENS: 3.9526e-03 LO: 0 HI: 261
NO HISTOGRAM: #BKT: 1 #VAL: 2
– Index stats
INDEX NAME: I_USER# COL#: 1
TOTAL :: LVLS: 0 #LB: 1 #DK: 258 LB/K: 1 DB/K: 1 CLUF: 13
INDEX NAME: I_USER1 COL#: 2
TOTAL :: LVLS: 0 #LB: 1 #DK: 253 LB/K: 1 DB/K: 1 CLUF: 87


这说明数据字典中记录的统计信息与真实情况不符合,导致了SQL选择了错误的执行计划,在使用CBO的Oracle9i数据库中,这种情况极为普遍,通过删除表的统计信息,或者重新收集正确的统计信息,可以使SQL执行恢复到正常合理的范畴内。在Oracle10g开始的自动统计信息收集,就是为了防止出现统计信息陈旧的现象。
以下是通过dbms_stats包清除和重新收集表的统计信息的简单参考。
SQL> exec dbms_stats.delete_table_stats(user,‘OBJ);PL/SQLproceduresuccessfullycompleted.SQL>execdbmsstats.gathertablestats(user,OBJ'); PL/SQL procedure successfully completed. SQL> exec dbms_stats.gather_table_stats(user,'OBJ’);
PL/SQL procedure successfully completed.

基于这样的判断我们建议客户做出修正,最后的客户反馈结果是:按照你的建议,在更新OBJ的统计信息后,该语句的执行时间由10分钟减少到了2分钟。接下来,按照类似的思路,又更新了USER,CON,CDEF表的统计信息,这时,语句的执行时间减少到了零点几秒。至此,问题解决。
4.3.3 通过10046事件跟踪解决未知问题
掌握了Oracle数据库最为重要的跟踪方法,就可以在遇到问题时,快速定位根源。而找到问题根源,距离解决问题也就不远了——不论这些问题是已知的还是未知的。
以下一个案例来自于Oracle Database 12.2的版本,在数据库启动时遇到错误,数据库无法启动,抛出的异常是ORA-00600 908错误,关于这个错误在MOS上也没有说明,这属于最早发现的12.2的问题。
[oracle@enmocoredb ~]$ SQLplus / as sysdba
SQL*Plus: Release 12.2.0.0.3 Production on Mon Aug 1 09:59:54 2016
Copyright © 1982, 2016, Oracle. All rights reserved.
Connected to an idle instance.

SQL> startup
ORACLE instance started.
Database mounted.
ORA-00603: ORACLE server session terminated by fatal error
ORA-01092: ORACLE instance terminated. Disconnection forced
ORA-00604: error occurred at recursive SQL level 1
ORA-00600: internal error code, arguments: [908], [], [], [], [], [], [], [], [], [], [], []
ORA-07445: exception encountered: core dump [ksupdbsesinc()+866] [SIGSEGV]
[ADDR:0x13650] [PC:0x6170C62] [Address not mapped to object] []
Process ID: 24446
Session ID: 30 Serial number: 42586

进一步的检查告警日志文件,获取后台记录的更详细信息,在这个案例中后台的异常日志和前台抛出的一致,包括跟踪日志,很难获得更明确的判断线索。
2016-08-01T10:02:56.913024+08:00
Errors in file /u01/app/oracle/diag/rdbms/eygle/eygle/trace/eygle_ora_24446.trc:
ORA-00604: error occurred at recursive SQL level 1
ORA-00600: internal error code, arguments: [908], [], [], [], [], [], [], [], [], [], [], []
ORA-07445: exception encountered: core dump [ksupdbsesinc()+866] [SIGSEGV] [ADDR:0x13650] [PC:0x6170C62] [Address not mapped to object] []
Error 604 happened during db open, shutting down database

为了进一步分析这个问题,我们在Open数据库的阶段启用跟踪,以寻找最后出现问题的步骤,这一方法在实践中非常有效。
SQL> startup mount;
ORACLE instance started.
Database mounted.
SQL> alter session set events ‘10046 trace name context forever,level 12’;
Session altered.

SQL> alter database open;
ORA-00600: internal error code, arguments: [908], [], [], [], [], [], [], [],[], [], [], []

现在可以在后台找到这个跟踪文件,通过定位最后出现问题的部分,获得线索。以下一段输出是数据库启动报错前最后执行的一段递归SQL。

PARSING IN CURSOR #0x7f19251432d8 len=123 dep=1 uid=0 oct=3 lid=0 tim=2423484984490 hv=1601912009 ad=‘0x6181e130’ SQLid=‘65m6cgpgrqg69’
select /*+ NO_PARALLEL© */ c.con_id# from cdb_service$ c where lower(c.name) = lower(:1) or lower(c.name) = lower(:2)
END OF STMT
PARSE #0x7f19251432d8:c=0,e=293,p=0,cr=0,cu=0,mis=1,r=0,dep=1,og=4,plh=0,tim=2423484984490
BINDS #0x7f19251432d8:

Bind#0
oacdty=01 mxl=32(04) mxlc=00 mal=00 scl=00 pre=00
oacflg=18 fl2=0001 frm=01 csi=852 siz=32 off=0
kxsbbbfp=7f19266bdf70 bln=32 avl=04 flg=05
value=“enmo”
Bind#1
oacdty=01 mxl=32(04) mxlc=00 mal=00 scl=00 pre=00
oacflg=18 fl2=0001 frm=01 csi=852 siz=32 off=0
kxsbbbfp=7f19266bdf38 bln=32 avl=04 flg=05
value=“enmo”
EXEC #0x7f19251432d8:c=1000,e=594,p=0,cr=0,cu=0,mis=1,r=0,dep=1,og=4,plh=2885951592,tim=2423484985147
FETCH #0x7f19251432d8:c=0,e=24,p=0,cr=3,cu=0,mis=0,r=1,dep=1,og=4,plh=2885951592,tim=2423484985192
STAT #0x7f19251432d8 id=1 cnt=1 pid=0 pos=1 obj=434 op=‘TABLE ACCESS FULL CDB_SERVICE$ (cr=3 pr=0 pw=0 str=1 time=23 us cost=2 size=16 card=1)’
CLOSE #0x7f19251432d8:c=0,e=41,dep=1,type=0,tim=2423484985251
kswscrs: error service is already defined in pdb 3.
: current pdb id=1

注意最后出现的提示:kswscrs: error service is already defined in pdb 3,current pdb id=1。这个提示给了我们一个重要线索,错误出现的原因是服务名已经在PDB 3中被定义和使用,因此出现了冲突。
再来看看最后执行的这个递归SQL,该SQL从cdb_service表来取得服务名,进行验证。 select /*+ NO_PARALLEL(c) */ c.con_id# from cdb_service c where lower(c.name) = lower(:1) or lower(c.name) = lower(:2)

两个绑定变量的输入参数是:value=“enmo” 。
找到了这样一个方向,进一步的检查数据库参数,发现在初始化参数中的确设置了一个服务名enmo,根据错误应该是这个服务名和PDB自动注册产生了冲突。
SQL> startup mount;
ORACLE instance started.
Database mounted.
SQL> show parameter service
NAME TYPE VALUE


service_names string yhem,eygle,enmo

接下来尝试去掉这个服务名,重新启动数据库,数据库成功启动。
SQL> alter system set service_names=‘yhem,eygle’;
System altered.
SQL> shutdown immediate;
ORA-01109: database not open
Database dismounted.
ORACLE instance shut down.
SQL> startup
ORACLE instance started.
Database mounted.
Database opened.
SQL> show parameter service
NAME TYPE VALUE


service_names string yhem,eygle

在熟悉了Oracle的跟踪方法之后,就可以据此不断深入理解Oracle数据库的工作原理,在实践中应对各种异常,并直指根源,找到解决方案。
在12c的多租户环境,修改service_names参数应该非常谨慎,对于该参数的修改会直接反映到监听器的动态注册,甚至会覆盖PDB的动态注册,影响服务。以下过程展示这一知识点,在运维工作中尤其应当引起大家的注意和关注。
测试环境的CDB中存在两个PDB,分别是ENMO和YHEM2,同时service_names参数中存在两个服务名设定,分别是 yhem 和 eygle,这样数据库存在了四个服务名。
SQL> col name for a30
SQL> select con_id,dbid,name from v$pdbs;
CON_ID DBID NAME


  2  612507346 PDB$SEED
  3  965292808 ENMO
  4 2839503056 YHEM2
复制

SQL> show parameter service_names
NAME TYPE VALUE


service_names string yhem,eygle

在监听器中的注册的服务名如下表征(去除了不必要内容)。
[oracle@enmocoredb ~]$ lsnrctl status
LSNRCTL for Linux: Version 12.2.0.0.3 - Production on 03-AUG-2016 11:08:46
Copyright © 1991, 2016, Oracle. All rights reserved.

Connecting to (ADDRESS=(PROTOCOL=tcp)(HOST=)(PORT=1521))
STATUS of the LISTENER

Services Summary…
Service “enmo” has 1 instance(s).
Instance “eygle”, status READY, has 1 handler(s) for this service…
Service “eygle” has 1 instance(s).
Instance “eygle”, status READY, has 1 handler(s) for this service…
Service “yhem” has 1 instance(s).
Instance “eygle”, status READY, has 1 handler(s) for this service…
Service “yhem2” has 1 instance(s).
Instance “eygle”, status READY, has 1 handler(s) for this service…
The command completed successfully

如果通过如下方式修改了参数,去除部分服务名。
SQL> alter system set service_names=‘yhem’;
System altered.

此时的监听状态会随之改变,包括PDB自动注册在内的服务名被一起清除。
[oracle@enmocoredb ~]$ lsnrctl status
LSNRCTL for Linux: Version 12.2.0.0.3 - Production on 03-AUG-2016 11:10:33
Copyright © 1991, 2016, Oracle. All rights reserved.
Services Summary…
Service “eygle” has 1 instance(s).
Instance “eygle”, status READY, has 1 handler(s) for this service…
Service “yhem” has 1 instance(s).
Instance “eygle”, status READY, has 1 handler(s) for this service…
The command completed successfully

如果遇到这种状况,这意味着所有通过服务名访问PDB的请求都会无法获取连接。解决这个问题的方法是,将PDB的服务名重新通过service_names参数进行显示的设置,就可以临时恢复这个问题。
SQL> alter system set service_names=‘yhem,yhem2,enmo’;
SQL> !lsnrctl status
LSNRCTL for Linux: Version 12.2.0.0.3 - Production on 03-AUG-2016 15:19:49
Copyright © 1991, 2016, Oracle. All rights reserved.

Connecting to (ADDRESS=(PROTOCOL=tcp)(HOST=)(PORT=1521))
Services Summary…
Service “enmo” has 1 instance(s).
Instance “eygle”, status READY, has 1 handler(s) for this service…
Service “eygle” has 1 instance(s).
Instance “eygle”, status READY, has 1 handler(s) for this service…
Service “yhem” has 1 instance(s).
Instance “eygle”, status READY, has 1 handler(s) for this service…
Service “yhem2” has 1 instance(s).
Instance “eygle”, status READY, has 1 handler(s) for this service…
The command completed successfully

但是注意,在12.2的已知问题未作为Bug修复之前,如果带着这个service_names参数设置重启数据库就会遇到前文所说的问题。
4.3.4 通过10046解决数据库RAC集群案例
在某次客户案例中,遇到了数据库无法启动的一则案例,根据现场工程师的反馈,获得了启动时的10046跟踪。在跟踪文件中,末端呈现的是大量的TT锁等待(这是一个10.2.0.4版本的数据库)。
WAIT #2: nam=‘enq: TT - contention’ ela= 488293 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1 tim=6632936391
WAIT #2: nam=‘enq: TT - contention’ ela= 488294 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1 tim=6633424737
WAIT #2: nam=‘enq: TT - contention’ ela= 488292 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1 tim=6633913072
WAIT #2: nam=‘enq: TT - contention’ ela= 488292 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1 tim=6634401394
WAIT #2: nam=‘enq: TT - contention’ ela= 488292 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1 tim=6634889716
WAIT #2: nam=‘enq: TT - contention’ ela= 488293 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1 tim=6635378039
WAIT #2: nam=‘enq: TT - contention’ ela= 488292 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1 tim=6635866366
找到最后执行的递归SQL,从这一点开始分析,主要信息显示如下。

PARSING IN CURSOR #3 len=348 dep=1 uid=0 oct=3 lid=0 tim=5645840078 hv=2512561537 ad=‘b5f8e850’
select name,intcol#,segcol#,type#,length,nvl(precision#,0),decode(type#,2,nvl(scale,-127/MAXSB1MINAL/), 178,scale,179,scale,180,scale,181,scale,182,scale,183,scale,231,scale,0),null,fixedstorage,nvl(deflength,0),default,fixedstorage,nvl(deflength,0),default,rowid,col#,property, nvl(charsetid,0),nvl(charsetform,0),spare1,spare2,nvl(spare3,0) from col$ where obj#=:1 order by intcol#
END OF STMT
EXEC #3:c=0,e=118,p=0,cr=0,cu=0,mis=0,r=0,dep=1,og=4,tim=5645840075
FETCH #3:c=0,e=52,p=0,cr=3,cu=0,mis=0,r=1,dep=1,og=4,tim=5645840215
STAT #3 id=1 cnt=130 pid=0 pos=1 obj=0 op=‘SORT ORDER BY (cr=35 pr=0 pw=0 time=732 us)’
STAT #3 id=2 cnt=130 pid=1 pos=1 obj=21 op=‘TABLE ACCESS CLUSTER COL$ (cr=35 pr=0 pw=0 time=330 us)’
STAT #3 id=3 cnt=11 pid=2 pos=1 obj=3 op=‘INDEX UNIQUE SCAN I_OBJ# (cr=22 pr=0 pw=0 time=102 us)’
WAIT #2: nam=‘DFS lock handle’ ela= 450 type|mode=1413545989 id1=4 id2=0 obj#=-1 tim=5645841105
WAIT #2: nam=‘DFS lock handle’ ela= 445 type|mode=1413545989 id1=2 id2=0 obj#=-1 tim=5645841592
WAIT #2: nam=‘enq: US - contention’ ela= 443 name|mode=1431502854 undo segment #=0 0=0 obj#=-1 tim=5645842085
WAIT #2: nam=‘gc current block 2-way’ ela= 1420 p1=1 p2=9 p3=16777231 obj#=-1 tim=5645843634
WAIT #2: nam=‘gc current grant busy’ ela= 516 p1=1 p2=9 p3=33619983 obj#=-1 tim=5645844244
WAIT #2: nam=‘rdbms ipc reply’ ela= 637 from_process=17 timeout=900 p3=0 obj#=-1 tim=5645844928
WAIT #2: nam=‘enq: TT - contention’ ela= 488657 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1 tim=5646333647
WAIT #2: nam=‘enq: TT - contention’ ela= 488293 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1 tim=5646821976

等待事件是分析的起点,而真正能够理解等待事件后面隐藏的深意是DBA不断成长的重要标志。
第一个需要分析清楚的是DFS lock handle,该事件出现了两次。
WAIT #2: nam=‘DFS lock handle’ ela= 450 type|mode=1413545989 id1=4 id2=0 obj#=-1 tim=5645841105
WAIT #2: nam=‘DFS lock handle’ ela= 445 type|mode=1413545989 id1=2 id2=0 obj#=-1 tim=5645841592

DFS 是 Distributed File System 的缩写,最早在Oracle并行服务器(OPS)时代引入,指分布式集群文件系统,但是今天这个DFS涵盖的意义更加广泛,这里代表着需要获取一个全局锁。之所以加上Handle,是因为很多锁的获取是分为两个步骤,首先获得lock handle上的锁定。这里的DFS给我们的提示是,这是一个RAC环境,而事实上,另外一个节点还在正常运行,当前故障节点无法启动。
由于Oracle的锁类型很多,这个事件的一个参数是type|mode,通过这个参数合并编码了锁类型和请求模式。可以通过SQL将这两个元素解码出来。
select chr(bitand(&&p1,-16777216)/16777215) ||
chr(bitand(&&p1,16711680)/65535) type, mod(&&p1, 16) md
from dual;
TY MD


TA 5

查询得到的结论是TA锁,请求模式5。如果不熟悉TA这个锁类型,可以通过数据字典查询一下。
SQL> exec print_table(‘select * from v$lock_type where type=’‘TA’’’);
TYPE : TA
NAME : Instance Undo
ID1_TAG : operation
ID2_TAG : undo segment # / other
IS_USER : NO
IS_RECYCLE : NO
DESCRIPTION : Serializes operations on undo segments and undo tablespaces
CON_ID : 0

以上print_table引用了一个Tom的脚本,其内容如下所示。
create or replace procedure print_table( p_query in varchar2 )
AUTHID CURRENT_USER
is
l_theCursor integer default dbms_SQL.open_cursor;
l_columnValue varchar2(4000);
l_status integer;
l_descTbl dbms_SQL.desc_tab;
l_colCnt number;
begin
execute immediate
‘alter session set
nls_date_format=’‘dd-mon-yyyy hh24:mi:ss’’ ';

dbms_SQL.parse(  l_theCursor,  p_query, dbms_SQL.native );
dbms_SQL.describe_columns
( l_theCursor, l_colCnt, l_descTbl );

for i in 1 .. l_colCnt loop
    dbms_SQL.define_column
    (l_theCursor, i, l_columnValue, 4000);
end loop;

l_status := dbms_SQL.execute(l_theCursor);

while ( dbms_SQL.fetch_rows(l_theCursor) > 0 ) loop
    for i in 1 .. l_colCnt loop
        dbms_SQL.column_value
        ( l_theCursor, i, l_columnValue );
        dbms_output.put_line
        ( rpad( l_descTbl(i).col_name, 30 )
          || ': ' || 
          l_columnValue );
    end loop;
    dbms_output.put_line( '-----------------' );
end loop;
execute immediate
    'alter session set nls_date_format=''dd-MON-rr'' ';
复制

exception
when others then
execute immediate
‘alter session set nls_date_format=’‘dd-MON-rr’’ ';
raise;
end;
/

我们看到TA锁是Instance Undo——实例重做锁,其描述表明是和Undo段、Undo表空间相关的串行操作。
进一步锁定信息在下一个事件体现出来,成为显性的US锁等待。
WAIT #2: nam=‘enq: US - contention’ ela= 443 name|mode=1431502854 undo segment #=0 0=0 obj#=-1 tim=5645842085

这里的name|mode转换出来,请求的是模式6——X的排它锁。
select chr(bitand(&&p1,-16777216)/16777215) ||
chr(bitand(&&p1,16711680)/65535) type,
mod(&&p1, 16) md from dual dual;
Enter value for p1: 1431502854
TY MD


US 6

再进一步,Oracle通过GC Current读向远程请求了文件1的第9个Block,但是在远程实例中这个请求没有被授予,呈现Busy的状态,也就是排它锁不能被获取。
WAIT #2: nam=‘gc current block 2-way’ ela= 1420 p1=1 p2=9 p3=16777231 obj#=-1 tim=5645843634
WAIT #2: nam=‘gc current grant busy’ ela= 516 p1=1 p2=9 p3=33619983 obj#=-1 tim=5645844244

再向下,数据库以TT锁等待处于无尽循环等待,数据库无法启动。
WAIT #2: nam=‘enq: TT - contention’ ela= 488657 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1
WAIT #2: nam=‘enq: TT - contention’ ela= 488293 name|mode=1414791174 tablespace ID=0 operation=0 obj#=-1

那么TT锁是什么?通过以下查询,可以看到这同样是串行表空间操作的DDL锁。
SQL> set serveroutput on
SQL> exec print_table(‘select * from v$lock_type where type=’‘TT’’’);
TYPE : TT
NAME : Tablespace
ID1_TAG : tablespace ID
ID2_TAG : operation
IS_USER : NO
IS_RECYCLE : NO
DESCRIPTION : Serializes DDL operations on tablespaces
CON_ID : 0

整个问题的脉络梳理清楚了,还剩下一个问题,为什么在数据库的启动过程中要在回滚段上获取串行DDL排他锁?文件1的Block 9给我们了一个线索,存活节点不能给予的锁定块到底是什么?
SQL> exec print_table(‘select segment_name,segment_type from dba_extents where file_id=1 and block_id=9’);
SEGMENT_NAME : SYSTEM
SEGMENT_TYPE : ROLLBACK

我们看到了输出结果,系统回滚段,那么数据库启动过程为何要在SYSTEM回滚段上获取排他锁呢?
回顾BOOTSTRAP$中的对象,数据库初始化中顺序执行的第一个语句就是CREATE ROLLBACK SEGMENT SYSTEM STORAGE,也就是分配系统回滚段。
CREATE ROLLBACK SEGMENT SYSTEM STORAGE
( INITIAL 112K NEXT 1024K MINEXTENTS 1 MAXEXTENTS 32765 OBJNO 0 EXTENTS
(FILE 1 BLOCK 9))

正是因为这样一个DDL操作需要获得排他锁,数据库在启动过程中才需要向存活实例发出锁请求,无法获得就只有等待。
这里需要注意,系统回滚段从11g开始,向后偏移至第128个Block。
CREATE ROLLBACK SEGMENT SYSTEM STORAGE ( INITIAL 112K NEXT 56K MINEXTENTS 1 MAXEXTENTS 32765 OBJNO 0 EXTENTS (FILE 1 BLOCK 128))

那么对方实例为什么无法给予授权?那一定是因为对方节点同样存在锁定。我们从存活实例上查询VLOCK分析其当前锁定,发现同样存在大量的TT锁等待,而连接时间最长的会话CTIME已经长达8066215秒。 SQL> select * from gvlock where type=‘TT’;
INST_ID ADDR KADDR SID TY ID1ID2 LMODE REQUEST CTIME BLOCK


     1 07000002D8867018 07000002D8867038 2112 TT    0  0      0       4      12725     0
     1 07000002D88643B0 07000002D88643D0 2115 TT     0  0      0       4      13055     0
     1 07000002D88707D0 07000002D88707F0 2116 TT   0  0      4       0    8066215    2
     1 07000002D8864020 07000002D8864040 2116 TT     0  0      0       4      13385     0
     1 07000002D88644E0 07000002D8864500 2124 TT    0  0      0       4      10415     0
     1 07000002D88670B0 07000002D88670D0 2134 TT      0  0      0       4      11735     0
     1 07000002D88674D8 07000002D88674F8 2145 TT      0  0      0       4      10745     0
     1 07000002D8868BB0 07000002D8868BD0 2155 TT   0  0      4       0    8314633    2
     1 07000002D88673A8 07000002D88673C8 2155 TT   0  0      0       4      11405     0
复制

注意这两个长时间的锁定,其BLOCK类型为2,这在官方文档上有明确的解释(文档v$lock视图部分),这样的进程阻塞了RAC环境远程的操作。
2 - The lock is not blocking any blocked processes on the local node, but it may or may not be blocking processes on remote nodes. This value is used only in Oracle Real Application Clusters (Oracle RAC) configurations (not in single instance configurations).

那么这些进程到底在执行什么操作?通过V$SESSION.SQL_ID发现这些SQL正在执行表空间的剩余空间计算工作。
SELECT a.ts_name,
bytes/data_sum,
data_sum,
free_sum
FROM
(SELECT tablespace_name ts_name,
SUM(NVL(blocks,0)) data_sum,
SUM(NVL(bytes,0)) bytes
FROM dba_data_files
GROUP BY tablespace_name
)a,
(SELECT tablespace_name ts_name,
SUM(NVL(blocks,0)) free_sum
FROM user_free_space
GROUP BY tablespace_name
)b
WHERE a.ts_name=b.ts_name
ORDER BY a.ts_name

通过手工Kill终止两个阻塞进程之后,数据库节点可以成功启动。通过MOS确认这是一个Bug,如果对于DBA_FREE_SPACE的查询未正常结束可能导致锁定无法释放,产生TT队列竞争,其影响范围甚至包括11.2.0.4版本,如图4-1所示。

图4-1
其实DBA_FREE_SPACE视图的结构在不同版本之间变化较大,尤其是回收站引入之后。查询DBA_FREE_SPACE可能极慢并且可能触发问题,在监控表空间变化时,应当注意这些问题。
4.4 Oracle跟踪总结
在我的职业生涯中,每当我接触到一项技术之后,总是希望能够了解隐藏在软件之后的细节。所以我总会迫切地去寻找更多的软件跟踪方法,而一旦掌握了这些方法之后,就能够持续地帮助我去发现原理、解决问题。这样的经验一直伴随着我成长和前行。
这篇文章中,总结的案例跨越了我至今的整个职业历程,涉及的数据库版本从Oracle9i到今天的Oracle 12.2。也正因为这些方法的持续赋能,我更愿意将这作为我最宝贵的经验分享给读者,希望大家能够掌握方法,从中获取长期受益的经验。
4.5 数据类型比较规则
本章内容多个案例涉及了Oracle的数据比较和隐式转换,因此我们在此翻译了官方文档中的一段文字,作为Oracle数据比较的基础知识,供读者参考。
4.5.1 数值类型
数值类型的值的大小跟其数字本身大小的比较是一致的,数字较大的值也较大。负数小于0和所有正数,比如,−1小于100,−100小于−1。
浮点型的数值(不是数字或不能以数字表示)大于任何其他的数值类型的值,并等于它本身。
4.5.2 日期类型
一般认为日期类型的值,出现越晚,值越大。例如,2005年3月29日下午1:35对应的日期类型的值小于2006年1月5日对应的日期类型的值,但大于2005年1月5日早上10:09对应的日期类型的值。
4.5.3 字符类型
字符类型的值的比较有以下两种规则。
(1)基于二进制或语言学排序。
(2)基于填充空格或不填充空格的语义比较。
接下来将会详细介绍这两种规则。
1.二进制或语言学排序
二进制比较是数据库的默认比较规则。数据库字符集中的每一个字符都有一个特定的值,对于任何一个字符类型的值,会通过比较该值中每一个字符在字符集中对应的编码值的和来决定它的大小。和越大,则该字符类型的值越大。Oracle认为空格小于任何字符,这个结论适用于大部分的字符集。
下面是比较常见的一些字符集。
7-bit ASCII(American Standard Code for Information Interchange)。
EBCDIC Code(Extended Binary Coded Decimal Interchange Code)。
ISO 8859/1(International Organization for Standardization)。
JEUC Japan Extended UNIX。
当你所比较的字符类型对应的数值编码的二进制序列跟字符产生的语法序列不一致的时候,就需要考虑使用语法比较。但语法比较需要满足两个条件,一是NLS_SORT参数没有被设置为BINARY,二是NLS_COMP参数的值被设为LINGUISTIC。使用语法比较的时候,所有的SQL排序和比较都会根据NLS_SORT参数指定的语法规则来进行。
2.基于填充空格或不填充空格的语义比较
(1)基于填充空格的语义比较。如果两个字符类型的值长度不相等,Oracle会首先在较短的值后面添加空格,使二者长度相等。然后通过比较两个字符类型的值从左到右第一个不同的字符的编码值,编码值大的我们就认为该字符类型的值比较大。如果两个值不存在不同字符,则认为二者相等。也就是说,如果两个字符类型的值的区别仅在于末尾空格的数目,我们认为这两个值相等。
这种基于空格填充的比较规则仅在以下条件下使用:要比较的值既不是CHAR或者NCHAR类型的表达式,也不是文本或者用户函数返回的结果。
(2)基于不填充空格的语义比较。Oracle通过比较两个字符类型的值的左边起首个不同字母对应的编码值的大小决定大小。编码值越大的,我们认为该字符类型的值也比较大。当两个字符类型的值的长度不等时,我们认为长度较长的值也较大。只有两个字符类型的值长度相等,并且对应位置字符也相等时,我们才认为这两个值是相等的。当要比较的两个值中的一个或者两个都是VARCHAR2或者NVARCHAR2类型时,就会用到该比较规则。
使用不同的比较规则比较两个字符类型的值,结果可能不相同。表4-2列举了五组字符类型的值分别基于这两种规则时的比较结果。
表4-2
Blank-Padded Nonpadded
‘ac’ > ‘ab’ ‘ac’ > ‘ab’
‘ab’ > 'a ’ ‘ab’ > 'a ’
‘ab’ > ‘a’ ‘ab’ > ‘a’
‘ab’ = ‘ab’ ‘ab’ = ‘ab’
'a ’ = ‘a’ 'a ’ = ‘a’

表4-3中列举了ASCII包含的字符集,表4-4列举了EBCDIC包含的字符集。区分大小写。对于某些语言,部分字符集中的字符的数值可能并不与其语法的顺序相吻合。
表4-3
Symbol Decimal value Symbol Decimal value
Blank 32 ; 59
! 33 < 60
" 34 = 61

35 > 62

$ 36 ? 63
% 37 @ 64
& 38 A-Z 65-90
’ 39 [ 91
( 40 \ 92
) 41 ] 93

  • 42 ^ 94
  • 43 _ 95
    , 44 ’ 96
  • 45 a-z 97-122
    . 46 { 123
    / 47 | 124
    0-9 48-59 } 125
    : 58 ~ 126
    表4-4
    Symbol Decimal value Symbol Decimal value
    Blank 64 % 108
    c 74 _ 109
    . 75 > 110
    < 76 ? 111
    ( 77 : 122
  • 78 # 123
    | 79 @ 124
    & 80 ’ 125
    ! 90 = 126
    $ 91 " 127
  • 92 a-I 129-137
    ) 93 j-r 145-153
    ; 94 s-z 162-169
    ӱ 95 A-I 193-201
  • 96 J-R 209-217
    / 97 S-Z 226-233

4.5.4 对象类型
对于对象类型的值的比较,通常会采用以下两种函数:MAP和ORDER函数。这两个函数都可以基于对象类型作比较,但二者有很大区别。使用这两种函数必须要在对象的类型中指定要用来比较的方法。
4.5.5 数组和嵌套表类型
1.数据类型的优先级
Oracle利用数据类型的优先级来决定数据类型的隐式转换。接下来我们会详细描述。Oracle的数据类型具有以下的优先级顺序。
(1)日期类型和间隔日期类型。
(2)双精度二进制类型。
(3)浮点二进制类型。
(4)数值类型。
(5)字符类型。
(6)其他内置数据类型。
2.数据转换
通常情况下,在一个表达式中不可能同时包含两种数据类型。比如在一个表达式中,不能同时计算5*10+‘JAMES’,但Oracle支持使用隐式转换或者显式转换的方式将一个类型转换成另一个类型,然后进行相关运算。
3.关于数据类型的隐式转换和显式转换
Oracle建议多使用显式转换而不要太多依赖于隐式转换,主要有以下几点原因。
(1)使用显式转换可以让SQL语句更容易理解。
(2)隐式转换会对性能造成一些影响,尤其是当我们需要把一列值通过隐式转换转为常量的时候。
(3)隐式转换可能会依赖环境,并不一定会起作用。例如,使用隐式转换的方式将一个日期类型的值转换为VARCHAR2 类型,受到NLS_DATE_FORMAT参数的影响,并不一定能返回正确的结果。
(4)实现隐式转换的算法会因在软件版本和数据库产品的不同而导致结果有差异,容易出错。而显式转换的结果会比较稳定。
(5)当索引表达式上出现隐式转换时,这时Oracle数据库会将索引类型定义为“转换前的类型”,因而导致索引不再可用。这会对性能产生很不好的影响。
4.隐式数据转换
在Oracle数据库中,面对类型不一致的情况,只要某种转换方式行得通,Oracle会自动将一个数据类型转换成另外一种。
表4-5列出了Oracle数据库中的隐式转化,包含了所有可转换的情况。
表4-5
CHAR VAR
CHA
R2 NCH
AR NVA
RCH
AR2 DATE DATE
TIME
/INTE
RVAL NUM
BER BINA
RY_F
LOAT BINA
RY_D
OUB
LE LO
NG RAW ROW
ID CL
OB BL
OB NCL
OB
CHAR – × × × × × × × × × × – × × ×
VARCHAR2 × – × × × × × × × × × × × – ×
NCHAR × × – × × × × × × × × × × – ×
NVAR
CHAR2 × × × – × × × × × × × × × – ×
DATE × × × × – -- – -- – -- – -- – -- –
DATETIME/INTERVAL × × × × – -- – -- – × – -- – -- –
NUM
BER × × × × – -- – × × – -- – -- – --
BINARY_F
LOAT × × × × – -- × – × – -- – -- – --
BINARY_DOUBLE × × × × – -- × × – -- – -- – -- –
LONG × × × × – XFoot
1 – -- – -- × – × – ×
RAW × × × × – -- – -- – × – -- – × –
ROWID – × × × – -- – -- – -- – -- – -- –
CLOB × × × × – -- – -- – × – -- – -- ×
BLOB – -- – -- – -- – -- – -- × – -- – --
NCLOB × × × × – -- – -- – × – -- × – --

注意,不能直接将long类型的值转换为间隔日期类型的,但可以使用TO_CHAR(interval)
将long类型转化为VARCHAR2 类型。
以下是使用隐式转换的一些规则。
(1)在使用insert和update操作时,Oracle会对受影响的列进行类型转换。
(2)在select from操作中,Oracle会将from子句中的类型转换为目标类型。
(3)在数值类型的值的先关操作中,Oracle会首先基于最多符合原则调整数值的精度。在这种情况下,对于数值类型的值的操作结果可能会跟以下我们将会提到的操作结果不一致。
(4)若要比较字符类型和数值型的值,Oracle会将字符型转换成数值类型。
(5)将字符类型或者Number类型的值与浮点类型进行转换可能会产生误差,因为字符类型和Number类型使用十进制的精度表达数值大小,而浮点型使用的是二进制的精度。
(6)如果将一个CLOB的值转换为字符类型,比如VARCHAR2类型,或者将BLOB的值转换为RAW类型,如果源类型下的值大于转换后的值,则会返回错误。
(7)如果将时间戳类型转换为日期类型,分时和秒时的部分会被直接摒弃掉,而在Oracle数据库的早期版本,则会将这些部分近似处理。
(8)将二进制的浮点类型转换为二进制的双精度类型不会产生误差
(9)将双精度二进制类型转换为浮点型二进制类型的结果可能不准确,会产生误差。因为双精度比浮点型多使用一位,要将多出来的这一位删掉,肯定会产生误差。
(10)当比较字符型和日期类型的值,Oracle会将字符型转换为日期类型。
(11)当使用带有参数的SQL函数或者操作的时候,参数对应的数据类型不能被识别,Oracle会将参数的类型转换为能够被识别的类型。
(12)在进行运算的时候,Oracle会把等号右边的类型转换为跟等号左边一样的类型。
(13)在转换中,Oracle会将非字符类型的值转换为CHAR或者NCHAR。
(14)在算数运算中,为比较字符类型和非字符类型的大小,Oracle会根据实际情况将任何字符类型的转换为数值,日期或者rowid等。如果算数运算中包含char/nchar类型与nchar/nvarchar2类型,Oracle会将这些类型都转换为Number类型再进行运算。
(15)大部分的SQL函数能够识别和接受CLOB类型的参数,并且会在CLOB类型和字符类型之间做隐式转换。因此,在一些不识别CLOB类型或者不能对CLOB类型的值做隐式转换的函数中,调用前Oracle会先尝试进行转换。如果CLOB类型的字段大于4000字节,则Oracle默认智能转换前面的4000字节,也就是说,当CLOB字段过大的时候,可能会导致错误产生。
(16)当进行字符类型和RAW类型或者LONG RAW类型的相互转换的时候,二进制类型的值会被转换为十六进制来表示,用一个十六进制的字符代表RAW类型的四位。
(17)CHAR和VARCHAR2类型的转换或者NCHAR和NVARCHAR2类型的比较可能需要不同的字符集。默认的转换方向是将当前的数据库字符集转换为国际字符集。表4-6描述了不同字符类型的隐式转换方向。
表4-6
to CHAR To VARCHAR2 to NCHAR to NVARCHAR2
from CHAR – VARCHAR2 NCHAR NVARCHAR2
from VARCHAR2 VARCHAR2 – VARCHAR2 NVARCHAR2
from NCHAR NCHAR NCHAR – NVARCHAR2
from NVARCHAR2 NVARCHAR2 NVARCHAR2 NVARCHAR2 –

对于一些未定义类型的值,是不能进行隐式转换的。必须使用CAST … MULTISET语法进行显式转换。
5.隐式数据类型转换实例
(1)文本类型隐式转换
文本类型值“10”属于CHAR类型,当它出现在数学表达式中的时候,Oracle会将它隐式转换为Number类型。
SELECT salary + ‘10’ FROM employees;

(2)字符和数字类型隐式转换
当比较数字类型和字符类型值的大小时,oracle会隐式将字符类型转换为数字类型,在下面的例子中,oracle将’200’转换为200。
SELECT last_name FROM employees WHERE employee_id = ‘200’;

(3)日期类型隐式转换
在下面的语句中,Oracle将‘24-JUN-06’隐式转换为日期类型的默认格式’DD-MON-YY’
SELECT last_name FROM employees WHERE hire_date = ‘24-JUN-06’;

6.显式数据转换
你可以使用SQL的转换函数显式地转换数据类型。表4-7列举了将一个类型转换为其他类型可以使用的一些函数。
在Oracle不能进行隐式转换的情况下,不能指定RAW类型或者LONG RAW类型。例如LONG类型和LONG RAW类型不能出现在包含函数或者操作符的表达式中。
表4-7
to
CHAR,VAR
CHAR2,NC
HAR,NVAR
CHAR2 to
NUMBER
to
Datetime/
lnterval to
RAW to
ROWID to
LONG,
LONG RAW to CLOB,
NCLOB,
BLOB to
BINARY_
FLOAT to
BINARY_
DOUBLE
from
CHAR,
VARCHAR2,
NCHAR,NVARCHAR2 TO_CHAR
(char.)

TO_NCHAR
(char.) TO_NUMBER TO_DATE

TO_TIMESTAMP

TO_TIMEST
AMP_TZ

TO_YMINTERVAL

TO_DSINTERVAL HEXTORAW CHARTO-=ROWID – TO_CLOB

TO_NCLOB TO_BINARY_FLOAT TO_BINARY_BOUBLE
From
NUMBER TO_CHAR
(number)

TO_NCHAR(number) – TO_DATE

NUMTOYM-
INTERVAL

NUMTODS-
INTERVAL – -- – -- TO_BINARY_FLOAT TO_BINARY_DOUBLE
from
Datetime/Interval TO_CHAR
(date)

TO_NCHAR(datetime) – -- – -- – -- – --
from RAW RAWTOHEX

RAWTONHEX – -- – -- – TO_BLOB – --
from ROWID ROWIDTOCHAR – -- – -- – -- – --
from LONG/ LONGRAW – -- – -- – -- TO_LOB – --
from CLOB,NCLOB,BLOB TO_CHAR

TO_NCHAR – -- – -- – TO_CLOB

TO_NCLOB – --
From CLOB,NCLOB,BLOB TO_CHAR

TO_NCHAR – -- – -- – TO_CLOB

TO_NCLOB – --
from BINARY_FLOAT TO_CHAR
(char.) TO_NUMBER – -- – -- – TO_BINARY_FLOAT TO_BINARY_DOUBLE
from BINARY_DOUBLE TO_CHAR
(char.)

TO_NCHAR
(char.) TO_NUMBER – -- – -- – TO_BINARY_FLOAT TO_BINARY_DOUBLE
7.数据类型转换中的安全考虑
当一个日期类型的值被转换为文本的时候,不管是用隐式转换的方式还是显式转换的方式,如果没有指定格式规范,这个格式由部分全局会话中的参数决定,这些参数包括NLS_DATE_FORMAT、NLS_TIMESTAMP_FORMAT或者NLS_TIMESTAMP_TZ_FORMAT等,而这些参数都可以通过客户端或者命令行做修改。
在显式转换中,要是没有明确指定转换格式(比如日期类型的值通过动态SQL语句执行后被应用),会对数据库的安全造成该很大影响。 所谓动态SQL语句指的是其语句在提交给数据库执行前,是一些碎片。动态SQL语句频繁地与内置PL/SQL包或者DBMS_SQL中的PL/SQL语句关联并快速执行,但这仅仅是动态组织的SQL语句被当做参数传递的其中一个例子(在该例子中start_date属于日期类型)。
EXECUTE IMMEDIATE
‘SELECT last_name FROM employees WHERE hire_date > ‘’’ || start_date || ‘’’’;

上述的例子中,start_date的值根据会话级别的NLS_DATE_FORMAT参数的格式被隐式转换成文本类型。日期类型被转换成文本格式但使用双引号引起来。这样,任何用户都可以通过设置全局的参数格式来决定日期类型的值转换后的格式。如果SQL语句是以PL/SQL的存储过程执行,这些存储过程的执行可能会受到这些会话参数的影响。如果这些过程以定义者的角色执行,拥有比会话高一点的权限,就可以获得对未授权的敏感数据的访问,造成不安全。
这里需要注意,这些安全风险不仅存在于SQL执行的时候。事实上,在中间层的应用上,在利用OCI日期函数将日期类型转换为文本类型后重构SQL语句的过程中也存在安全风险。如果一个会话的全局参数被用户获得,会对应用产生很大的影响。
数值类型的隐式转换和显式转换还可能受到同类问题的影响。由于依赖于如NLS_ NUMERIC_CHARACTERS等定义格式和分隔符的参数, 如果将分隔符定义为单引号或者双引号,一些基本的SQL的输入都会被合并。

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

评论

小虎牙
暂无图片
4年前
评论
暂无图片 0
非常实用,😊
4年前
暂无图片 点赞
评论
red_hope
暂无图片
4年前
评论
暂无图片 0
文章好长,读了2个小时,还是要实践一下
4年前
暂无图片 点赞
评论