收获,不止Oracle(第2版)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 物理体系从老余开店慢慢铺开

2.2.1 老余的三个小故事

5分钟过去了,“大家都记下这幅体系物理结构图了吗?”梁老师微笑着问。

“记下了!”台下异口同声。

“好吧,让我给大家讲个故事吧。”

台下的同学聚精会神。

2.2.1.1 顾客的尺寸

“话说有一个开成人服装店的老余,生意经营得还不错,他的店铺有一个特点,就是主要是熟客频繁光顾他的店铺,并且每次买的量还不少。这段时间放暑假,老余的儿子小余也来服装店帮忙,他的任务除了帮忙在门口招呼招呼客人外,还帮客人量腿长和身长,好给客人匹配合适尺寸的衣裤。

“过了几天,小余问老余:‘老爸,我怎么老给客人量尺寸啊,他们自己都记不住吗,不会直接报给我吗?’老余一听,笑着说:‘我们这里的客人大多都不去记自己的尺寸,等你帮他们量好了,他们也不会去问具体尺码,就等着你提供给他们合适尺寸的衣裤来试穿,都这样啊。’

“‘老爸,那我可以把他们的尺码记录下来啊,下次他们来了,我只要一查就知道尺寸了,能省不少时间,还不会出错。’昨天早上小余没留神在量的过程中把尺寸给看错了,结果让客户试了一次很不合身才意识到,如果记录了客人某次试穿合身的衣裤的尺寸,下次直接用这个尺寸就可以避免昨天早上的失误了,小余为此事耿耿于怀,因为昨天为此事没少给客人道歉。

“‘不错啊,小家伙,我怎么没想到呢?’老余觉得是好主意,他想了想,又问,‘你准备记录在哪儿,本子上吗?’

“‘那不行,本子里查询多不方便啊,说不定还不如我量一下快,我们可以录入在电脑甚至智能手机里啊。老爸,不知你听过没,有种软件叫数据库,录入和查询都很方便。到时我们直接输入客户的名字,就能得到对应的尺寸信息,此外还可以把该尺寸和具体衣裤的小、中、大、超大号对应起来,那就更方便了。’

“‘成!就依你的主意,小家伙。’

“在随后的一段日子里,不少熟客感觉到了变化,他们很少被要求量尺寸,经常在选好款式后就直接拿到衣裤开始试穿了,尺码每次都还非常匹配。这个变化让顾客更加满意了,因此店里的生意更好了。老余开心极了,不只是因为生意更好开心,发现儿子越来越能干才是真开心。

“好了,故事说完了,好听吗?”梁老师笑眯眯地问大家。

“好听!”

“再来一个!”

台下同学们情绪非常高涨,也无拘无束。

“还要再听啊,那好吧。”

“耶!”台下爆发出欢呼声。

2.2.1.2 有效的调整

“话说老余的店铺规模还真是不小,各种衣裤五花八门、琳琅满目,有时真能让人挑花了眼,这也说明老余生意是真的不错,要不哪有资金搞这么大的规模呢?

“最近小余发现一个现象,店铺顾客是不少,生意也还不错,可是很多顾客逛了好长时间,才买到自己称心如意的服装,还有的就是转悠了好久,最后没买走人了。其实这种情况一直就存在,只是小余刚刚留意到。

“如果顾客能很快就挑到称心如意的服装,那对大家来说可真是皆大欢喜啊,小余开始观察并思考这个问题。

“小余开始研究被买走的服装的类别,终于领悟到了什么叫潮流,近期被买得最多的款式最畅销!他明白他要怎么做了。

“‘老爸,我有个想法。’

“‘哦,又有什么好主意啊?乖儿子。’

“‘晚上打烊时我们加加班,把最前排货柜的服装全取下来,弄出点空位子来,然后我要挑一些特定款式的服装摆到这最前排的货柜上。’

“‘儿子,为什么要这么弄啊?’

“‘老爸,你没注意到最近大家都买什么款式的服装吗?我们要注意潮流,发现规律,把这些被买得最多的系列款式放到最显眼的地方去,我们店铺这么大,选服装岂不是让人选得头晕眼花,其实现在很多人都是冲着这一系列款式来的,你没看出来吗?’

“出于对儿子的信任,老余立即答应了。晚上他们忙得热火朝天,第二天,店铺有了新变化,店铺最前排的货柜被特别装修了一番,并且上面赫然写着‘店主推荐’四个大字。

“在随后的一段日子里,老余惊喜地发现生意变得更好了。而小余也悄悄地做了统计,发现每位客人在店里逗留的时间也短了很多,大多在店主推荐的货柜上快速找到自己喜欢的,就迅速离去了。不过小余和老余也没少做功课,他们不断地维护着这个店主推荐的货柜。陆续有不少服装从店主推荐货柜转到大厅里,也有不少从大厅中转入店主推荐货柜里,他们的依据很简单,就是哪些款式买的人最多,哪些就留在店主推荐货柜里,很少被买的就转到大厅里。而大厅内也不是没有人买服装,他们观察大厅内的服装购买情况,哪个款式卖得最多,这个款式的服装很快就会被转到店主推荐货柜里。

“后续还有新的改良,小余发现有一群顾客其实是冲着便宜来的,于是小余专门选了一个和店主推荐货柜差不多显眼的前排货柜,也特别装修了一番,写上‘超值优惠’四个大字。然后将成本比较低、卖价比较便宜的服装专门摆放在此处,这下方便了原来在大厅里到处寻觅相对便宜衣服的顾客,他们往往一进店门,就直奔超值优惠货柜,很快就心满意足地离去了……

“好了,第二个故事也说完了,好听吗?”梁老师一脸微笑地问大家。

“好听!”

“有意思!”

“原来梁老师是教我们怎么做生意啊!”

“再来一个!”

台下七嘴八舌依旧无拘无束,不过他们听讲的时候,却是非常认真。

“还要再讲故事啊,你们怎么这么贪心!俗话说,事不过三,那我就再讲一个吧。”

“耶!”台下又爆发出欢呼声。

2.2.1.3 记录的习惯

“老余的服装生意越做越红火,老余夫人余太太在学校附近新开的餐饮店却磕磕碰碰遇到些闹心的事,怎么说呢?

“原来这里不少学生由于平日里没支配好父母给的生活费,买了零食和玩具,导致早餐及午餐时口袋里钱不够,于是不少孩子就赊账,有的时候记得第二天还了,有时第二天就没给补上,这样余太太就吃亏了,有时余太太明明记得孩子有欠钱,但是她也明白个中原因,也就算了。

“最糟糕的是,居然有部分孩子由于经常赊账未兑现,最后弄得自己也不好意思,居然干脆不去余太太店里就餐,早上连中午一起饿肚子,这对孩子的生长影响多不好啊!

“此外余太太还发现了另外一种情况,就是由于孩子胡乱花费父母给的费用后,导致手里钱不够,早餐和晚餐就开始减少食量,菜饭都减半,余太太看着这些还在长身体的孩子,急在心里。

“你说余太太闹心不闹心,不仅是烦恼没赚钱,善良的她更烦恼这些孩子的健康问题。

“小余听了妈妈的烦恼后,想到了后付费的方式,他让妈妈在店门口贴了一张告示,内容是允许孩子们就餐时暂缓付钱,到月底后,餐厅会让孩子将当月开销总费用明细带回家给父母确认,由父母通过网银或给孩子现金带来支付给餐厅。

“接下来,余太太还让就餐的孩子将事先准备好的协议单带回去给父母,里面首先描述了近期存在的孩子就餐中的不良情况,接下来描述了后付费的具体细节,最后还有填写联系方式及签字确认的相关内容。

“情况的发展果然非常顺利,孩子吃完饭只需要在记有自己消费时间和金额的单子上签个字就可以离开了,之前的所有烦恼再也不存在了。孩子父母也更放心了,每月的结算也非常爽快。餐厅的顾客越来越多,越来越固定,生意越做越红火。

“另外,余太太餐厅的工作效率也大大提升了。为什么呢?原来余太太面对经常赊账的情况还要经常想办法追讨,现在不用操心这个了,等到月底,等着孩子的父母通过网银转账就好了。

“余太太在这个餐厅的特色经营过程中,养成了记录的习惯,并且成了她生活中的习惯,认为有用的事情她都会尽量记录下来,这个习惯也帮了老余一个大忙。前段时间大暴雨导致城市内涝严重,老余赶紧手忙脚乱地雇人把店里所有服装转移出来,等灾害过后,服装要重新搬回店里时,如何摆设成了难题。原先有店主推荐货柜和超值优惠货柜,大厅的摆设也有一定的分布规律。不过余太太在灾害来临尚未搬运期间对服装店的摆放做了详细的记录,还多角度拍了不少照片。这下老余小余父子俩就少了很多琢磨的时间,在很短时间内就部署摆设好了,又开始了正常的营业,没有耽搁太多的时间。

“好了,第三个故事也说完了,故事到此结束了。”梁老师接着对大家说,“听完这三个故事,你们有什么感想?”

“要善于动脑筋。”

“要注意工作效率。”

“小余很聪明。”

“小余有生意头脑。”

……

台下发言倒是非常踊跃,梁老师不回答,他在等着某个提问。

“梁老师,您说的三个故事和体系结构有什么关系啊?”

哈哈,终于等到了,梁老师笑了。

最后一个同学的发言让大家都安静了下来,是啊,大家也都开始思考这个问题,三个故事和体系结构有什么关系,是如何结合在一起的呢?

2.2.2 体系结构原理初探

2.2.2.1 从一普通查询SQL语句说起

“同学们,关于故事与体系物理结构的联系我们暂且放一放,现在我们开始描述 Oracle 的体系结构,你们现在有没有忘记刚才介绍过的体系结构图啊?”

“记得!”对于这些年轻的新人来说,记性好是他们最大的优势之一,5分钟时间强记这个看上去不是非常复杂的结构图,大多数人都做得到。

“很好,这里我先问大家一个问题,大家在校园和最近的实习过程中,有接触过 SQL 语句的同学请举手。”

台下绝大部分同学举手了。

“我发现不少人没接触过Oracle数据库,但是标准SQL倒是大多数人都接触过。”梁老师笑着说道。

其实梁老师心里明白这是什么原因,这是因为大学里很少会开设具体的数据库课程(如Oracle、DB2、SQL Server、Informix),即便Oracle占据了70%以上的市场份额也不例外。标准SQL由于较为通用,所以大学中一般会开设这样的课程,此外,数据库基础这样的课程(不和具体数据库挂钩的),很多大学中也会开设。因此很多人没接触过 Oracle,但大多数人接触过SQL这一现象并不奇怪。

“很好!大家都有一定的SQL基础知识,我就以一些SQL语句为例,来和大家谈谈Oracle体系结构。

“大家先来看一条最简单的SQL查询语句,select object_name from t where object_id=29;,当你发出这条SQL语句后,该SQL语句从1区先做准备工作,如图2-3所示。

“前面我向大家描述过PGA不同于SGA,它是仅供当前发起用户使用的私有内存空间,这个区域的具体作用有三点,忘记的同学打一下屁股,然后回头看看课件记录。这里该连接只是完成了两点,即用户连接信息的保存和权限的保存,别小看这个保存的重要性,只要该会话不断开连接,下次系统不用再去硬盘中读取数据,而直接从PGA内存区中获取。

图2-3 PGA区说明

“此外,该SQL指令还会立即匹配成一条唯一的hash值,接下来该SQL指令进入2区进行处理,首先敲开SGA区的共享池的大门,准备登门拜访,如图2-4所示。

图2-4 共享池说明

“共享池的大门打开了,该SQL指令先在房内查询是否什么地方存储过这个SQL指令的身份证(就是那个唯一的 hash 值),如果没有,那就要辛苦了。首先查询自己的语句语法是否正确(比如from是否写成了form)、语义是否正确(比如id字段根本就不存在)、是否有权限,在这些都没问题的情况下生成这条语句的身份证,唯一的hash值就被存储下来了。接下来开始进行解析,解析什么呢?比如select object_name from t where object_id=29;这条语句,在object_id列有索引的情况下,是用索引读更高效,还是全表扫描更高效呢?Oracle要做出选择。

“Oracle处理这件事情的依据很简单,就是把两种方式都估算一遍,看哪个代价(Cost)更低,就用哪种。好比上街买菜,有的菜摊白菜卖5毛/斤,有的卖6毛/斤,那就选5毛的。不过这里可不是真正地分别执行两次来比较,具体方法在后续涉及优化的学习中大家会了解到。

“假设Oracle认定使用索引代价(Cost)更低,只要5毛钱,于是Oracle就选用了索引读的执行计划而放弃了需要6毛钱的全表扫描方式。接下来这个索引读的执行计划就立即被存储起来,并且和之前存储的该SQL指令的身份证(唯一hash值)对应在一起。

“接下来,该 SQL 指令好比钦差大臣一样,手持‘索引读获取某某数据’这个圣旨,继续往前走,它这是要去哪呢?原来是直奔‘数据缓存区’府内去宣读圣旨了,如图2-5所示。

图2-5 数据缓存区说明

“数据缓存区开门迎接跪谢天恩后,立即要根据ID列上的索引从t表中查找object_id值为29的宝物,但是所要的东西府内找不到,怎么办呢?数据缓存区府只好传令下去,八百里加急赶去偏远的Database军营的数据文件区去查找皇上要的东西(当然,必须用索引读的方式查找,这是圣旨,不可违抗)。如果查到了,就带回数据缓存区府,并由钦差大臣展现给皇上,如果找不到,也只有就此复命,如图2-6所示。

图2-6 数据文件查数据

“至此,一条最普通的SQL指令的执行经历就说完了,大家听明白了吗?有什么疑问吗?”梁老师的脸上始终充满微笑。

“听明白了!”

“梁老师,我还是没体会到您之前说的故事和您描述的体系结构有什么关系?”小莲同学前面的课程记得真牢,有些不依不饶啊。

“梁老师,您说 Oracle 会选择代价最低的一种执行计划,方法是分别尝试一下,比较出高低,但是又不是真正执行,这是怎么做到的啊,准确吗?”

“梁老师,我听是听明白了,不过为什么没说到体系结构图的2区中SGA区的日志缓存区啊?”

“哦,还有还有,3区Database区怎么就能描述一个数据文件啊,其他组件呢?”

“梁老师,您说实例是由一个内存区SGA和一组进程组成的,您刚才怎么也没说进程啊,DBWR、LGWR等,都没说啊?”

“梁老师,这个语句也太简单了吧,复杂的语句也是如此吗?”

……

“很好!老师发现大家上课非常认真,前面的体系结构图也记得非常牢固,思维也非常活跃,老师非常满意!” 梁老师开心地说道,“不过我确确实实是把一条最常见的 SQL 指令在数据库中的前后执行经历描述给大家了,且并没有描述错,大家却有这么多疑问,你们觉得问题出在哪里呢?”

台下安静下来,大家都在思考。

2.2.2.2 老余故事终现用心良苦

“疑问总会得到解决的,刚才某个用户发出那条 SQL 指令的执行描述大家都听明白了吧,之前和大家海阔天空地聊了不少,现在我们就具体做一些试验,让大家有一个直观的感受。

“下面我就来当那个用户,由我来执行这条 select object_name from t where object_id=29;SQL指令。我构造的试验环境如下,所有的人都可以根据我的准备工作完成这个试验,请同学们回去后务必自己动手操作一遍:

“这里我简单介绍一下上述脚本,首先是登录数据库,sqlplus ljb/ljb就是我的连接,接下来是建表构造数据和建索引的过程,随后的set autotrace on 是开始跟踪SQL指令的执行计划和执行的统计信息,而set timing on 是表示跟踪该语句执行完成的时间。这些都准备好了,最后关键语句select object_name from t where object_id=29;就可以粉墨登场了。

“这些构造脚本的试验方法这里先简单提一下,在将来的系列培训中还会提到,准备试验脚本也是我多年的习惯,要善于构造出试验,研究分析事物本质。此外大家尽量在自己的笔记本电脑中安装一下数据库软件,Oracle 11g和Oracle 12c都可以,这样才方便自己随意做试验而不影响他人环境。

“好了,话不多说,现在我们开始执行脚本,让大家直观地体会一下,试验分析过后,大家的不少疑问都会自然而然地消除,大家期待吗?”

“期待!”台下异口同声地回答。

“好,那我们开始吧,我们就简单地先在Windows平台中做操作吧,不管是Windows还是UNIX,数据库的原理都是一样的。另外,大家别忘记在自己的笔记本电脑中安装 Oracle 数据库,然后执行我课上的脚本,来实践上课的内容。

“我们进入cmd命令行,复制粘贴上述脚本,执行结果如下(见脚本2-1):

脚本2-1 SQL语句首次查询的情况

“接下来我们再执行一次select object_name from t where object_id=29;这个SQL指令,参见脚本2-2:

脚本2-2 同一SQL指令再次查询后性能提升

“好了,该 SQL 指令前后分别被执行了两次,大家认真观察前后两次输出的结果,找找看有没有什么差异?记住,这可是一模一样的语句,返回一模一样的结果哦。”

“执行时间第一次更长!”

“统计信息方面两次执行好像差别很大……”

“非常好,大家观察得很仔细!”梁老师满意地说道。

“大家基本上都说到了,下面看我分析贴出比较图,左边显示0.04秒为第1次的执行时间,而右边0.01秒为第2次的完成时间,比第一次快了整3倍,如图2-7所示。

图2-7 比较执行时长

“接下来看统计信息的比较,首次执行产生了52次递归调用、73次逻辑读、4次物理读。而第2次执行递归调用为0,逻辑读为4次,物理读为0,如图2-8所示。

图2-8 比较递归调用与逻辑读

“看来前后两次执行差异非常大啊!同学们,这是为什么呢,真正的原因在于第2次执行实现了同样的目的却少做了不少事。少做了哪些事呢?大家脑海里要再次浮现体系结构图,否则跟不上我讲课的思路哦。

“SQL指令的执行我之前已经描述过了,现在我是专门描述第2次执行的差异。

“第一,用户首次执行该SQL指令时,该指令从磁盘中获取用户连接信息和相关权限信息,并保存在PGA内存里。当用户再次执行该指令时,由于SESSION之前未被断开重连,连接信息和相关权限信息就可以从PGA内存中直接获取,避免了物理读。

“第二,首次执行该SQL指令结束后,SGA内存区的共享池里已经保存了该SQL指令唯一的hash值,并保留了语法语义检查及执行计划等相关解析动作的劳动成果,当再次执行该SQL指令时,由于该 SQL指令的hash值和共享池里保存的相匹配,所以之前的硬解析动作无须再做,不仅跳过了相关语法语义检查,对于该选取哪种执行计划也无须考虑,直接拿来就好。

“第三,首次执行该 SQL 指令时,数据一般不在 SGA 的数据缓存区里(除非被别的 SQL读入内存了),只能从磁盘中获取,不可避免地产生了物理读,但是由于获取后会保存在数据缓存区里,再次执行时可直接从数据缓存区里获取,完全避免了物理读,大家可以注意到我们的试验,首次执行的物理读为4,第2次执行的物理读居然为0,没有物理读,数据全在缓存中,效率当然高得多!

“所以,这就是前后两次试验执行效率差异如此之大的原因,和 Oracle 的体系结构有密切关系,Oracle 的体系结构设计是有玄机的,这种设计保证了再次执行的效率能大大提升,是非常有意义的。

“现在我可以问大家一下,之前我说的故事《顾客的尺寸》《有效的调整》的思考方式,和刚才描述的体系结构相通吗?请大家思考。”

“梁老师,《顾客的尺寸》讲述了小余首次丈量顾客衣服尺寸时将尺寸记录下来,等下次遇到同一顾客时就直接快速查出该顾客的衣服尺寸,从而无须每次重复做这个丈量的动作的故事,这不是和共享池一模一样嘛,缓存避免了再次解析这个耗时的动作。”小莲恍然大悟。

“梁老师,我也来说说。数据缓存区获取数据比磁盘获取数据要快好多,越经常获取的数据往往意味着是越有意义的数据,后续再次查询这些数据的概率比较大,而《有效的调整》中小余把顾客经常购买的服装款式放到最显眼、方便获取的大厅前排货柜里。在整个大厅寻找和在前排货柜寻找的差异类似于数据缓存区查找数据和磁盘查找数据的差异!”晶晶也如梦初醒,她也明白了梁老师讲这个故事的用心。

“看来我的故事没白讲啊。”梁老师这句话显然是肯定了两位同学的回答,台下同学们也纷纷点头,表示赞许。

2.2.2.3 一起体会Oracle代价

“在之前介绍体系物理结构时有不少同学提问,有问体系结构和我说的故事有什么关系,不过这个大家自己搞明白了。还有问Oracle判断哪种执行计划更高效时,是用代价大小来判断的,这基于什么原理,是否准确。这个问题我想在这里和大家一起研究一下。当然还有不少同学有其他疑问,我们留在后续再分析研究。

“判断代价大小基于什么原理我们先暂缓讨论,首先测试一下 Oracle 对代价的判断是否准确,这样多少能让我们对Oracle数据库有些信心。

“我们知道在表有索引的情况下,Oracle可以选择索引读,也可以选择全表扫描,这是两种截然不同的执行计划,不见得一定是索引读胜过全表扫描,有时索引读的效率会比全表扫描更低,所以Oracle的选择不是看执行的是什么计划,而是判断谁的代价更小。

“让我们设计一组试验来证实一下 Oracle 的代价判断是否准确。前面我做过一组试验,证实了执行某SQL指令后,再继续执行同样的SQL指令,性能会大幅度提升。

“现在我还是继续这个例子,同样的t表,同样的SQL语句,但是我要加上一个HINT(什么叫HINT,就是一种强制写法,比如强行让某SQL语句不走索引,或者强行让某SQL语句走某索引)。原来的SQL语句如下:

“现在我增加/*+full(t)*/的写法,来强制该SQL语句不走索引,而走全表扫描,具体如下:

“执行这个语句的目的是,看看 Oracle 选择索引是否是明智正确的选择,是否会更快,逻辑读等是否都少得多。

“准备脚本如下(见脚本2-3),请注意我故意让select /*+full(t)*/object_name from t where object_id=29;执行两遍,是为了多次执行后消除共享池的解析、减少甚至消除物理读以及递归调用:

脚本2-3 故意强制走全表扫描的情况

“我们进入cmd 命令行,复制粘贴上述脚本,执行结果如下,我只截取了第2次SQL指令执行后的部分信息:

“图2-9至图2-11的左边显示的均为HINT强制走全表扫描的SQL指令,右边显示的均为此前Oracle自己选择用索引方式的SQL指令。

图2-9 强制全表扫描比较时长

“比较强制走全表扫描的SQL指令和此前Oracle自己选择用索引方式的SQL指令的执行时间,以及各自执行代价及逻辑读的大小,发现强制走全表扫描明显在性能上要大大逊色于Oracle自己选择的索引方式。

“其中最为重要的如图2-10所示,左边的代价为174,右边的代价为2,显而易见左边的代价高得多,所以Oracle当然会抛弃使用全表扫描的方式来检索数据,转而使用代价小得多的索引读。“由此我们得出结论,Oracle对代价的评估和判断是相当准确的:代价为174的情况下,执行时间需要0.12秒,远高于代价为2的情况下的0.01秒;代价为174的情况下,逻辑读为765,也远高于代价为2的情况下的逻辑读4,如图2-11所示。

图2-10 比较代价

图2-11 比较逻辑读

“这个小试验证明了 Oracle 数据库还是可以信赖的,我们可以充分相信它的选择,只是它的选择比较艰难,开销很大。如果它们选择的结果能保留下来重用,那艰难的动作就可以避免重复操作,性能就能大幅度提升了,Oracle在这方面的设计确实很巧妙。

“同学们,有谁没听明白吗?”梁老师结束了“代价”这个关键概念的介绍,笑着问大家。

“没有!”场下异口同声。

确实,场下的小伙子小姑娘年轻好学、理解力强,梁老师的描述又如此通俗易懂、风格又如此轻松愉快不会让人感到乏味和走神,谁还不能接受呢?

2.2.3 体系结构原理再探

2.2.3.1 从一条普通更新语句说起

“关于体系结构,我已经用很通俗的方式给大家粗略地描述了一遍,从同学们的反应交互来判断,我所描述的大家应该都听懂了,大家确实都非常聪明!

“不过在前面描述体系结构时,大家有关于体系结构图的三个疑惑,老师还没给大家答疑:SGA区的日志缓存区没描述;后台进程的功能都没提及;Database区数据库文件之外的其他组件也没有提及。

“其实这三条疑问我可以汇总成一条,即:在梁老师的描述中,体系结构图中不少组件根本没被提及,难道这些组件是多余的吗?

“请问之前提问的三个同学,我这么汇总对吗?”

“对!”三个提问的同学齐声应到。

“你们知道为什么我当时不回答你们这些疑问吗?那是因为你们的疑问其实是由于梁老师的首次描述太粗略了,如果描述再细致一点,你们就没有疑问了。

“我的风格是循序渐进,先说最简单的、最重要的、最易理解的部分,让大家先理解一部分,然后再略为深入、接下来再深入,直至最后全面深刻理解完毕。而且这个循序渐进的过程我希望伴随着大家的积极思考、提问,我认为这样的课程效果是最好的!”

听到这里,小莲一个劲儿地点头。她感觉这次培训生动活泼、引人入胜的程度,是她之前任何一次培训所没经历过的,而且她也喜欢上了数据库的学习和研究,觉得其乐无穷。

“select object_name from t where object_id=29;语句是我刚才举的例子,还把该SQL语句具体执行了一下,让大家直观地感受了一下,不过不知道大家有没有注意到,这条SQL语句仅仅是一条查询语句,你们觉得除了查询语句,还应该有什么语句?”

“更新语句!”台下回答很响亮。

“是啊,SQL 语句中除了查询语句,还应该有更新语句,否则这个数据库就要变成一个只读数据库了。如果是一个只读库,那组件当然不需要那么多了,这就可以解释你们的疑问了。

“此外,更新又可分为插入、修改、删除三类,这些动作都属于数据库操作中最常见的操作,现在我准备举一个更新语句的例子,来继续描述体系结构。下面我们仅以更新语句update t set object_id=92 where object_id=29;为例。

“回想之前的select object_name from t where object_id=29;语句,大家应该可以很清楚地想象到这个update语句的前后经历:

“首先,如果该用户并没有退出原连接去新建一个连接,PGA 区的用户连接信息和权限判断等诸多动作依然不用重做,否则需要完成用户连接信息和权限判断等诸多动作。

“其次,如果该语句是第一次执行,在共享池里依然需要完成语法语义分析及解析,update t set object_id=92 where object_id=29;指令想匹配到object_id=29的记录既可以用索引读,也可以用全表扫描,到底选用哪种执行计划需要根据代价的大小来选择。

“接下来进入数据缓存区,首次执行该语句时数据一定不在缓存区里,也是和前面一样,先从磁盘中获取到缓存区中……

“以上三点和之前的select object_name from t where object_id=29;描述没有本质区别,差异在于查询语句做完这三步后,给用户返回数据结果,就收工回家休息了。而更新语句的工作却远没结束,还要流着汗水继续工作。

“接下来,更新语句要完成什么动作呢?

“大家看图2-12所示的方框部分,终于涉及之前未描述的部分了,DBWR进程,看来系统后台进程未被提及的历史终于要被改写了!

图2-12 DBWR进程

“其实描述起来特别简单,就是在数据缓存区内修改完数据后,会启用DBWR进程,完成更新的数据从内存中刷入磁盘,将磁盘中的object_id=29的值更新为92。因为磁盘才是真正存储数据的地方,否则一断电,数据在内存中,那就灰飞烟灭了。

“接下来请大家继续看图2-13中我标记出的方框部分,日志缓存区、LGWR和ARCH后台进程及日志文件都出现在议题上了,人物情节越来越丰富,之前未被提及的人物尽数粉墨登场了。

图2-13 写日志

“日志缓存区保存了数据库相关操作的日志,记录了这个动作,然后由 LGWR后台进程将其从日志缓存区这个内存区写进磁盘的日志文件里。目的很简单,就是为了便于将来出现异常情况时,可以根据日志文件中记录的动作,再继续执行一遍,从而保护数据的安全。

“举一个简单的例子,新建好一个数据库,完成如下三个动作:

“一、A动作,建立一张表T。

“二、B动作,往T表中插入一条数据。

“三、C动作,用该数据更新某字段。

“A、B、C三个动作都被记录到日志中了。接下来数据库出现异常,比如T表记录被人误删除了,怎么办?

“很简单,根据日志把B、C动作再执行一遍就行了。

“那如果整张表T都被人删除了,怎么办?

“也很简单,根据日志把A、B、C三个动作再执行一遍就行了。

“由此可见,Oracle中的写日志操作是很重要的,LGWR进程是联系日志缓存区和日志文件的辛勤工作者,请看图2-14,我把日志文件标记上了1、2、3、4。

图2-14 归档切换

“一开始我们写日志文件1,写满后切换到日志文件2继续写,2写满后写3,3写满后写4。当4也写满后,该怎么办呢?”梁老师说到这里突然停下来问大家。

“4写满后再写1啊!”马上有人抢答。

“那1中的数据呢,被覆盖?”梁老师问。

“会不会是先把1的数据备份出去,再覆盖重写?”有同学不敢确定地小声回答。

“完全正确,这个ARCH就是1在被重写时先备份出去的文件,命名为归档文件,接下来再到2、3、4,我们就可以依此类推了。此外,这些ARCH文件也需要定时转移到新的存储介质中,这个存储介质里的ARCH就是将来数据库故障恢复时的法宝了。听明白了吗?”

“听明白了!”大家回答得很响亮。

“那你们将眼睛闭上,回忆一下体系结构图,脑子里分别回想我举过的查询语句和更新语句的例子,体会一下是否感觉亲切多了,也更容易记忆了。”梁老师让大家做一个尝试。

原来数据库的主要组件的功能是这样的,小莲瞬间豁然开朗了!她闭上眼睛,脑子像放电影一样过了一遍查询语句和更新语句在数据库中的执行过程,顿时觉得这个体系结构图一下子就在脑海里展现出来,清晰极了。

2.2.3.2 体系结构中提交的探讨

“大家都理解Oracle体系物理结构的原理了吗?”梁老师问。

“理解了!”同学们回答得很是自信。

“其实大家只是了解了一个大致原理,很多细节还不知道,我还以刚才那条更新语句update t set object_id=92 where object_id=29;为例,如果我执行完这条指令,你们可以在数据库中查询到object_id=29被改变为92了吗?”

“可以!”大部分同学都这么回答。

“不可以,因为没提交。”少数几个同学持反对意见。

“那我自己查数据库,可以查到object_id=29被改变为92了吗?”梁老师继续问。

“也不可以!”

“哦,如果还是发起更新语句的本SESSION做的查询,这倒是可以查到变化的。”梁老师笑着说道,“这个大家后续简单地做一个试验就可以证明了。

“其实这里的机制涉及的细节非常多,说得太细会和我今天上课的预想冲突,后续会有很多机会和大家细细探讨的。当然也不是所有的细节都需要了解,了解实用的、有借鉴意义的细节即可。我还是以我的风格来授课,在引导思索中让大家慢慢地越了解越多,最后贯穿起来灵活应用。

“一条更新语句无论插入、修改还是删除,最终执行完毕后都需要执行用户做提交COMMIT或回滚ROLLBACK的确认。前者表示用户确认无误了,确实需要更新。后者表示用户后悔了,赶紧撤销刚才的动作。

“回滚的场景我后面再描述,先说提交。在此我先问大家一个问题,当发起提交命令后,你们觉得数据缓存区中的数据一定会立即被DBWR进程写进磁盘吗(见图2-15)?”

图2-15 数据缓存区写出机制

台下有的人说会,有的人说不会,七嘴八舌,大家都等着梁老师来确定答案。

“那我举一个例子吧,比如我去银行存自己辛苦赚的10万元血汗钱(之前户头上已经有10万元),柜台工作人员在你签字确认后,收走了你的10万元钱,你就空手回去了。

“而工作人员做的动作无非就是执行一条10万元记录的插入语句或是执行一条从原先的10万元变为20万元的更新语句。由于你签字确认了,工作人员执行了COMMIT的动作;如果你在签字确认前忽然提出不想存了,工作人员无非就是执行一条ROLLBACK语句,10万元钱你带回去就好了。

“刚才有人说 COMMIT 后数据不一定会立即被写进磁盘,换句话说,就是还保留在 SGA的数据缓存区里。

“那接下来我的故事就有些不幸了,当我离开银行时银行忽然断电,可是我的新存10万元存款的记录还在数据缓存区这个内存区域里,断电的后果是内存里存储的任何信息都消失了。

“我在回家的路上心里很不踏实,10万元可是我的血汗钱啊,于是去某个ATM机再去查询看看总金额是否是20万元,结果查询后,发现还是10万元,于是愤怒地返回银行。”

说到这里,梁老师停了下来,问大家:“现在你们觉得提交后数据会立即从数据缓存区写到磁盘吗?”

“会!” 这下大家回答得相当一致。

“为什么?”

“因为提交表示最终确认,如果此时数据依然存放在内存中,断电会导致数据丢失,放磁盘中保险。”小莲回答问题倒是非常快速。

“那我宣布答案,”梁老师笑了笑说道,“所有人都错!”

“不是吧,居然是不会刷入磁盘,那断电了怎么办?刚才梁老师不是提示我们肯定是会刷进磁盘吗?”

“梁老师,我觉得肯定会刷入磁盘,您不要您的10万元血汗钱了啊?”

台下笑声一片。

“原来是心疼梁老师的血汗钱啊,我很感动。”梁老师笑着调侃道,“不过说不会刷入磁盘的那位同学回答也是错的,答案是也许会也许不会,不一定。这里我问大家一个问题,数据缓存区的数据每提交一次就刷出一次和积累到一定量后成批刷出,哪个性能更高?”

“当然是批量做效率更高!”同学们回答得很干脆。

“回答正确!大家还记得之前梁老师说过的《记录的习惯》那个故事吧,余太太可以每天都拿着孩子们的赊账单去找家长兑现钱,也可以选择每月甚至每季度去一次,前者会把余太太累晕,后者则轻松多了,效率孰高孰低自是不言而喻的。

“所以答案是,COMMIT 无法左右数据何时从数据缓存区刷入数据区,Oracle 根据一定的规则来促成这个动作,就是缓存区中的数据积累到一定的程度,再批量刷入磁盘中,因为这样高效得多,大家听明白了吗?”

“听明白了,只是难道Oracle数据库不怕断电吗?内存中的数据最怕断电啊,梁老师。”小莲脱口而出。

“很好,同学们担心的是安全问题,安全和效率很多时候是不能兼顾的,在无法兼得的情况下,我们肯定是牺牲效率换取安全,可是Oracle在这里做到了兼顾,所以才可以批量刷出而不怕断电危机,大家想想Oracle是如何做到的?”

台下一片寂静,暂时没人回应。

“梁老师提醒大家一下,还记得我说的第三个故事《记录的习惯》吧,余太太是每天拿记录本找孩子家长结算效率高还是每个月底或者季度清算一次效率更高?”

“当然是每个月底或季度清算一次效率更高!”同学们回答得毫不含糊。

“那余太太为什么放心每个月底或者是季度和孩子家长结算一次呢?”梁老师继续提问。

“梁老师,因为她记录下来了,有明细单及孩子的签字,之前还与家长签订了合同,家长无从抵赖,所以可以放心啊。” 曾祥毫不迟疑地举手回答了。

“回答得非常好,那Oracle数据库呢?”梁老师继续引导着。

“梁老师,您之前说过 Oracle 数据库有日志缓存区和日志文件,这不就是余太太的记事本吗?只是日志缓存区也是内存区,也怕断电,但是日志文件是永久保存在物理磁盘中的,不怕断电。

“因此我认为COMMIT时日志缓存区肯定会把要操作的动作写到磁盘的日志文件里,这样Oracle 就不一定非要将数据从数据缓存区写到磁盘了。磁盘中的日志文件不是内存中的日志缓存区,会永久保存的,不怕断电,断电后可以依据磁盘里的日志文件重新操作一次,把数据缓存区丢失的数据恢复。

“所以数据缓存区是可以批量刷出的,效率和安全可以同时得到保障。”小莲思考后自信地给出了回答。

“小莲同学回答得非常好,完全正确,你把你刚才说的说慢一点重复给大家听。”梁老师开始注意到这个积极思考善于动脑的同学了。

小莲的描述让台下的同学终于都恍然大悟了。

“那老师问大家一个问题,数据缓存区中的数据是否批量越大越好呢?”

大家积极思考了一番,觉得如果出了问题,由于磁盘中的日志文件可恢复,就不必担忧安全问题了,而在性能方面,也想不出更大批量有什么坏处。余太太还存在资金周转的问题,Oracle应该不存在吧,不过由于感觉梁老师这样的提问肯定有玄机,大家都不说话了。

“同学们,你们知道老师为什么每次都喜欢用启发性的提问吗?就是希望大家养成思考的习惯,学会多角度看待问题。

“在数据库运行的过程中,批量刷出的数据占数据缓存区的比例越大,一般来说效率越高,而且也不用担心断电后的恢复问题。可是大家想想看,如果批量刷出的数据占数据缓存区的比例很大,那断电后数据库重启的恢复数据的动作必然需要的时间更长,大家等待的时间也就更长,难道不是这样吗?

“因此很多时候要考虑一个平衡问题:批量刷出的量比较小,Oracle性能就会降低,但是断电开机恢复的时间就较短;反之,批量刷出的量比较大,Oracle 性能是更高了,但是断电开机恢复的时间也较长。

“此外,我要给大家介绍一个后台进程的新成员,它就是CKPT。什么时候将数据缓存区中的数据写到磁盘的动作正是由进程CKPT来触发的,CKPT触发DBWR写出。这是我继DBWR、LGWR后介绍的第三个重要的后台进程,如图2-16中圆圈标记处。

图2-16 CKPT进程

“这是一个相当重要的进程,我们可以通过设置某参数来控制CKPT的触发时间,比如万一出现数据库崩溃,希望Oracle的SMON最多用多长时间来做实例恢复,该参数就是FAST START MTTR TARGET,通过调整该参数,Oracle会调配CKPT在适当的时候去调用DBWR……当然,这个参数也并非越小越好,太小的数值会导致Oracle性能降低。”

没想到一个COMMIT有这么多玄机,小莲越听越觉得有意思。

“那我再问一个问题,之前描述的update t set object_id=92 where object_id=29;语句执行完毕后,如果一直不提交,最终会从数据缓存区刷进磁盘吗?”

“会,因为DBWR将数据缓存区中的数据写到磁盘,不是由COMMIT决定的,而是由CKPT进程决定的。”小莲回答得很自信,台下同学纷纷表示赞同。

“回答得非常好,我补充一下,在 CKPT 的触发或者说命令下,DBWR将数据缓存区中的数据写到磁盘,但是如果LGWR出现故障了,DBWR此时还是会不听CKPT的命令罢工的,因为 Oracle 在将数据缓存区中的数据写到磁盘前,会先进行日志缓存区写进日志文件的操作,并耐心地等待其先完成,才会去完成这个‘内存刷到磁盘’的动作,这就是所谓的凡事有记录。

“好了,大家休息10分钟,下一节课我们一起评选一下数据库后台进程中的劳模。”

劳模?小莲有些发愣。管他呢,先休息再说。小莲起身出去休息了。

2.2.3.3 劳模的评选

“欢迎大家来到劳模评选的现场。”休息结束后从梁老师嘴里蹦出的这句话让大家都乐了。

“参加评选的总共有8名‘选手’,它们分别是 PMON、SMON、LCKn、RECO、CKPT、DBWR、LGWR、ARCH,它们有一个共同的特点,都是 Oracle 的后台进程,分别位于体系结构图的如下位置,我们用椭圆框标记出来(见图2-17)。

“实际情况是,Oracle的后台进程远不止这8个,我们只需挑选最核心、最重要、最有用的进程来描述就可以了,请大家记住。

“要想选出劳模,首先得了解它们的工作岗位职责及先进事迹,下面我依次介绍每位参评人员的情况。

图2-17 8个进程

“PMON的含义为Processes Monitor,是进程监视器。如果你在执行某些更新语句,未提交时进程崩溃了,这时候PMON会自动回滚该操作,无须人工去执行ROLLBACK命令。除此之外它还可以干预后台进程,比如RECO出现异常失败了,此时PMON会重启RECO进程,如果遇到LGWR进程失败这样的严重问题,PMON会做出中止实例这个激烈的动作,用于防止数据错乱。

“SMON的含义为System Monitor,可理解为系统监视器。与PMON不同的是,SMON关注的是系统级的操作而非单个进程,工作重点在于实例恢复,除此之外还有清理临时表空间、清理回滚段表空间、合并空闲空间等功能。

“LCKn仅用于RAC数据库,最多可有10个进程(LCK0,LCK1,…,LCK9),用于实例间的封锁。

“RECO用于分布式数据库的恢复,全称是Distributed Database Recovery,适用于两阶段提交的应用场景。这里我简单描述一下,比如我们面临多个数据库A、B、C,某个应用跨越三个数据库,在发起的过程中需要A、B、C库都提交成功,事务才会成功,只要有一个失败,就必须全部回滚。

“这和LCKn一样,适用的场景比较特殊。

“CKPT进程在前面已经介绍过了,由Oracle的FAST_START_MTTR_TARGET参数控制,用于触发DBWR从数据缓存区中写出数据到磁盘。CKPT执行得越频繁,DBWR写出就越频繁,DBWR写出越频繁越不能显示批量特性,性能就越低,但是数据库异常恢复的时间会越短。

“DBWR 是 Oracle 最核心的进程之一,负责把数据从数据缓存区写到磁盘里,该进程和CKPT相辅相成,因为是CKPT促成DBWR去写的。不过DBWR也和LGWR密切相关,因为DBWR 想将数据缓存区中的数据写到磁盘的时候,必须通知 LGWR 先完成日志缓存区写到磁盘的动作后,方可开工。

“再说说LGWR吧,这个进程的作用很简单,就是把日志缓存区中的数据从内存写到磁盘的REDO文件里,完成创建数据库对象、更新数据等操作过程的记录。这个REDO的记录非同小可,可以用来做数据库的异常恢复,只要保护好了这些 REDO 文件和后续对应的归档文件,从理论上来说,即使数据文件被删除了,也可以让数据库根据这些日志记录,把所有的在数据库中曾经发生的事情全部重做一遍,从而保证数据库的安全。

“正因为日志文件对数据库如此重要,LGWR也成了和DBWR一样核心的数据库进程。

“因为发生的事情是有顺序的,所以必须顺序地进行记录。比如数据库的操作是①先建一张表;②插入部分记录;③修改记录;④删除部分记录;⑤增加表字段……

“如果日志记录成①③②④⑤,那恢复的时候就有问题了,会提示数据不存在无须修改。变成②③①④⑤就更糟糕了,会提示表对象不存在。在保证顺序记录的前提下,出现中断也是绝对不允许的,只能恢复到断号前的场景。比如不小心在记录②的时候失败了,变成只记录了①③④⑤,那数据库最多只能恢复到①,即建表。”说到这里梁老师忽然停下来,问大家为什么。

“因为记录都没插入,怎么修改和删除啊?”同学们因为听得很仔细,所以回答得很迅速。

“那如果只记录了②③④⑤呢?”梁老师又发问。

“那就更不能恢复了,因为表都没建起来,插数据、改数据、删数据都没意义了。”同学们纷纷抢着回答。

“嗯,所以,有的时候,辛辛苦苦保留了几百个归档日志文件用于恢复,而第一个被破坏了,保留的那几百个归档文件全都没意义了,都变成废品了。”梁老师补充强调了一句。

小莲听后,不由得心中感叹了一番。

“继续说LGWR吧,LGWR必须记录下所有从数据缓存区写进数据文件的动作,工作任务相当繁重。由于在顺序记录情况下保留的日志才有意义,多进程难以保证顺序,因此LGWR只能采用单进程。为了解决这个问题,LGWR给自己施压,制定了五条严格的制度来要求自己,以此来适应高强度的日志记录工作。

“一、每隔三秒,LGWR运行一次。

“二、任何COMMIT触发LGWR运行一次。

“三、DBWR要把数据从数据缓存写到磁盘,触发LGWR运行一次。

“四、日志缓存区满三分之一或记录满1MB,触发LGWR运行一次。

“五、联机日志文件切换也将触发LGWR。

“最后登场的是ARCH进程,它的作用是在LGWR写日志写到需要被覆盖重写的时候,触发ARCH进程去转移日志文件,将日志文件复制出去形成归档日志文件,以免日志丢失,之前已经描述过了。

“好了,现在我的8位‘参评人员’都已经介绍完毕,请大家投票选出一名劳模。”

“LGWR!LGWR!LGWR!LGWR……”台下就听到一种声音。

“好,今天的劳模评选结束了,恭喜LGWR!”梁老师一本正经的样子逗乐了台下所有的同学。

2.2.3.4 回滚的研究

“在选劳模的过程中,大家了解了8位‘参评人员’的先进工作事迹,也顺道明白了Oracle主要后台进程的功能和特色,大家都记住了吗?”

“记住了!”小莲回答得最响亮。

“我在给大家描述体系物理结构时,开始用的是一条查询语句,结果很快就把体系结构说完了,大家却发现很多体系结构的组件未被提及,所以大家在考虑问题时一定要全面,如果整个数据库只有查询没有任何更新,那LGWR就没有存在的意义了,也拿不到劳模的殊荣了。不过这里还是有必要提一下,Oracle 数据库只要一启动,就会开始触发各种操作,即使用户不主动读写,系统进程也要有操作,联机日志文件中就会不断记录内容。

“说起更新语句,这可是非常关键的 SQL 指令,因为更新语句改变了数据库中的数据,正确与否非常重要,所以需要有一个用户确认的过程。如果用户认为更改没问题,就可以发出COMMIT指令,放心地将改变的结果保存在数据库中。如果当场后悔了,就发出ROLLBACK指令,及时撤销刚才的操作,这就是更新语句的回滚。

“比如大家执行 update t set object_id=92 where object_id=29;后,如果继续执行一个ROLLBACK,数据库不会将object_id=29的数据更改为92,这中间到底发生了什么,Oracle是如何做到的呢?”梁老师说到这里有些故作神秘,稍稍停顿了一会儿。

台下静悄悄的。

“那我来说说这个语句的回滚过程吧,为了简化描述,前面说过的PGA和共享池区的经历我就不再重复了。

“1.想更新object_id=29的记录,首先需要查到object_id=29的记录,检查object_id=29是否在数据缓存区里,不存在则从磁盘中将其读取到数据缓存区中,这一点和普通的查询语句类似。

“2.但是这毕竟不是查询语句而是更新语句,于是要做一件和查询语句很不同的事,在回滚表空间的相应回滚段事务表上分配事务槽,从而在回滚表空间分配到空间。该动作需要记录日志写进日志缓存区。

“3.在数据缓存区中创建 object_id=29的前镜像,前镜像数据也会写进磁盘的数据文件里(回滚表空间的数据文件),从缓存区写进磁盘的规律前面已经说过了,由CKPT决定,当然也别忘记这些动作都会记录日志,并将其写进日志缓存区,劳模LGWR还在忙着将日志缓存区中的数据写入磁盘形成redo文件呢。

“4.前面的步骤做好了,才允许将object_id=29修改为object_id=92,这个显然也是要记录进日志缓存区的。

“5.此时用户如果执行了提交,日志缓存区立即要记录这个提交信息,然后就把回滚段事务标记为非激活INACTIVE状态,表示允许重写。

“6.如果是执行了回滚呢,Oracle需要从回滚段中将前镜像object_id=29的数据读出来,修改数据缓存区,完成回滚。这个过程依然要产生日志,要将数据写进日志缓存区。”

说到这里,梁老师忽然停了下来,丰富的授课经验告诉他,学员每学习一段原理如果没有积极思考,是很难真正理解或者说很容易就淡忘了的。

“同学们,老师先说到这里,大家都听明白了吗?有什么疑问就举手问吧。”

“梁老师,记录前镜像为什么也要写日志,回滚的内容为什么要记录,我们偶尔才需要回滚吧?”胖小伙子举手发问。

“那你认为前镜像记录首先应记录在哪里,SGA 的数据缓存区中,还是磁盘的回滚段的数据文件里?”梁老师笑着问。

“数据缓存区里。”

“正确,那SGA的数据缓存区中的前镜像数据什么时候会刷新到磁盘里呢?”

“当数据缓存区中的数据达到一定量的时候,由CKPT触发数据缓存区写出刷新到磁盘中去,前镜像数据和正常的数据操作应该是一样的吧。”小莲看到胖小伙子暂时回答不出来,忍不住抢答了。

“小莲回答得非常好,其实刚才那位同学的疑问主要在于没有意识到用于准备回滚的前镜像数据的生成其实和普通数据操作差不多,唯一的差别就在于一个是刷新到磁盘的普通文件里,一个是刷新到磁盘的回滚数据文件里。

“如果是这样,就好理解了。普通数据操作可能会出现事务已经COMMIT了,但是数据在数据缓存区中并没有立即被刷新到磁盘,数据丢失后需要依据redo来重做的场景。回滚前镜像数据也是如此,你也需要用日志记录相关前镜像的操作来应对用户需要回滚的情况,否则当回滚的前镜像数据既不在内存又不在磁盘的情况出现后(比如突然断电等),用户此时该如何做回滚呢?当然是依据记录了前镜像相关操作的日志来重新做一次还原前镜像的操作。”

同学们这下听明白了。

“接下来大家跟我一起看看回滚段的相关参数,其中undo_management为AUTO表示是自动进行回滚段管理,回滚段空间不够时可以自动扩展;undo_retention 为900的含义是,DML语句需要记录前镜像,当 COMMIT 后,表示回滚段保留的前镜像被打上了可以覆盖重新使用的标记,但是要在900秒后方可允许;undo_tablespace为UNDOTBS1就不用多解释了,表示回滚段表空间的名字为UNDOTBS1,具体如下:

“为了加深大家的理解,我想问大家一个问题,DML 语句一般分为三类,即 insert 插入、update 修改和delete 删除,大家觉得这三类语句哪种语句对回滚的相关操作负荷最大,哪种最小,或者说哪种操作产生的undo最多,哪种产生的undo最少?”

同学们沉默了许久,梁老师开始提示大家:“回滚记录了什么?是记录了反向操作的动作,用于后续执行后‘覆水可收’的。”梁老师修改的这个成语把大家再次逗乐了。

“我明白了,应该是delete最多吧,删除的反向操作是插入,等于把整行记录完整地插入回去,那就是要把表的所有字段信息都记录下来。insert 应该最少吧,反向操作是delete,这个只要标记定位到原先的位置就可以,是不是只要记录这个定位的标记就可以了?而在update语句中,只需记录更新前字段的值并保存起来就可以了。” 晶晶回答得相当流利。

“回答得非常好,更准确的说法是insert的undo信息是记录了插入记录的rowid,这个就是你说的唯一标记。根据这个rowid就足以定位到插入的记录并删除,从而达到回退目的。rowid在未来的学习中我们会详细描述。”

台下的同学们不住地点头。

“那我再问第2个问题,我们不说undo说redo,insert、update、delete三类语句,哪种记录redo最多,哪种最少?”

“梁老师,这个和undo正好相反啊,redo就是把刚才做的事情重新做一遍。那delete只需记录相关rowid就可以再删除一遍了,很精简,产生的redo最少。而insert如果想再完成一遍,至少需要知道所有字段的取值,产生的redo肯定最多啊。不过update的情况不变,都是居中。”小莲回答得很自信,她知道自己一定回答正确了,经过梁老师启发性的发问后,她感觉自己对回滚的理解加深了很多。

“这个回答显然是经过思考的,很不错,不过我先不说对不对,再问第3个问题,专门针对undo会产生对应的redo吗?”

“会!”沉默了一小会儿,台下有几个同学同时大声回答。

“很好,梁老师前面说过,undo 的信息是准备用户回退的前镜像信息,是用于大家后悔自己操作后的还原动作,这些数据也是需要保护的,如果丢失了就别想还原了,而redo是非常好的恢复宝物,有它的记录我们就放心了。

“下面我总结一下DML语句的一个特点:

“DML 语句不同于查询语句,会改变数据库中的数据。除此之外,它还会产生用于将来恢复的redo和用于回退的undo。另外还有一个细节,由于undo也需要保护,所以还会专门产生保护undo操作的redo。”

小莲觉得自己不仅听明白了,思路也开阔了不少,她觉得Oracle的机制还挺合理的。

“大家还记得我刚才没有答复小莲的回答是否正确吗,大家好好思考一下,我不做回答,把这个悬念留到后面,在后面表设计的章节中会有相关试验,那时大家就会知道答案了。”

2.2.3.5 一致的查询

“同学们,前面有关回滚的机制都听明白了吧,无论是undo还是redo,都和更新语句有关,和查询语句无关。

“现在大家再回顾一下我最早举例的查询语句select object_name from t where object_id=29;,我对这个语句的执行情况做一个极端的假设。假设该语句查询时间非常长,我从早上8点开始发起查询,直到早上9点才查询完毕,给我返回结果。

“可是在8点到9点这段时间里,数据库发生了很多变化,比如object_id=29的记录8点的时候是可以返回2条结果的,但是8点半的时候已经被别的用户删除甚至还提交了,请问9点返回结果时,我看到的记录是没有返回还是返回2条,换句话说,是查询到8点那个时间点数据库的情况,还是9点这个时间点数据库的情况?”

同学们迟疑地你看我,我看你,有些犹豫不决,不过大部分回答都是查询不到记录,即返回的结果是基于9点那个时间点数据库的真实情况的结果。

“看来大家基本都认为该SQL语句会返回9点结束查询时数据库的数据。到底对还是不对呢?”

“梁老师,我觉得这个问题没有对还是不对,都有道理,要求返回8点那个时刻t表结果的人,认定自己要的是查询时刻的t表的记录情况。而认定返回9点那个时刻t表的人,他查询的时候是返回了当前数据库的真实情况,感觉各自都有各自的道理啊。”刚才一言不发的小莲起身说道。

“小莲说得很好,我发现大多数同学还是更倾向于查询结束后,t表此时要将真实情况反映给自己,好比有人查询自己有没有被录用,从8点查到9点才结束,但是自己的录用记录是在8点半时被插入的,如果9点查询结束后返回结果发现自己被录用了,很开心,但是如果按8点时刻来查询,最终只是失望地离去,实际上他已经被录用了。

“现在大家觉得返回8点的和返回9点的表记录这两种机制,哪种更合理,选哪个?”

“9点!” 这下大家回答得异口同声。

“哦,刚才我随便给大家举了一个在数据库中查询自己录用情况的例子后,大家都变得很肯定了,那我再给大家说一个故事,看看大家会不会改变想法。

“比如我有4个户头,户头1金额为1000元,户头2为2000元,户头3为3000元,户头4为4000元。如果我在数据库中查询自己的4个账户的总金额是多少,应该是返回10000元,细节为:1000+2000+3000+4000=10000元。具体如图2-18所示。

图2-18 4个户头中的金额

“假如数据库查询过程很慢,当我从自己的户头1读到户头3,还没来得及读到户头4时,我家人从户头1转移500元到户头4上,从而户头1变成500元,户头4变成4500元,其实此时我的总金额依然不变还是10 000元,细节为:500+2000+3000+4500=10 000元,如图2-19所示。

但是实际情况是,由于户头1到户头3我都已经读过了,Oracle不可能实时回头读以前读过的数据,否则查询永远都结束不了了。

图2-19 转户头

“因此情况就有可能变成这样,户头1到户头3读过时是1000+2000+3000,而读到户头4时该户头变为4500,总金额就是1000+2000+3000+4500=10 500,这显然是一个错误的答案,我的户头金额总数怎么会变成10 500呢(见图2-20)?

图2-20 转户头后有错

“梁老师查询自己所有户头总金额时发现结果展现的是10 500元,比预计的多了500元,这多出来的500元是不是要用来请大家吃饭啊?”

台下引来笑声一片,同学们都乐了。

“同学们知道问题出在哪里吗,如果我查询到户头4的时候,不是4500而是4000,结果是对的还是错的?”

“是对的!”同学们都看得很清楚。

“那刚才我说的8点查询开始到9点查询结束,返回的结果是8点的结果还是9点的结果,现在可以回答了吗?”梁老师又重复了一下引导大家思考之前提出的问题。

“8点!”所有同学都改口了。

“很好,如果老师从查询自己户头总金额的那一刻起,中间的变化都不去理会,那老师就不会多出500元了,这就是所谓的一致读。

“查询的结果由查询的那个时刻决定,数据新的变化是不予理睬的。如果不这样,Oracle居然会得出我的账户是10 500这个无论何时都不可能存在的错误数据,所以Oracle务必要保持一致读,避免错误产生。现在大家都明白了吗?”

“梁老师说得真是通俗易懂啊,还真没细想过数据库会存在这样的情景,看来设计数据库软件给大家使用,需要考虑的东西还真不少啊。”小莲心中暗自想到。

“大家再想另外一个问题,Oracle是如何保证查询到查询的那个时间点的数据的,比如大家知道,我查到最后一个户头的时候,明明这个值是4500,我却一定要查出4000,否则就会有问题,那数据库是如何做到的呢?”

台下半晌没人答上话来,是啊,Oracle如何做到这点呢?小莲也没想明白。

“其实这和回滚段有关系,Oracle的回滚段不仅保证了数据的回退,其实还提供了另外一个功能,即保证数据库的一致读。

回滚段究竟是如何保证数据库的一致读的呢?这其中到底隐藏着什么不为人知的秘密呢……大家休息10分钟,不要走远,后面的节目更精彩!”

2.2.3.6 一致读的原理

“大家好,现在我们来说一致读的原理,就是查询的记录由查询的这一时间点决定,后面即便发生变化了,也要根据回滚段保存的前镜像记录,取到那个时间点的数据。刚才梁老师举了查询自己账户的例子,大家应该印象很深刻吧?

“不过还有一个细节,比如我的转账记录是发生在我查询之前,由另一个人操作的,但是没提交,此时我依然看不到这个转账的变化。这个该如何保证呢?下面我也会一并说说。

“我想把问题说明白,必须先知道下面两个前提。

“一是了解数据库的SCN,SCN的全称是System Change Number,这是一个只会增加不会减少的递增数字,存在于Oracle的最小单位块里,当某块改变时SCN就会递增。

“二是数据库的回滚段记录事务槽(前面我在描述回滚的时候提过,事务槽是用来分配回滚空间的),如果你更新了某块,事务就被写进事务槽里。如果未提交或者回滚,该块就存在活动事务,数据库读到此块可以识别到这种情况的存在。

“现在我们来描述一下Oracle是如何实现一致读的,还是用我刚才转账的例子。当我早上8点整开始查询数据库时,首先会获取8点那个时刻的SCN号,并记录下来,比如是SCN8:00,那么8点的SCN8:00一定大于或等于记录在所有数据块头部的ITL槽中的 SCN号(如果有多个ITL槽,则为其中最大的那个SCN号)。比如在数据库中查询我的4个账户经历了块1到块4,而8点后需要查询的这些块都没更新,结果在查询过程中,发现这些块的SCN其实都小于SCN8:00,说明该块在这段时间内确实没被更新过,我们就放心地全读进去,总金额就是1000+2000+3000+4000=10 000,如图2-21所示。

“假如8点以后有更新,发生转账动作,那是怎么回事呢?比如当8点30分查询到块3(户头3)刚结束时,从户头1转500元到户头4,户头1从1000元变成500元,户头4从4000元变为4500元。此时块1和块4的ITL槽中的SCN号同时改变了,都变为SCN8:30,当数据库查询到块4时,发现块4的头部的ITL槽中的SCN号SCN8:30大于发出查询时间的SCN8:00,说明该数据块在8点以后被更新了,于是根据ITL槽中记录了对应的undo块的地址找到undo块,将undo块中的被修改前的数据(4000)取出,从而构建出被更新之前的那个时间点(8点30)的数据块内容,这个值就是4000。

图2-21 转户头与SCN

“此外虽然块1也变化为SCN8:30了,不过由于之前已经读过了,所以不会回头去比较,所以记录的还是1000,具体见图2-22。

图2-22 SCN详细分析

“因此我们查询的结果还是1000+2000+3000+4000=10 000。

“如果我们再读一遍数据库,比如9点10分开始,此时系统取到的SCN为SCN9:10,这时块1的SCN8:30、块2的SCN6:00、块3的SCN7:00、块4的SCN8:30,每个都小于SCN9:10,所以此时的总和是500+2000+3000+4500=10 000,和上一次的1000+2000+3000+4000不一样了,但是结果却都是对的,都是10 000。

“不过并不是查询开始的SCN大于或等于查询中所有块的SCN就一定可以直接获取数据,什么情景是这样的呢?”

梁老师辛苦描述了大半天,终于停下来问台下的同学们了。

“已经读过的块的 SCN 如果大于当前查询的 SCN,因为查过的不会回头查。”小胖子起身回答。

“很好,你说得很对,但是梁老师想说的不是这个,是指还没读过的场景,刚读到,发现SCN小于我们发起查询的SCN,不过还是不读取这个块的数据,去回滚段读取,是说这个。”

“会不会是别的用户发起的,更新未提交的数据啊?”小莲举手回答。

“正确,比如梁老师是8点开始查询的,7点时我的户头1为1000,7点30分被人更新为500未提交,此时我开始查询,SCN8:00显然大于SCN7:30,不过Oracle会发现另外一个问题,就是这个块有活动事务,所以还会从回滚段找到之前的前镜像数据,还是1000。此外要特别注意,如果由于前镜像被人不断地重写,1000这个数据从回滚段里找不回来了,那这个查询将会以ORA-01555的报错而终止退出,你的查询会失败,但是不会查询出一个错误的结果。

“因此Oracle在做一致读时,首先看发起的SCN是否大于当前查询块的SCN,如果小于,毫无疑问从回滚段获取前镜像数据。如果 SCN 确实大于当前查询块的 SCN,还要确保该块没有活动事务,否则还是要去前镜像查找。如果更新梁老师账户的那个人在7:30操作完后提交或者回退,那该块就不存在活动事务了。

“现在大家听明白了吗?”

“明白了!”同学们回答得很一致,这回大家认识得比较清晰了。

“早期的SQL Server的数据库版本,是读产生锁,在读数据时表就被锁住,这样确实是不存在问题了,不过如果读表会把表锁住,那数据库的并发也就做得太糟糕了。早期的其他数据库版本也有边读边锁的,比如已经读过的记录就允许被修改,而未读过的数据却是被锁住的,不允许修改,这虽然稍稍有些改进,只锁了部分表而非全部,但是还是读产生锁,非常糟糕。

“而 Oracle 的回滚段,解决了读一致性的问题,又避免了锁,大大增强了数据库并发操作的能力。”

这下大家对Oracle的回滚段有了新的认识,原来既可以回滚数据,又可以保证一致读,小莲觉得受益匪浅,同时也感叹软件设计的重要性。

2.2.3.7 实践的体会

“同学们,至此我已经将Oracle的体系结构基本讲完了,大家都听懂了吗?”

“梁老师,听是听懂了,不过觉得有一点抽象,不够直观啊。”小莲起身回答。

“还不够直观啊?梁老师又是画图又是讲故事,还执行SQL指令让大家去感受Oracle设计的优良之处,大家还记得SQL语句先后两次执行效率差别很大吧,知道原因吗?”

“知道!”大家响应倒是非常积极。

“其实我知道你们的意思,”梁老师笑着说,“你们只是感觉我说的这些东西看不见摸不着,都只是我说说而已,想眼见为实,是吧?”

台下同学纷纷点头。

1.内存的体会

“大家脑子里再回想一下体系结构图,梁老师描述了SGA、PGA内存区,首先体会这个。

“梁老师这里有一个数据库环境让大家体会一下,用sqlplus "/as sysdba" 连进数据库后,我们可以用show parameter sga来了解SGA开辟了多大空间,用show parameter pga来知道PGA开辟了多大空间,具体看脚本2-4:

脚本2-4 查看SGA及PGA的大小分配

“这里说明SGA是2368MB,而PGA是788MB。那共享池和数据缓存区又是多大呢?

“我们继续往下看脚本2-5:

脚本2-5 查看共享池和数据缓存区的大小分配

“结果令人大吃一惊,共享池和数据缓存区的大小都为0,这是怎么回事呢?这是因为这里Oracle 被设置为 SGA 自动管理,共享池和数据缓存区的大小分配由之前的 sga_max_size 和sga_target决定,总的大小为2368MB,它们分别被分配多少由Oracle来决定,无须人工干预,其中sga_target不能大于sga_max_size。二者有什么差别呢?举一个例子,比如sga_target=2GB,而 sga_max_size=8GB,表示数据库正常运行的情况下操作系统只分配2GB 的内存区给 Oracle使用,而这2GB就是共享池和数据缓存区等内存组件分配的大小,可是运行中发现内存不够用,这时操作系统可以再分配内存给SGA,但是最大不可以超过8GB。

“一般情况下建议使用SGA内存大小自动分配的原则,如果一定要手工分配,把sga_target设置为0,再把shared_pool_size和db_cache_size设置为非0,就可以了。

“此外,我们这里是以Oracle 10g为例的,在Oracle 11g中,自动化程度更彻底,推出了memory_target参数,只要设置这个参数值,连PGA都不需要设置了,memory_target参数指定的内存会自动分配给SGA与PGA。

“接下来我要让大家看得更直观、更具体一点,大家看如下ipcs-m这个查看共享内存的命令,见脚本2-6:

脚本2-6 从操作系统层面来感受SGA分配情况

“我们看看Oracle开辟的共享内存到底是不是2368MB,其中dest 表示共享内存段已经被删除,但是仍然有程序在连接着它,所以Oracle开辟的共享内存段大小是2 485 125 120,换算成MB是2 485 125 120 /1024/1024=2370MB,基本上等同于2368MB,略有一点点误差是正常的。“好了,梁老师把Oracle的内存区说完了,大家有什么疑问吗?”

“梁老师,我觉得不对啊,日志缓存区您好像没说,这个大小也是自动分配的吗?”晶晶举手问。

“非常好!老师发现大家上课都非常认真,而且记忆力很强,我特意漏说了这个,居然这么快就被同学们发现了。让我们看看日志缓存区在这个数据库环境中被分配了多大,见脚本2-7:

脚本2-7 查看日志缓存区的大小

“从这里可以看出,log_buffer被分配的大小大致为15MB,大家知道,log_buffer的大小是不能由sga_target来自动分配的,这个必须手动分配和调整。由于log_buffer每满1MB就要写一次,每满三分之一也要写一次,所以分配太大优化效果不是很明显,一般15MB即可满足需求。

“大家听到这里,还有什么疑问吗?”

“梁老师,这些参数的设置如何修改啊?”同样很积极的胖小伙子敬昱起身提问。

“敬昱同学问得很好,如果老师在上课,学生只是听课、记录而没有一点自己的想法,那效果根本就不会明显。这里显而易见的是,如果我们不修改数据库的这些内存参数,那所有的取值都是默认的,默认的取值可以满足所有的项目需求吗?肯定是不可能的,所以学会修改数据库参数就变得很重要了。具体命令如下。

alter system命令增加了一个新的选项scope。scope参数有3个可选值:memory、spfile和both。

● memory:只改变当前实例运行,重新启动数据库后失效。

● spfile:只改变spfile的设置,不改变当前实例运行,重新启动数据库后生效。

● both:同时改变实例及spfile,当前更改立即生效,重新启动数据库后仍然有效。

可以通过alter system或者导入导出来更改spfile的内容。

针对RAC环境,alter system还可以指定sid参数,对不同实例进行不同的设置。

通过spfile修改参数的完整命令如下:

① 如果当前实例使用的是pfile而非spfile,则scope=spfile或scope=both会产生错误。

② 如果实例以pfile启动,则scope的默认值为memory,若以spfile启动,则默认值为both。

③ 有些参数必须重启才能生效,如log_buffer。

④ scope的默认值为both。

“现在我想把sga_target从2368MB改为2000MB,这个命令如脚本2-8所示:

脚本2-8 修改SGA的大小(scope=spfile的方式)

“请问同学们,现在修改过来了吗?”

“改过来了!”大家异口同声。

梁老师笑了笑,再输入一次show parameter sga,结果大家发现sga_target依然是2368MB而不是2000MB。

“应该加上scope=memory或者scope=both才会立即生效!”终于有同学反应过来了。

“很好,memory重启后修改的参数就失效了,而both却可以永久保存。下面我举scope=both的例子,如脚本2-9所示:

脚本2-9 修改SGA的大小(scope=both的方式)

“终于改过来了,现在大家会修改了吧?这里要注意的是scope=xxx可以不写,默认为scope=both。此外,log_buffer 等参数必须重启才能生效,因此 alter system set log_buffer=15000000 scope=memory 或者是 scope=both 会报错,只支持 alter system set log_buffer=15000000 scope=spfile,然后重启后生效,具体如脚本2-10所示:

脚本2-10 修改log_buffer参数

2.进程的体会

“大家还记得Oracle数据库的组成吗?Oracle数据库是由实例和一组数据库文件组成的,实例则是由Oracle开辟的内存区和一组后台进程组成的。

“关于后台进程我们之前介绍了最主要的8个,其中LGWR还被大家全票通过,选为‘劳模’,大家都投出了自己神圣的一票,应该记忆深刻吧?

“不过在描述体系结构的过程中虽然我极力说得生动形象,但难免还是有些抽象。幸亏我及时带大家登录数据库环境体会了一下数据库内存,现在大家终于对SGA和PGA有了更形象的认识。

“体验过Oracle内存区后,现在我们继续登录Oracle数据库环境,来体会一下这些后台进程。我们登录的环境是Linux/UNIX环境,因为Windows环境中Oracle是多线程形式的,不好查看。而Oracle在UNIX环境中是多进程形式的,方便大家查看,如图2-23所示。

图2-23 Oracle进程体会

“方框标记的部分果然是刚才熟悉的几个进程,Oracle还有很多其他相关进程,这里就不一一探讨分析了。另外,从这里可以看出Oracle实例的名叫itmtest,具体可以在数据库中查看到,如脚本2-11所示:

脚本2-11 查看Oracle实例名

“因此我们刚才可以如下更准确地进行搜索,即ps –ef |grep itmtest

“此外,大家注意LOCAL=NO的部分(见图2-24),表示这是非Oracle本身的后台进程,是其他用户通过监听连进该数据库进行访问的,关于监听,我们随后会让大家体会到。

图2-24 LOCAL为NO的进程体会

“LOCAL=NO的这些进程如果被杀死了,数据库不会崩溃,只是如果某些应用正好连上来操作数据库,会被强制踢出数据库。而如果那些LGWR和DBWR进程被杀死了,那数据库立即就会崩溃,操作时请千万小心。

“不过这里少了一个很重要的进程(见脚本2-12),就是ARCH,这是为什么呢?”梁老师忽然停下来问大家。

脚本2-12 查看Oracle归档进程

台下同学都没想明白,大家纷纷摇头。

“大家还记得这是归档进程吗?当日志循环写入过程中会出现下一个日志已经被写过的情况,再继续写将会覆盖其内容,需要将这些即将被覆盖的内容写出到磁盘以形成归档文件,这样日志记录不会丢失,将来数据库就可以从这些日志文件和归档文件中进行数据库的恢复处理。

“不过这个归档并非总是必要的,因为有的数据库只是用来测试的,安全性要求不高,此时就可以考虑把归档关闭,数据库少做一件事,效率自然就更高了。

“所以刚才同学们查不到归档进程,正是因为被关闭了。”

同学们这下才都明白过来,不过梁老师的启发式教学让大家学会了上课期间踊跃思考问题,马上就有同学站起来问梁老师:“怎么看归档是开还是关,我们又如何打开和关闭呢?”

“好,那我就演示给大家看看。首先是如何查看数据库归档是开启还是关闭。操作很简单,登录数据库后输入archive log list 命令,具体如脚本2-13所示。显而易见,Database log mode 为No Archive Mode表示当前数据库是非归档的。

脚本2-13 查看Oracle归档是否开启

“更改数据库归档模式比较麻烦,需要重启数据库,将数据库置于mount状态后,输入alter database archivelog(如果是归档改为非归档,命令是alter database noarchivelog),然后再开启数据库alter database open,才可以将数据库更改为非归档,具体步骤如脚本2-14所示:

脚本2-14 将Oracle归档开启的方法

“现在再看一下,数据库已经是归档模式了,具体如脚本2-15所示:

脚本2-15 查看开启是否成功

“进程也可以查询到了,我们还发现ARCH进程是允许多进程的,当前是两个进程在运行,如脚本2-16所示:

脚本2-16 查看Oracle归档进程

3.启停的体会

“启动和关闭数据库可以说是数据库最常用的操作了,前面大家已经注意到了在将数据库归档开启时,我是先关闭数据库,再将数据库启动到mount状态,然后执行开启归档命令,最后将数据库打开。

“这里我将会结合之前介绍的体系结构图来向大家详细介绍一下数据库启动的具体步骤,如图2-25所示。首先看体系结构图的数据库部分,之前我并没有给大家描述参数文件和控制文件。

图2-25 启停的体会

“参数文件及控制文件和数据库的启动与关闭是息息相关的,数据库的启动可分为三个阶段,分别是nomount、mount和open。在启动的过程中可以直接输入startup启动,也可以分成startup nomount、startup mount和alter database open三步分别启动,下面我们依次描述三个步骤的细节。

“① startup nomount阶段

“Oracle必须读取到数据库的参数文件(pfile或者spfile),如果读不到该参数文件,数据库根本无法nomount成功!如果读到参数文件,将完成一件非常重要的事,就是根据参数文件中的内存分配策略分配相应的内存区域,并启动相应的后台进程,换言之,就是创建实例instance。

“为了保证数据库可以动态地修改参数,从Oracle 9i起,Oracle引进了spfile参数来替代之前仅有单一 pfile 的情况。具体在数据库开启后可以执行如下命令来了解,脚本2-17所示的就表示是spfile启动的:

脚本2-17 查看Oracle的spfile参数情况

“一般来说,Oracle 9i版本以后的数据库是这样一种情况,首先查找spfile文件,查找不到再查init.ora文件,再查不到,就报错,nomount失败。

“② startup mount阶段

“实例已经创建了,Oracle继续根据参数文件中描述的控制文件的名称及位置,去查找控制文件,一旦查找到立即锁定该控制文件。控制文件里记录了数据库中的数据文件、日志文件、检查点信息等非常重要的信息,所以Oracle成功锁定控制文件,就为后续读取操作这些文件打下了基础,锁定控制文件成功就表示数据库mount成功,为实例和数据库之间桥梁的搭建打下了基础。

“③ alter database open阶段

“根据控制文件记录的信息,定位到数据库文件、日志文件等,从而正式打通了实例和数据库之间的桥梁。

“总结一下,nomount阶段仅需一个参数文件即可成功,mount阶段要能够正常读取到控制文件才能成功,而open阶段需要保证所有的数据文件与日志文件和控制文件里记录的名称和位置一致,能被锁定访问更新的同时还要保证没有损坏,否则数据库的open阶段就不可能成功。

“下面我们来演示一下数据库启动的过程,前面大家见过 startup 命令,该命令实际是封装了三个阶段的步骤,下面我分步骤执行,让大家体会一下,见脚本2-18。

脚本2-18 Oracle启动的三个步骤

“数据库的关闭过程是启动过程的逆过程,先把数据库关闭,然后关闭数据库和实例之间的dismount,最后实例关闭,开辟的内存区消失,后台进程也全部消失。命令就是 shutdown immediate,注意这里没有分三个阶段执行的命令,整合在一个shutdown immediate命令中完成,如脚本2-19所示。

脚本2-19 关闭Oracle

“这时我们再观察可发现,Oracle开辟的key=0x66a02988、状态status不为dest的、大小为2 485 125 120的共享内存段已经消失了,如脚本2-20所示:

脚本2-20 观察Oracle关闭后的共享内存情况

“此外,进程也消失了,我们用 itmtest 的实例名来查找进程,发现不存在,如脚本2-21所示:

脚本2-21 观察Oracle进程情况

“这里我要让大家加深一下印象,我把数据库再次开启,并只开启到nomount状态,如脚本2-22所示:

脚本2-22 启动Oracle到nomount状态

“这时无论是共享内存SGA还是Oracle系列后台进程,都已经出现了,如脚本2-23所示:

脚本2-23 观察Oracle启动后内存分配和进程情况

“此外,我只要把数据库的参数文件移除或者重命名,数据库启动时就会失败在 nomount阶段。如果把数据库的控制文件移除或重命名,数据库启动就会失败在mount阶段。如果数据库中的任何一个数据文件或者日志文件被移除或者重命名了,数据库就会失败在open阶段。这里我就不做试验了,大家如果在电脑中搭建过数据库环境,可以自行尝试,大家想不想回去做个试验看看?”

“想!”台下传来一致的声音。梁老师虽然已经说得非常清楚了,大家还是很想回去后试验一下以加深印象。

“不过要切记,所有的试验都必须在你们自己的电脑环境中操作,千万别在生产环境中随便做试验!”梁老师再次强调。

4.文件的体会

“没有参数文件,无法创建实例,数据库无法 nomount 成功;没有控制文件,数据库无法mount;没有数据文件,数据库无法打开使用(此外没有了数据文件,数据也没地方保存了,数据库也失去意义了);没有日志和归档文件,数据库就失去了保护伞,变得很不安全。因此所有这些文件都非常重要。

“我想让大家感受一下 Oracle 数据库中这些参数文件、控制文件、数据文件、日志文件、归档文件存在数据库所在主机的什么位置,又都是通过什么方法查询到的,让大家真真切切感觉到它们的存在,从而进一步加深印象,请参见脚本2-24:

脚本2-24 查看参数文件、控制文件、数据文件、日志文件、归档文件和告警文件

“至此,梁老师历尽千辛万苦,终于和大家一起完成了 Oracle 数据库实践的4次体会,大家现在还觉得抽象吗?是否觉得面对Oracle不再陌生,亲切了许多?”

“确实是感觉亲切多了!”小莲心中暗自感慨了一番。

结合这堂实践体会课和先前梁老师做的SQL脚本执行的系列试验,小莲忽然觉得先前梁老师让自己不断强记的体系结构,现在好像根本不需要去记忆了。甚至觉得,体系结构图理所当然就应该是这样的。

5.监听的体会

“梁老师在这里补充一个知识点,就是Oracle的监听,如果想在远程A机器上通过网络访问本地B机器上的数据库,B机器上的数据库必须开启监听。远程的A机器只需安装数据库客户端,然后通过读取A机器上数据库客户端配置的tnsnames.ora配置文件,即可连接并访问B机器上的数据库。这里只是简要说明一下,不是课程的重点,详细的文档可以参考Oracle官方文档的CONCEPT说明。下面我们介绍监听状态的查看,监听的开启,以及监听的关闭。

“脚本2-25中所示的lsnrctl status命令是查看监听状态的命令,其中Listener Parameter File和Listener Log File定位了监听文件listener.ora以及对应的日志:

脚本2-25 查看监听状态

“脚本2-26所示的lsnrctl stop命令是关闭监听的命令:

脚本2-26 关闭Oracle监听

“查看发现监听果然被关闭,提示No listener,如脚本2-27所示:

脚本2-27 查看关闭Oracle监听后的情况

“开启命令为lsnrctl start,具体如脚本2-28所示:

脚本2-28 开启Oracle监听