一个新的版本是作为当前版本的子版本创建的,它继承了父版本的所有“状态”:所有的PL/SQL代码,视图,同义词,等等。当一个子版本**被创建,它是父版本的一个镜像,但这个镜像是可以修改的。例如,你可以在你当前的生产库代码基础上创建新版本,在这个新版本中,执行CREATE OR REPLACE PROCEDURE P指令,在新版本中覆盖存储过程P。而当前的版本,即你的最终用户所见的那个版本,具有存储过程P的旧版,你的用户将不受新版本的影响。你可以在新版本中安装新版本的存储过程P并且重新编译那些失效的代码,不会对当前版本有任何影响。当你确认你在新版本中的所有修改都已经是正确和完整的,你可以把这个新版本公开发布,新代码就可以使用了。为了安装新版本的存储过程P并且使得所有依赖代码生效,你用不着把正在执行过程P的用户赶出去;我们可以在旧代码还被使用的时候做这些事情。产品环境的变更本来可能是一个冗长的、复杂的、脱机的过程,现在对最终用户(这是最重要的人群)而言瞬间就完成了。
上一次,在“A Closer Look at the New Edition”一文中,我们看到了一个简单的应用场景。我们的数据库中安装了一个版本为1.0的应用程序,我们想为它打补丁;具体地说,我们需要改正其中的一个PL/SQL单元。通常情况下,DBA或者应用管理员不得不安排一个系统维护时段来打这个补丁,因为他要重新编译代码。大部分DBA都知道,如果别人正在执行一个PL/SQL单元,你就无法编译它。此外,就算你能够重编译那个包,你的应用用户会告诉你,他们突然看到一个“ORA-04068: existing state of packages has been discarded” 的错误信息。这些情形使得在现实生活中,你不可能在产品环境中为你的应用程序做在线升级或者在线补丁。这就是在线版本变更功能所要改变的:现在,当最终用户正在执行旧版本的代码时,我们能够在线为应用程序打补丁。
那么我们来看看面对哪些挑战。第一件要做的事是新增两个列并去掉一个旧的列。我们必须以一种特殊方式来增加新列,使得现在的应用程序不受影响。万一应用程序中有一个SELECT * FROM EMPLOYEES查询怎么办?这是一个编程坏习惯,但是它仍然可能发生。或者应用程序中有这样的代码:INSERT INTO EMPLOYEES VALUES (. . . ), 没有指定列的清单(另一个非常糟糕的编程习惯)。增加新的两个列可能破坏现有的应用程序。类似的,我们需要删除一个当前应用程序正在使用的列(所以显然我们不能够真正删除它)。
为了对付这些难题,Oracle数据库11gR2版引入了一个版本视图的概念,它为应用程序代码和物理模式之间提供了一个缓冲空间。一个版本视图是一种特殊的视图,它只能够从单一的表执行SELECT操作,并把它所需要的属性清单投影到那个表。没有WHERE子句,也没有表连接,仅仅是SELECT和FROM。你可以把一个版本视图想象为和同义词非常相似,但这个同义词不仅仅能够将指向对象重命名,而且还能够选择列和列的命名。不仅如此,这个版本视图还可以在上面定义触发器;不是一个视图通常具有的INSTEAD OF触发器,而是所有常规的表触发器类型,例如BEFORE, BEFORE EACH ROW, 和复合触发器(译者注:compound trigger是11G以上支持的新类型)。因此,我们可以利用这些版本视图来为现有的应用程序隐藏两个新列,我们还能用一个新的版本视图在我们的新应用程序中来隐藏那个旧列(就像我们已经删除了它一样)并且把两个新列显露出来。
SQL> alter table emp add resume blob;
alter table emp add resume blob
*
ERROR at line 1:
ORA-00054: resource busy and acquire
with NOWAIT specified
在过去,在一个活跃的系统上执行DDL操作通常是不可能的。可是,在Oracle数据库11GR1和R2的某些场景中,在一个活跃的系统上执行DDL的能力被大大增强了。许多DDL操作,比如增加一个列,现在可以用一种不阻塞的方式来执行,或者换句话说,假如这个DDL确实需要隔离执行,我们可以使用等待型的DDL来避免ORA-00054错误(参见Oracle杂志 2008年五/六月刊中有关这个话题的文章“On Seeing Double in V$SQL” http://www.Oracle.com/technetwor ... 8asktom-085659.html)
在过去,DDL修改例如"ADD COLUMN"所带来的另一个问题是相关的对象失效:任何在此表上创建的视图都会失效,任何依赖于这张表的PL/SQL单元都会失效,等等。这使得你不能够在线增加一个列。但是,Oracle数据库11GR1开始有了"精细依赖性跟踪"(参见Oracle杂志2009年七/八月刊文章“ On Popularity and Natural Selection ”中有关“不用SELECT *的第13134213个理由”一节, http://www.Oracle.com/technetwor ... 9asktom-090487.html),我们已经解决了第一个问题:“如何安全地增加两个新列而不会使任何现有代码失效”。我们利用版本视图对现有应用程序隐藏新列,这可以受到精细依赖性跟踪的保护。此外,我们现在可以利用版本视图,虚拟地将现有的列删掉;同时把两个新增的列显露给最新版本的应用程序,同样还是利用版本视图。
那么在PHONE#列上创建一个索引又会如何?这里有两个问题:创建索引,还要担心这个索引影响现有系统的查询性能。增加一个新索引可能改善某些查询的响应时间,对另外一些查询没有影响,而对第三类查询却可能有负面影响。过去,在Oracle数据库11GR1之前,一个CREATE INDEX语句总是需要在表上加一个排他锁, 甚至一个CREATE INDEX ONLINE的操作也需要在开始创建索引的瞬间需要这么一个锁。然而,从Oracle数据库11GR1开始,在线创建一个索引已经完全没有阻塞了。它完全不需要任何锁,所以能够避免加锁和阻塞问题。(注意:CREATE INDEX ONLINE操作是Oracle企业版数据库才有的功能)此外,索引现在可以被创建为隐形的,意思是它会存在、被修改维护,但是不会被用在存取路径中,除非有一个会话明确地要求使用这个索引。所以,现在我们能够在PHONE#列上增加一个索引而不用担心阻塞/加锁的问题,并且能够确定这个新索引不会对现有的查询性能产生影响,而这个查询我们也许还没有用新索引测试过。因此我们可以安全地增加这个索引而不会对现版本的应用程序产生影响。
幸运的是,我们可以解决这两个问题。第一个问题:单个批量UPDATE对整个表加锁的问题可以利用一个新的包来解决,DBMS_PARALLEL_EXECUTE (在Oracle杂志2009年十一/十二月刊的文章“Looking at the New Edition”中我们已经见识过这个功能,我给它取了个昵称叫做“DIY并行操作”http://www.Oracle.com/technetwor ... 9asktom-089919.html)利用DBMS_PARALLEL_EXECUTE包,我们能够把任务划分成很小的片, 以我们所需的任何并行度:从并行度1(其实就是串行)到1000不等,来修改表中的每一行。这会缩减每片表数据涉及到的数据量,以及被锁的时间。如果我们把表分成100片并且使用并行度1, 我们每次加锁的表数据不超过1%;如果我们把表分成1000片, 我们每次加锁的表数据不超过0.1%, 以此类推。
我们假设我们已经有了在上一篇专栏中设置和使用过的DEMO账户。你可能记得,DEMO账户是一个普通模式,它仅仅有CREATE SESSION 和 CREATE PROCEDURE,另外还有为模式创建版本的权限(ALTER USER DEMO ENABLE EDITIONS),同时还能够使用一个现有的称为VERSION2 的版本(GRANT USE ON EDITION VERSION2 TO DEMO)。在这个例子中,我们会赋予DEMO账户创建表和序列的权限,这样我们就能够拷贝EMPLOYEES表;我们还要赋予创建视图和触发器的权限,除此之外的权限集保留原样。
我们来看看现有版本1.0的应用程序的设置:
SQL> create table
2 employees
3 as
4 select *
5 from hr.employees;
我们还利用ONLINE和INVISIBLE功能,在新增的列上建立了一个新索引。从Oracle数据库11GR1版开始,CREATE INDEX ONLINE操作是**的在线操作:它从未在任何时间点锁住表,而在以前的版本是要锁表的。新的INVISIBLE选项使得我们能够创建索引而不会影响当前应用版本的查询计划。我们使用这个选项是因为增加一个索引可能使得某些查询跑得更快,另一些维持原速度,而第三类查询可能变得更慢。因为我们没有用这个新索引测试过现有应用的查询计划,所以我们想让这个新索引隐藏起来。
为了克服这些障碍,我们查看了两个新功能。首先我们讨论了DBMS_PARALLEL_EXECUTE包。(在Oracle杂志2009年十一/十二月刊的文章“Looking at the New Edition”中我们**见识过这个功能,我给它取了个昵称叫做“DIY并行操作”http://www.Oracle.com/technetwor ... 9asktom-089919.html)利用DBMS_PARALLEL_EXECUTE包,我们能够把任务划分成很小的片, 以我们所需的任何并行度:从并行度1(其实就是串行)到1000不等,来修改表中的每一行,因而我们永远不需要锁住整个表,而是每次只锁住一小片。然后,我们引入了一个新型的触发器:跨版本触发器。跨版本触发器只在应用升级的过程中被使用(我们在升级过程结束后将会尽快把他们删除)。跨版本触发器可以被用来“转送”旧版本上的一个修改到新的版本。它们也能被用来“返送”新版本所作的一个修改到旧版本。
SQL> insert into employees
2 select * from
3 (
4 with data(r)
5 as
6 (select 1 r from dual
7 union all
8 select r+1 from data where r <= 100
9 )
10 select rownum+(select max(employee_id)
11 from employees_rt),
12 FIRST_NAME, LAST_NAME, EMAIL,
13 PHONE_NUMBER, HIRE_DATE, JOB_ID,
14 SALARY, COMMISSION_PCT, MANAGER_ID,
15 DEPARTMENT_ID
16 from employees_rt, data
17 );
SQL> select phone_number, country_code, phone#
2 from employees_rt
3 where country_code is null
4 and rownum <= 5
5 union all
6 select phone_number, country_code, phone#
7 from employees_rt
8 where country_code is NOT null
9 and rownum <= 5;
现在我们想要安装我们的新应用代码。我们采用的方法,和这个三部分系列文章中的第一篇中使用的方法相同:我们简单地切换到新版本并且CREATE OR REPLACE我们的可版本化对象(即EMPLOYEES版本视图和EMP_PKG包)。注意为了简洁起见,并不是所有代码都在清单6中列出,而仅仅是对当前代码的变动。
代码清单 6: 安装新的应用
SQL> alter session set edition = version2;
Session altered.
SQL>
SQL> select object_name, object_type, status, edition_name
2 from user_objects_ae
3 where object_name in ( 'EMPLOYEES', 'EMP_PKG' );
为了让新版本成为缺省版本,我们会用一个AFTER LOGON触发器来为每个新创建的会话设置版本。这使得新的会话会看到我们的新版本代码,同时现有的会话继续执行旧的版本。因为现有版本继续执行旧代码,我们可以完全去除可怕的“ORA-4068 existing state of packages has been discarded”错误,在过去每当你替换现有的代码时,你往往会得到这个错误。这个功能进一步增强了应用系统升级的能力。我们不仅仅消除了应用升级过程中的停机时间,而且还消除了切换到新版本的停机时间。