Oracle 9i & 10g编程艺术-深入数据库体系结构——第14章:并行执行

来源:互联网 发布:淘宝店铺导航条装修 编辑:程序博客网 时间:2024/05/18 00:28
 

第14章                      并行执行

并行执行(parallel execution)是Oracle企业版才有的特性(标准版中没有这个特性),最早于1994年在Oracle 7.1.6中引入。所谓并行执行,是指能够将一个大型串行任务(任何DML,或者一般的DDL)物理地划分为多个较小的部分,这些较小的部分可以同时得到处理。Oracle中的并行执行正是模拟了我们在实际生活中经常见到的并发处理。你可能很少看到一个人单枪匹马地盖房子;更常见的是由多个团队并发地工作,迅速地建成房屋。采用这种方式,某些操作可以划分为较小的任务,从而并发地执行。例如,铺设管线和电路配线就可以同时进行,以减少整个工作所需的总时间。

Oracle中的并行执行遵循了几乎相同的逻辑。Oracle通常可以将某个大“任务”划分为较小的部分,并且并发地执行各个部分。例如,如果需要对一个大表执行全表扫描,那么Oracle完全可以建立4个并行会话(P001~P004)来一起执行完全扫描,每个会话分别读取表中一个不同的部分(既然这样能更快地完成任务,没有理由不这样做)。如果需要存储P001~P004扫描的数据,这个工作可以再由另外4个并行会话(P005~P008)来执行,最后它们可以将结果发送给这个查询的总体协调会话。

并行执行作为一个工具,如果使用得当,可以使某些操作的响应时间大幅改善,使速度呈数量级增长。但是,如果把并行执行当作一个“fast=true”型的提速开关,结果往往适得其反。在这一章中,我们并不打算明确地解释Oracle中如何实现并行查询,也不会介绍由并行操作可能得到的多种计划组合,在此不会讨论诸如此类的内容。我认为,这些内容已经在各种Oracle手册(Oracle Administrator’s GuideOracle Concepts Guide,特别是Oracle Data Warehousing Guide)中得到了很好的说明。这一章的目标只是让你对并行执行适用于哪些类型的问题(以及对哪些类型的问题不适用)有所了解。具体来讲,我们会介绍何时使用并行执行,在此之后,还将介绍下面的内容:

q         并行查询:这是指能使用多个操作系统进程或线程来执行一个查询。Oracle会发现能并行执行的操作(如全表扫描或大规模排序),并创建一个查询计划来实现)。

q         并行DMLPDML):这在本质上与并行查询很相似,但是PDML主要是使用并行处理来执行修改(INSERTUPDATEDELETEMERGE)。在这一章中,我们将介绍PDML,并讨论PDML所固有的一些限制。

q         并行DDL:并行DDL是指Oracle能并行地执行大规模的DDL操作。例如,索引重建、创建一个新索引、数据加载以及大表的重组等都可以使用并行处理。在我看来,这正是数据库中并行化的“闪光之处”,所以我们会用很多的篇幅重点讨论这方面内容。

q         并行恢复:这是指数据库能并行地执行实例(甚至介质)恢复,以减少从故障恢复所需的时间。

q         过程并行化:这是指能并行地运行所开发的代码。在这一章中,我会讨论两种过程并行化方法。第一种方法中,Oracle以一种对开发人员透明的方式并行地运行开发的PL/SQL代码(开发人员并不开放代码;而是由Oracle透明地为其并行执行代码)。此外还有另一种方法,我把它成为“DIY并行化”(do-it-yourself parallelism),即开发的代码本身被设计为要并行地执行。

1.1   何时使用并行执行

并行执行可能很神奇。一个过程原本需要执行数小时或者数天,通过使用并行执行,可能几分钟内就可以完成。将一个大问题分解为小部分,这在有些情况下可以显著地减少处理时间。不过,考虑并行执行时,要记住一个基本概念,这可能很有用。下面这句话选自Jonathan Lewis所著的Practical Oracle8i: Building Efficient Databases(Addison-Wesley, 2001),尽管很简短,但它对这个概念做了很好的总结:

并行查询(PARALLEL QUERY)选项本质上是不可扩缩的。

并行执行本质上是一个不可扩缩的解决方案,设计为允许单个用户或每个特定SQL语句占用数据库的所有资源。如果某个特性允许一个人使用所有可用的资源,倘若再允许两个人使用这个特性,就会遇到明显的竞争问题。随着系统上并发用户数的增加,要应付这些并发用户,系统上的资源(内存、CPUI/O)开始表现出有些勉为其难,此时能不能部署并行操作就有疑问了。例如,如果你有一台4CPU的主机,平均有32个用户同时执行查询,那你很可能不希望并行执行他们的操作。如果允许每个用户执行一个“并行度为2”的查询,这就会在一台仅有4CPU的主机上发生64个并发操作。即使并行执行之前这台机器原本不算疲于奔命,现在也很可能难逃此劫。

简单地说,并行执行也可能是一个很糟糕的想法。在许多情况下,应用并行处理后,可能只会带来资源占用的增加,因为并行执行试图使用所有可用的资源。在一个系统(如一个OLTP系统)中,如果资源必须由多个并发事务所共享,你就会观察到,并行执行可能导致响应时间增加。Oracle会避开某些在串行执行计划中能高效使用的执行技术,而采用诸如全面扫描等执行路径,希望能把较大的批量操作划分为多个部分,并且并行地完成这些部分,从而能优于串行计划。但是如果并行执行应用不当,不仅不能解决性能问题,反而会成为性能问题的根源。

所以,在应用并行执行之前,需要保证以下两点成立:

q         必须有一个非常大的任务,如对50GB数据进行全面扫描。

q         必须有足够的可用资源。在并行全面扫描50GB数据之前,你要确保有足够的空闲CPU(以容纳并行进程),还要有足够的I/O通道。50GB数据可能分布在多个物理磁盘上(而不只是一个物理磁盘),以允许多个并发读请求能同时发生,从磁盘到计算机应当有足够多的I/O通道,以便能并行地从磁盘获取数据,等等。

如果只有一个小任务(通常OLTP系统中执行的查询就是这种典型的小任务),或者你的可用资源不足(这也是OLTP系统中很典型的情况),其中CPUI/O资源通常已经得到最大限度的使用,那就根本不用考虑并行执行。

关于并行处理的类比

我经常使用一个类比来描述并行处理,解释为什么需要有一个大任务,还要求数据库中有足够多的空闲资源。这个类比是写一个有10章的详尽报告,每一章之间都要相互独立。例如,可以考虑你手上的这本书。本章与第9章是独立的,不必按先后顺序写这两章。

如何完成这两个任务?你认为并行处理对哪个任务有好处?

1.      一页的总结

在这个类比中,第一个任务是要完成只有一页的总结,这并不是一个大任务。你可以自己来写,也可以把这个任务安排给某个人。为什么呢?因为倘若将这个处理“并行化”,所需的工作量反而会超过你自己写这一页所需的工作。如果要实现“并行化”,你可能必须坐下来,想清楚应该有12段,并确定每一段不能依赖于其他段落,然后召开一个小组会议,选择12个人,向他们解释问题是什么,并安排每个人完成一段,然后作为一个协调员收集所有段落,按正确的顺序摆好,验证这些段落无误,然后打印报告。与你自己串行地写这一页相比,上述过程很可能需要花费更长的时间。对于这样一个小规模的项目,管理这么一大群人的开销远远超过了并行编写12段所能得到的收益。

数据库中的并行执行也是同样的道理。如果一个任务只需要几秒(或更短时间)就能串行地完成,引入并行执行后,相关的管理开销可能会让整个过程花费更长的时间。

2.      10章的报告

现在再来分析第二个任务。你希望你尽快地写出这个10章的报告,对此最慢的办法就是将所有工作都安排给某一个人(相信我说的话,因为我很清楚这一点,看看这本书就知道了!有时我真是希望能有15个“我”在同时写)。此时,你要召开会议,审查处理步骤,分配工作,然后作为协调员收集结果,完成最后的报告并提交。这个过程可能不是在1/10的时间内完成,而可能在1/8左右的时间内完成。同样,这么说有一个附加条件,你要有足够的空闲资源。如果你的员工很多,而且目前手头都没有什么具体工作,那么划分这个工作绝对是有意义的。

不过,假设你是经理,请考虑以下情况:你的员工可能手头有很多任务。在这种情况下,就必须谨慎对待这个大项目。你要保证不要让你的员工疲于奔命;你不希望他们过度疲劳,超过承受极限。如果你的资源(你的员工)无力处理,你就不能再分配更多的工作,否则他们肯定会辞职。如果你的员工已经是满负荷的,增加更多的工作只会导致所有进度都落空,所有项目都延迟。

Oracle中的并行执行也是一样。如果一个任务要花几分钟、几小时或者几天的时间执行,引入并行执行可能会使这个任务的运行快上8倍。但是,重申一遍,如果资源已经被过度使用(员工已经超负荷工作),就要避免引入并行执行,因为系统可能会变得更迟钝。尽管Oracle服务器进程不会递交“辞职书”,但它们可能会用完所有的RAM并失败,或者只会长时间地等待I/OCPU,看上去就好像没有做任何工作一样。

如果牢记这个类比(但要记住不能不合理地过分夸大类比),对于并行化是否有用,你就有了一个常识性的指导原则。如果有一个任务需要花几秒的时间完成,要想通过使用并行执行让它更快一些,就很可能成问题,而且通常可能适得其反。如果已经在过度使用资源(也就是说,你的资源已经得到充分利用),再增加并行执行很可能会使情况更糟,而不是更好。如果有一个相当大的任务,而且有充分的额外资源,并行执行就再合适不过了。在这一章中,我们将介绍充分利用执行资源的一些方法。

1.2   并行查询

并行查询允许将一个SQL SELECT语句划分为多个较小的查询,每个部分的查询并发地运行,然后会将各个部分的结果组合起来,提供最终的答案。例如,考虑以下查询:

big_table@ORA10G> select count(status) from big_table;

通过并行查询,这个查询可以使用多个并行会话;将BIG_TABLE划分为较小的不重叠的部分(片);要求各个并行会话读取表并统计它那一部分中的行数。这个会话的并行查询协调器再从各个并行会话接收各个统计结果,进一步汇总这些结果,将最后的答案返回给客户应用。如果用图形来表示,这个过程如图14-1所示。

P000P001P002P003进程称为并行执行服务器(parallel execution server),有时也称为并行查询(parallel queryPQ)从属进程。各个并行执行服务器都是单独的会话,就像是专业服务器进程一样连接数据库。每个并行执行服务器分别负责扫描BIG_TABLE中一个部分(各个部分都不重叠),汇总其结果子集,将其输出发回给协调服务器(即原始会话的服务器进程),它再将这些子结果汇总为最终答案。

14-1    并行select count(status)示意图

可以通过一个解释计划来查看这个查询。使用一个有10,000,000行数据的BIG_TABLE,我们将逐步启用这个表的一个并行查询,来了解如何“看到”实际的并行查询。这个例子在一个4CPU主机上执行,所有并行参数都取默认值;也就是说,这是一个初始安装,只设置了必要的参数,包括SGA_TARGET(设置为1GB)、CONTROL_FILESDB_BLOCK_SIZE(设置为8KB)和PGA_AGGREGATE_TARGET(设置为512MB)。起初,我们可能会看到以下计划:

big_table@ORA10GR1> explain plan for

2 select count(status) from big_table;

Explained.

big_table@ORA10GR1> select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT

----------------------------------------

Plan hash value: 1287793122

--------------------------------------------------------------------------------------------------------

| Id    | Operation                      | Name           | Rows   |Bytes | Cost (%CPU)  | Time      |

------------------------------------------------------------------------------------------------------------------------------

| 0     | SELECT STATEMENT   |                     | 1        | 17     | 32390 (2)       |00:06:29   |

| 1     | SORT AGGREGATE     |                     | 1        | 17     |                      |              |

| 2     | TABLE ACCESS FULL   | BIG_TABLE              | 10M   | 162M             | 32390 (2) |00:06:29   |

------------------------------------------------------------------------------------------------------------------------------

这是一个典型的串行计划。这里不涉及并行化,因为我们没有请求启用并行查询,而默认情况下并不启用并行查询。

启用并行查询有多种方法,可以直接在查询中使用一个提示,或者修改表,要求考虑并行执行路径(在这里,我们将会使用后一种做法)。

可以具体指定这个表的执行路径中要考虑的并行度。例如,可以告诉Oracle:“我们希望你在创建这个表的执行计划时使用并行度4

big_table@ORA10GR1> alter table big_table parallel 4;

Table altered.

但我更喜欢这样告诉Oracle:“请考虑并行执行,但是你要根据当前的系统工作负载和查询本身来确定适当的并行度”。也就是说,并行度要随着系统上工作负载的增减而变化。如果有充足的空闲资源,并行度会上升;如果可用资源有限,并行度则会下降。这样就不会为机器强加一个固定的并行度。利用这种方法,允许Oracle动态地增加或减少查询所需的并发资源量。

因此,我们只是通过以下ALTER TABLE命令来启用对这个表的并行查询:

big_table@ORA10GR1> alter table big_table parallel;

Table altered.

仅此而已。现在,对这个表的操作就会考虑并行查询。重新运行解释计划,可能看到以下结果:

big_table@ORA10GR1> explain plan for

2    select count(status) from big_table;

Explained.

 

big_table@ORA10GR1> select * from table(dbms_xplan.display);

 

PLAN_TABLE_OUTPUT

-----------------------------------------

Plan hash value: 1651916128

----------------------------------------------------------------------------

|Id                                             | Operation     | Name         |Cost(%CPU)        | TQ                 |IN-OUT |PQ Distrib       |

----------------------------------------------------------------------------

| 0  | SELECT STATEMENT       |                     | 4465 (1)      |          |              |                      |

| 1  | SORT AGGREGATE         |                     |                   |          |              |                      |

| 2  | PX COORDINATOR           |                     |                   |          |              |                      |

| 3  | PX SEND QC (RANDOM)   | :TQ10000     |                   |Q1,00 | P->S      |QC (RAND)     |

| 4  | SORT AGGREGATE         |                     |                   |Q1,00 | PCWP   |                      |

| 5  | PX BLOCK ITERATOR      |                     | 4465 (1)      |Q1,00 | PCWC   |                      |

| 6  | TABLE ACCESS FULL       | BIG_TABLE  | 4465 (1)      |Q1,00 | PCWP   |                      |

----------------------------------------------------------------------------

注意      我们从这个计划输出中去掉了ROWSBYTESTIME这几列,以便能在一页篇幅内放下。不过,查询的总时间是00:00:54,而不是先前对串行计划估计的00:06:29。要记住,这些只是估计,不能保证肯定如此!另外,这是Oracle 10g的计划输出,Oracle9i的计划输出提供的细节更少(只有4步,而不是7步),但是最终结果是一样的。

如果自下而上地读这个计划(从ID=6开始),它显示了图14-1中所示的步骤。全表扫描会分解为多个较小的扫描(第5步)。每个扫描汇总其COUNT(STATUS)值(第4步)。这些子结果将传送给并行查询协调器(第2步和第3步),它会进一步汇总这些结果(第1步),并输出答案。

如果在一个新启动的系统上执行这个查询(这个系统还没有执行其他任何并行执行),可以观察到以下结果。在此使用Linux ps命令来找出并行查询进程(我们希望找不出这样的进程,因为系统开始时没有执行任何并行执行),启用并行执行后再运行这个查询,然后再次查找出并行查询进程:

big_table@ORA10GR1> host ps -auxww | grep '^ora10gr1.*ora_p00._ora10g'

big_table@ORA10GR1> select count(status) from big_table;

 

COUNT(STATUS)

-------------

10000000

 

big_table@ORA10GR1> host ps -auxww | grep '^ora10gr1.*ora_p00._ora10g'

ora10gr1 3411 35.5 0.5 1129068 12200 ? S 13:27 0:02 ora_p000_ora10gr1

ora10gr1 3413 28.0 0.5 1129064 12196 ? S 13:27 0:01 ora_p001_ora10gr1

ora10gr1 3415 26.0 0.5 1129064 12196 ? S 13:27 0:01 ora_p002_ora10gr1

ora10gr1 3417 23.3 0.5 1129044 12212 ? S 13:27 0:01 ora_p003_ora10gr1

ora10gr1 3419 19.5 0.5 1129040 12228 ? S 13:27 0:01 ora_p004_ora10gr1

ora10gr1 3421 19.1 0.5 1129056 12188 ? S 13:27 0:01 ora_p005_ora10gr1

ora10gr1 3423 19.0 0.5 1129056 12164 ? S 13:27 0:01 ora_p006_ora10gr1

ora10gr1 3425 21.6 0.5 1129048 12204 ? S 13:27 0:01 ora_p007_ora10gr1

可能看到,Oracle现在启动了8个并行执行服务器。如果你很好奇,想“看看”并行查询,使用两个会话就能很容易地满足你的好奇心。在运行并行查询的会话中,我们先来确定这个会话的SID

big_table@ORA10GR1> select sid from v$mystat where rownum = 1;

           SID

    ----------

           162

在另一个会话中,准备好要运行的查询:

ops$tkyte@ORA10GR1> select sid, qcsid, server#, degree

2    from v$px_session

3    where qcsid = 162

SID=162的会话中开始并行查询之后不久,返回到第二个会话,运行这个查询:

4    /

        SID QCSID   SERVER# DEGREE

---------- ----------         ----------      ----------

        145        162                    1                 8

        150        162                    2                 8

        147        162                    3                 8

        151        162                    4                 8

        146        162                    5                 8

        152        162                    6                 8

        143        162                    7                 8

        144        162                    8                 8

        162        162

9 rows selected.

在此可以看到,动态性能视图中的这9行(对应9个会话)中,并行查询会话(SID=162)的SID就是这9行的查询协调器SIDquery coordinator SID, QCSID)。现在,这个会话“正在协调”或控制着这些并行查询资源。可以看到每个会话都有自己的SID;实际上,每个会话都是一个单独的Oracle会话,执行并行查询时从V$SESSION中也可以看出这一点:

ops$tkyte@ORA10GR1> select sid, username, program

2     from v$session

3     where sid in ( select sid

4            from v$px_session

5            where qcsid = 162 )

6    /

 

           SID    USERNAME                   PROGRAM

    ----------    ------------------------------ -------------------------------

           143    BIG_TABLE                    oracle@dellpe (P005)

           144    BIG_TABLE                    oracle@dellpe (P002)

           145    BIG_TABLE                    oracle@dellpe (P006)

           146    BIG_TABLE                    oracle@dellpe (P004)

           147    BIG_TABLE                    oracle@dellpe (P003)

           150    BIG_TABLE                    oracle@dellpe (P001)

           151    BIG_TABLE                    oracle@dellpe (P000)

           153    BIG_TABLE                    oracle@dellpe (P007)

           162    BIG_TABLE                    sqlplus@dellpe (TNS V1-V3)

9 rows selected.

注意      如果你的系统中没有出现并行执行,就不要指望V$SESSION中会显示并行查询服务器。它们会在V$PROCESS中,但是除非真正使用,否则不会建立会话。并行执行服务器会连接到数据库,但是不会建立一个会话。关于会话和连接的区别,有关详细内容请参见第5章。

简而言之,并行查询就是这样工作的(实际上,一般的并行执行就以这种方式工作)。它要求一系列并行执行服务器同心协力地工作,生成子结果,这些子结果可以传送给其他并行执行服务器做进一步的处理,也可以传送给并行查询的协调器。

在这个特定的例子中,如所示的那样,BIG_TABLE分布在一个表空间的4个不同的设备上(这个表空间有4个数据文件)。实现并行执行时,通常“最好”将数据尽可能地分布在多个物理设备上。可以通过多种途径做到这一点:

q         跨磁盘使用RAID条纹(RAID striping);

q         使用ASM(利用其内置条纹);

q         使用分区将BIG_TABLE物理地隔离到多个磁盘上;

q         使用一个表空间中的多个数据文件,第二允许Oracle在多个文件中为BIG_TABLE段分配区段。

一般而言,如果能访问尽可能多的资源(CPU、内存和I/O),并行执行就能最好地发挥作用。但这并不是说:如果整个数据集都在一个磁盘上,就从并行查询得不到任何好处,并非如此;不过如果整个数据集都在一个磁盘上,可能确实不如使用多个磁盘那样能有更多收获。即使使用一个磁盘,在响应时间上也可能可以得到一定的速度提升,原因在于:给定的一个并行执行服务器在统计行时并不读取这些行,反之亦然。所以,与执行串行相比,两个并行执行服务器可以在更短的时间内完成所有行的统计。

类似地,即使在一台单CPU主机上,甚至也能得益于并行查询。一个串行SELECT COUNT(*)不太可能100%地完全占用单CPU主机上的CPU,其部分时间会用于执行(和等待)磁盘的物理I/O。通过并行查询,你就能充分利用主机上的资源(在这里就是CPUI/O),而不论是什么资源。

最后一点又把我们带回到先前引用的Practical Oracle8i: Building Efficient Databases中的那句话:并行查询本质上是不可扩缩的。如果在这台单CPU主机上利用两个并行执行服务器允许四个会话同时执行查询,你可能会发现,与串行处理相比,响应时间反而更长了。需要一个稀有资源的进程越多,满足所有请求所需的时间也越长。

而且要记住,并行查询需要保证两个前提。首先,你需要执行一个大任务,例如,一个长时间运行的查询,这个查询的运行时间要分钟、小时或天为单位来度量,而不是秒或次秒。这说明,并行查询不能作为典型OLTP系统的解决方案,因为在OLTP系统中,你不会执行长时间运行的任务。在这些系统上启用执行通常是灾难性的。其次,你需要有充足的空闲资源,如CPUI/O和内存。如果缺少任何一种资源,并行查询可能会过度使用资源,这就会负面地影响总体性能和运行时间。

过去,对于许多数据仓库来说,人们总认为必须应用并行查询,这只是因为过去(例如,1995年)数据仓库很稀少,通常只有一个很小、很集中的用户群。而在2005年的今天,到处都有数据仓库,而且数据仓库的用户群与事务性系统的用户群同样庞大。这说明,在给定时刻,你可能没有足够的空闲资源来启用这些系统上的并行查询。但这并不是说“这种情况下并行执行通常没有用”,而是说,并行执行更应该算是一个DBA工具(在“并行DDL“一节将介绍),而不是一个并行查询工具。

1.3   并行DML

Oracle文档将并行DMLPDML)一词的范围限制为只包括INSERTUPDATEDELETEMERGE(不像平常的DML那样还包括SELECT)。在PDML期间,Oracle可以使用多个并行执行服务器来执行INSERTUPDATEDELETEMERGE,而不是只利用一个串行进程。在一个有充足I/O带宽的多CPU主机上,对于大规模的DML操作,可能会得到很大的速度提升。

不过,不能把PDML当成提高OLTP应用速度的一个特性。如前所述,并行操作设计为要充分、完全地利用一台机器上的所有资源。通过这种设计,一个用户可以完全使用机器上的所有磁盘、CPU和内存。在某些数据仓库中(有大量数据,而用户很少),这可能正是你想要的。而在一个OLTP系统中(大量用户都在做很短、很快的事务),可能就不能希望如此了,你不想让用户能够完全占用机器资源。

这听上去好像有些矛盾:我们使用并行查询是为了扩缩,它怎么可能是不可扩缩的呢?不过要知道,应用到一个OLTP系统时,这种说法确实很正确。并行查询不能随着并发用户数的增加而扩展。并行查询设计为允许一个会话能像100个并发会话一样生成同样多的工作。但在OLTP系统中,我们确认不希望一个用户生成100个用户的工作。

PDML在大型数据仓库环境中很有用,它有利于大量数据的批量更新。类似于Oracle执行的分布式查询,PDML操作采用同样的方式执行,即每个并行执行服务器相当于一个单独数据库实例中的一个进程。表的每一部分(每一片)由一个单独的线程利用其自己的独立事务来修改(相应地,这个线程可能有自己的undo段)。这些事务都结束后,会执行一个相当于快速2PC的过程来提交这些单独的独立事务。图14-2展示了使用4个并行执行服务器的并行更新。每个并行执行服务器都有其自己独立的事务,这些事务要么都由PDML协调会话提交,要么无一提交。

实际上,我们可以观察到为并行执行服务器创建的各个独立事务。在此还是使用前面的两个会话。在SID=162的会话中,我们显式地启用了并行DML。在这方面,PDML有别于并行查询;除非显式地请求PDML,否则不能执行PDML

big_table@ORA10GR1> alter session enable parallel dml;

Session altered.

14-2    并行更新(PDML)示意图

这个表是“并行的“,但与并行查询不同,这对于PDML还不够。之所以要显式地在会话中启用PDML,原因是PDML存在一些相关的限制,我会在介绍完这个例子之后列出这些限制。

在这个会话中,下面执行一个批量UPDATE,由于表已经“启用并行DML“(enable parallel dml),所以实际上这会并行执行:

big_table@ORA10GR1> update big_table set status = 'done';

在另一个会话中,我们将V$SESSION联结到V$TRANSACTION,来显示对应这个PDML操作的活动会话,并显示这些会话的独立事务信息:

ops$tkyte@ORA10GR1> select a.sid, a.program, b.start_time, b.used_ublk,

2           b.xidusn ||'.'|| b.xidslot || '.' || b.xidsqn trans_id

3    from v$session a, v$transaction b

4    where a.taddr = b.addr

5           and a.sid in ( select sid

6                  from v$px_session

7                  where qcsid = 162 )

8    order by sid

9    /

 

   SID PROGRAM                                   START_TIME          USED_UBLK       TRANS_ID

    ---- ---------------------------------------    ------------------------   ------------------       ------------

   136 oracle@dellpe (P009)                08/03/05 14:28:17                 6256       18.9.37

   137 oracle@dellpe (P013)                08/03/05 14:28:17                 6369       21.39.225

   138 oracle@dellpe (P015)                08/03/05 14:28:17                 6799       24.16.1175

   139 oracle@dellpe (P008)                08/03/05 14:28:17                 6729       15.41.68

   140 oracle@dellpe (P014)                08/03/05 14:28:17                 6462       22.41.444

   141 oracle@dellpe (P012)                08/03/05 14:28:17                 6436       20.46.46

   142 oracle@dellpe (P010)                08/03/05 14:28:17                 6607       19.44.37

   143 oracle@dellpe (P007)                08/03/05 14:28:17                        1       17.12.46

   144 oracle@dellpe (P006)                08/03/05 14:28:17                        1       13.25.302

   145 oracle@dellpe (P003)                08/03/05 14:28:17                        1       1.21.1249

   146 oracle@dellpe (P002)                08/03/05 14:28:17                        1       14.42.49

   147 oracle@dellpe (P005)                08/03/05 14:28:17                        1       12.18.323

   150 oracle@dellpe (P011)                08/03/05 14:28:17                 6699       23.2.565

   151 oracle@dellpe (P004)                08/03/05 14:28:17                        1       16.26.46

   152 oracle@dellpe (P001)                08/03/05 14:28:17                        1       11.13.336

   153 oracle@dellpe (P000)                08/03/05 14:28:17                        1       2.29.1103

   162 sqlplus@dellpe (TNS V1-V3)    08/03/05 14:25:46                        2       3.13.2697

17 rows selected.

可以看到,与并行查询表相比,这里发生了更多事情。这个操作上有17个进程,而不像前面只有9个进程。这是因为:开发的计划中有一步是更新表,另外还有一些独立的步骤用于更新索引条目。通过查看从DBMS_XPLAN得到解释计划输出(我们对这个输出进行了编辑,去掉了尾部几列,以便这个输出能在一页的篇幅内放下),可以看到以下结果:

-------------------------------------------------------------------------------------

| Id | Operation                                                          | Name              |

-------------------------------------------------------------------------------------

| 0                                                                                | UPDATE STATEMENT          |       |

| 1                                                                                |   PX COORDINATOR          |       |

| 2                                                                                |     PX SEND QC (RANDOM)     | :TQ10001        |

| 3                                                            |       INDEX MAINTENANCE       | BIG_TABLE                                             |

| 4                                                                                |         PX RECEIVE          |       |

| 5                                                                                |           PX SEND RANGE       | :TQ10000        |

| 6                                                            |             UPDATE      | BIG_TABLE      |

| 7                                                                                |               PX BLOCK ITERATOR         |        |

| 8                                                                                |                 TABLE ACCESS FULL      | BIG_TABLE                                                               |

-------------------------------------------------------------------------------------

由于PDML采用的一种伪分布式的实现,因此存在一些限制:

q         PDML操作期间不支持触发器。在我看来,这是一个很合理的限制,因为触发器可能会向更新增加大量开销,而你使用PDML的本来目的是为了更快一些,这两方面是矛盾的,不能放在一起。

q         PDML期间,不支持某些声明方式的引用完整性约束,因为表中的每一片(部分)会在单独的会话中作为单独的事务进行修改。例如,PDML操作不支持自引用完整性。如果真的支持自引用完整性,可能会出现死锁和其他锁定问题。

q         在提交或回滚之前,不能访问用PDML修改的表。

q         PDML不支持高级复制(因为复制特性的实现要基于触发器)。

q         不支持延迟约束(也就是说,采用延迟模式的约束)。

q         如果表是分区的,PDML只可能在有位图索引或LOB列的表上执行,而且并行度取决于分区数。在这种情况下,无法在分区内并行执行一个操作,因为每个分区只有一个并行执行服务器来处理。

q         执行PDML时不支持分布式事务。

q         PDML不支持聚簇表。

如果违反了上述任何一个限制,就会发生下面的某种情况:语句会串行执行(完全不涉及并行化),或者会产生一个错误。例如,如果对表T执行PDML,然后在结束事务之前试图查询表T,就会收到一个错误。

1.4   并行DDL

我认为,Oracle并行就是中真正的“闪光点“就是并行DDL。我们讨论过,并行执行通常不适用于OLTP系统。实际上,对于许多数据仓库来说,并行查询也变得越来越没有意义。过去,数据仓库往往是为一个很小、很集中的用户群建立的,有时只包括一两个分析人员。不过,在过去的10年中,我发现,数据仓库的用户群已经从很小的规模发展为包括数百甚至上千位的用户。考虑这样一个数据仓库,它以一个基于Web的应用作为前端,按理讲,数千(甚至更多)的用户都可以即时地访问这个数据仓库。

但是如果是DBA执行大规模的批操作(可能在一个维护窗口中执行)则是另一码事。DBA只是一个人(而不是很多用户),他可能有一台能力很强的机器,有丰富的计算资源可用。DBA只有“一件事要做“:加载这个数据,重组表,再重建索引。如果没有并行执行,DBA将很难真正充分利用硬件的全部能力。但如果利用并行执行,则完全可以做到。以下SQL DDL命令允许”并行化“:

q         CREATE INDEX:多个并行执行服务器可以扫描表、对数据排序,并把有序的段写出到索引结构。

q         CREATE TABLE AS SELECT:执行SELECT的查询可以使用并行查询来执行,表加载本身可以并行完成。

q         ALTER INDEX REBUILD:索引结构可以并行重建。

q         ALTER TABLE MOVE:表可以并行移动。

q         ALTER TABLE SPLIT|COALESCE PARTITION:单个表分区可以并行地分解或合并。

q         ALTER INDEX SPLIT PARTITION:索引分区可以并行地分解。

4个命令还适用于单个的表/索引分区,也就是说,可以并行地MOVE一个表的单个分区。

对我来说,并行DDL才是Oracle中并行执行最突出的优点。当然,并行查询可以用来加快某些长时间运行的操作,但是从维护的观点看,以及从管理的角度来说,并行DDL才是影响我们(DBA和开发人员)的并行操作。如果认为并行查询主要是为最终用户设计的,那么并行DDL则是为DBA/开发人员设计的。

1.4.1             并行DDL和使用外部表的数据加载

Oracle9i中我最喜欢的新特性之一是外部表(external table),在数据加载方面外部表尤其有用。我们将在下一章详细地介绍数据加载和外部表。不过,作为一个简要说明,现在先简单地了解一下有关内容,以便讨论并行DDL对于确定区段大小和区段截断(extent trimming)有何影响。

利用外部表,我们可以很容易地执行并行直接路径加载,而无需太多的考虑。Oracle 7.1允许我们执行并行直接路径加载,即多个会话可以直接写至Oracle数据文件,而完全绕过缓冲区缓存,避开表数据的undo生成,可能还可以避免redo生成。这是通过SQL*Loader完成的。DBA必须编写多个SQL*Loader会话的脚本,手动地分解要加载的输入数据文件,确定并行度,并协调所有SQL*Loader进程。简单地说,尽管这是可以做到的,但很困难。

利用并行DDL,再加上外部表,就能通过一个简单的CREATE TABLE AS SELECT or INSERT /*+ APPEND */来实现并行直接路径加载。不用再编写脚本,不必再分解文件,也不用协调要运行的N个脚本。简单地说,通过结合并行DDL和外部表,不仅提供了纯粹的易用性,而且全无性能损失。

下面来看这方面的一个简单的例子。稍后将看到如果创建一个外部表。下一章还会更详细地介绍如果利用外部表实现数据加载。不过对现在来说,我们只是使用一个“真实“表(而不是外部表),由此加载另一个表,这类似于许多人在数据仓库中使用暂存表(staging table)加载数据的做法。简单地说,这些技术如下:

(1)         使用某个抽取、转换、加载(extract, transform, load, ETL)工具来创建输入文件。

(2)         将执行输入文件加载到暂存表。

(3)         使用对这些暂存表的查询加载一个新表。

还是使用前面的BIG_TABLE(启用了并行),其中包含10,000,000行记录。我们将把这个表联结到第二个表USER_INFO,其中包含ALL_USERS字典视图中与OWNER相关的信息。我们的目标是将这个信息逆规范化为一个平面结构。

首先创建USER_INFO表,启用并行操作,然后生成这个表的统计信息:

big_table@ORA10GR1> create table user_info as select * from all_users;

Table created.

 

big_table@ORA10GR1> alter table user_info parallel;

Table altered.

 

big_table@ORA10GR1> exec dbms_stats.gather_table_stats( user, 'USER_INFO' );

PL/SQL procedure successfully completed.

现在,我们想用这个信息采用并行直接路径加载来加载一个新表。这里使用的查询很简单:

create table new_table parallel

as

select a.*, b.user_id, b.created user_created

from big_table a, user_info b

where a.owner = b.username

Oracle 10g中,这个特定CREATE TABLE AS SELECT的计划如下所示:

--------------------------------------------------------------------------------------------------------------------------------

| Id   | Operation                                           | Name              | TQ        |IN-OUT     | PQ Distrib           |

--------------------------------------------------------------------------------------------------------------------------------

| 0    | CREATE TABLE STATEMENT      |                          |              |                  |                              |

| 1    |  PX COORDINATOR                      |                          |              |                  |                              |

| 2    |   PX SEND QC (RANDOM)          | :TQ10001       | Q1,01 | P->S        | QC (RAND)        |

| 3    |    LOAD AS SELECT                    |                          | Q1,01 | PCWP     |                              |

|* 4   |     HASH JOIN                               |                          | Q1,01 | PCWP     |                              |

| 5    |      PX RECEIVE                          |                          | Q1,01 | PCWP     |                              |

| 6    |       PX SEND BROADCAST    | :TQ10000       | Q1,00 | P->P        | BROADCAST    |

| 7    |        PX BLOCK ITERATOR     |                          | Q1,00 | PCWC     |                              |

| 8    |         TABLE ACCESS FULL    | USER_INFO | Q1,00 | PCWP     |                              |

| 9    |      PX BLOCK ITERATOR         |                          | Q1,01 | PCWC     |                              |

| 10 |       TABLE ACCESS FULL       | BIG_TABLE   | Q1,01 | PCWP     |                              |

--------------------------------------------------------------------------------------------------------------------------------

如果从第4步向下看,这些就是查询(SELECT)部分。BIG_TABLE的扫描和与USER_INFO的散列联结是并行执行的,并将各个子结果加载到表的某个部分中(第3步,LOAD AS SELECT)。每个并行执行服务器完成其联结和加载工作后,会把其结果发送给查询协调器。在这里,结果只是提示“成功“还是”失败“,因为工作已经执行。

仅此而已,利用并行直接路径加载,使得问题变得很容易。对于这些操作,最重要的是要考虑如何使用空间(或不使用)。有一种称为区段截断(extent trimming)的副作用相当重要,现在我要花一些时间来讨论这个内容。

1.4.2             并行DDL和区段截断

并行DDL依赖于直接路径操作。也就是说,数据不传递到缓冲区缓存以便以后写出;而是由一个操作(如CREATE TABLE AS SELECT)来创建新的区段,并直接写入这些区段,数据直接从查询写到磁盘(放在这些新分配的区段中)。每个并行执行服务器执行自己的部分CREATE TABLE AS SELECT工作,并且都会写至自己的区段。INSERT /*+ APPEND */(直接路径插入)会在一个段的HWM“之上“写,每个并行执行服务器再写至其自己的一组区段,而不会与其他并行执行服务器共享。因此,如果执行一个并行CREATE TABLE AS SELECT,并使用4个并行执行服务器来创建表,就至少有4个分区,可能还会更多。每个并行执行服务器会分配其自己的区段,向其写入,等填满时,再分配另一个新的区段,并行执行服务器不会使用由其他并行执行服务器非品牌的区段。

14-3显示了这个过程。这里由4个并行执行服务器执行一个CREATE TABLE NEW_TABLE AS SELECT。在图中,每个并行执行服务器分别用一种不同的颜色表示(白色、浅灰色、深灰色或黑色)。“磁鼓“中的方框表示这个CREATE TABLE语句在某个数据文件中创建的区段。每个区段分别用上述某个颜色表示,之所以能这样显示,原因很简单,任何给定区段中的所有数据都只会由这4个并行执行服务器中之一加载,在此显示出,P003创建并加载了其中4个区段。另一方面,P000则创建并加载了5个区段,等等。

14-3    并行DDL区段分配示意图

初看上去一切都很好。但是在数据仓库环境中,执行一个大规模的加载之后,这可能导致“过渡浪费“。假设你想加载1,010MB的数据(大约1GB),而且正在使用一个有100MB区段的表空间,你决定使用10个并行执行服务器来加载这个数据。每个并行执行服务器先分配其自己的100MB区段(总共会有10100MB的区段),并在其中填入数据。由于每个并行执行服务器都要加载101MB的数据,所以它会填满第一个区段,然后再继续分配另一个100MB的区段,但实际上只会使用这个区段中1MB的空间。现在就有了20区段,其中10个是满的,另外10个则不同,这10个区段中都各有1MB的数据,因此,总共会有990MB的空间是”已分配但未使用的“。下一次加载是可以使用这个空间,但是对现在来说,你就有了990MB的死空间。此时区段截断(extend trimming)就能派上用场了。Oracle会试图取每个并行执行服务器的最后一个区段,并将其”截断为“可能的最小大小。

1.      区段截断和字典管理表空间

如果使用传统的字典管理表空间,Oracle可以把只包含1MB数据的各个100MB区段转变或1MB的区段。遗憾的是,(在字典管理的表空间中)这会留下10个不连续的99MB空闲区段,因为你的分配机制采用的是100MB区段,所以这990MB空间就会用不上!下一次分配100MB时,往往无法使用现有的这些空间,因为现在的情况是:有99MB的空闲空间,接下来是1MB的已分配空间,然后又是99MB空闲空间,依此类推。在本书中,我们不再复习字典管理方法。

2.      区段截断和本地管理表空间

现在来看本地管理表空间。对此有两种类型:UNIFORM SIZE AUTOALLOCATEUNIFORM SIZE是指表空间中的每个区段大小总是完全相同;AUTOALLOCATE则表示Oracle会使用一种内部算法来确定每个区段应该是多大。这些方法都能很好地解决上述问题(即先有99MB空闲空间,其后是1MB已用空间,再后面又是99MB空闲空间,依此类推,以至于存在大量无法使用的空闲空间)。不过,这两种方法的解决策略截然不同。

UNIFORM SIZE方法完全排除了区段截断。如果使用UNIFORM SIZEOracle就不能执行区段截断。所有区段都只能有惟一一种大小,不能有任何区段小于(或大于)这个大小。

另一方面,AUTOALLOCATE区段支持区段截断,但是采用了一种聪明的方式。AUTOALLOCATE方法使用一些特定大小的区段,而且能使用不同大小的空间。也就是说,利用这种算法,一段时间后将允许使用表空间中的所有空闲空间。在字典管理的表空间中,如果请求一个100MB区段,倘若Oracle只找到了99MB的自由区段,请求还是会失败(尽管离要求只差了一点点)。与字典管理表空间不同,有AUTOALLOCATE区段的本地管理表空间可以更为灵活。为了试图使用所有空闲空间,它可以减小所请求的空间大小。

下面来看这种本地管理表空间方法之间的差别。为此,我们需要处理一个实际的例子。这里将建立一个外部表,它能用于并行直接路径加载(我们会频繁地执行并行直接路径加载)。即使你还在使用SQL*Loader来实现并行直接路径加载数据,这一节的内容也完全适用,只不过要求你自己编写脚本来执行数据的具体加载。因此,为了讨论区段截断,我们需要建立加载示例,然后在不同的条件下执行加载,并分析结果。

l          环境建立

首先需要一个外部表。我多次发现,加载数据时我常常用到SQL*Loader的一个遗留控制文件。例如,可能如下所示:

LOAD DATA

INFILE '/tmp/big_table.dat'

INTO TABLE big_table

REPLACE

FIELDS TERMINATED BY '|'

(

id ,owner ,object_name ,subobject_name ,object_id

,data_object_id ,object_type ,created ,last_ddl_time

,timestamp ,status ,temporary ,generated ,secondary

)

使用SQL*Loader就可以很容易地把这个文件转换为一个外部表定义:

$ sqlldr big_table/big_table big_table.ctl external_table=generate_only

SQL*Loader: Release 10.1.0.3.0 - Production on Mon Jul 11 14:16:20 2005

Copyright (c) 1982, 2004, Oracle. All rights reserved.

注意传递给SQL*Loader的参数EXTERNAL_TABLE。在这里,这个参数使得SQL*Loader不会加载数据,而是在日志文件中为我们生成一个CREATE TABLE语句。这个CREATE TABLE语句如下所示(在此有删减;为了让这个例子更简短,我对一些重复的元素进行了编辑):

CREATE TABLE "SYS_SQLLDR_X_EXT_BIG_TABLE"

(

"ID" NUMBER,

...

"SECONDARY" VARCHAR2(1)

)

ORGANIZATION external

(

                    TYPE oracle_loader

                    DEFAULT DIRECTORY SYS_SQLLDR_XT_TMPDIR_00000

                    ACCESS PARAMETERS

                    (

                           RECORDS DELIMITED BY NEWLINE CHARACTERSET WE8ISO8859P1

                           BADFILE 'SYS_SQLLDR_XT_TMPDIR_00000':'big_table.bad'

                           LOGFILE 'big_table.log_xt'

                           READSIZE 1048576

                           FIELDS TERMINATED BY "|" LDRTRIM

                           REJECT ROWS WITH ALL NULL FIELDS

                           (

                                   "ID" CHAR(255)

                                   TERMINATED BY "|",

                                   ....

                                   "SECONDARY" CHAR(255)

                                   TERMINATED BY "|"

                           )

                    )

                    location

                    (

                           'big_table.dat'

                    )

             )REJECT LIMIT UNLIMITED

我们所要做的只是给这个外部表起一个我们想要的名字:可能要改变目录等:

ops$tkyte@ORA10GR1> create or replace directory my_dir as '/tmp/'

2    /

Directory created.

在此之后,只需具体创建这个表:

ops$tkyte@ORA10GR1> CREATE TABLE "BIG_TABLE_ET"

2    (

3           "ID" NUMBER,

              ...

16        "SECONDARY" VARCHAR2(1)

17 )

18 ORGANIZATION external

19 (

20        TYPE oracle_loader

21        DEFAULT DIRECTORY MY_DIR

22        ACCESS PARAMETERS

23        (

24               RECORDS DELIMITED BY NEWLINE CHARACTERSET WE8ISO8859P1

25               READSIZE 1048576

26               FIELDS TERMINATED BY "|" LDRTRIM

27               REJECT ROWS WITH ALL NULL FIELDS

28        )

29        location

30        (

31               'big_table.dat'

32        )

33 )REJECT LIMIT UNLIMITED

34 /

Table created.

然后使这个表启用并行(PARALLEL)。这是很神奇的一步,正是这一步使我们可以很容易地执行并行直接路径加载:

ops$tkyte@ORA10GR1> alter table big_table_et PARALLEL;

Table altered.

注意      PARALLEL子句也可以在CREATE TABLE语句本身之上使用。也就是说,可以在REJECT LIMIT UNLIMITED后面增加关键字PARALLEL。我之所以使用ALTER语句,只是想让读者更注意这一点:外部表确实启用了并行。

l          使用UNIFORMAUTOALLOCATE本地管理表空间的区段截断

要建立加载组件,所要在的工作就这么多。现在,我们想分析在使用UNIFORM区段大小的本地管理表空间(locally-managed tablespaceLMT)中如果管理空间,以及在使用AUTOALLOCATE区段的LMT中如果管理空间,并对二者做一个比较。在这个例子中,我们将使用100MB的区段。首先创建LMT_UNIFORM,它使用统一的区段大小:

ops$tkyte@ORA10GR1> create tablespace lmt_uniform

2 datafile '/u03/ora10gr1/lmt_uniform.dbf' size 1048640K reuse

3 autoextend on next 100m

4 extent management local

5 uniform size 100m;

Tablespace created.

接下来,创建LMT_AUTO,它使用AUTOALLOCATE来确定区段大小:

ops$tkyte@ORA10GR1> create tablespace lmt_auto

2 datafile '/u03/ora10gr1/lmt_auto.dbf' size 1048640K reuse

3 autoextend on next 100m

4 extent management local

5 autoallocate;

Tablespace created.

每个表空间开始时有一个1BG的数据文件(另外LMT还要用额外的64KB来管理存储空间:如果使用32KB块大小,则另外需要128KB而不是64KB来管理存储空间)。我们允许这些数据文件一次自动扩展100MB。下面要加载这个文件:

$ ls -lag big_table.dat

-rw-rw-r-- 1 tkyte 1067107251 Jul 11 13:46 big_table.dat

这是一个有10,000,000条记录的文件。它使用big_table.sql脚本创建(这个脚本见本书开头的“环境配置“一节),再使用flat.sql脚本卸载(这个脚本可以从http://asktom.oracle.com/~tkyte /flat/index.html得到)。接下来,执行一个并行直接路径加载,将这个文件加载到各个表空间中:

ops$tkyte@ORA10GR1> create table uniform_test

2 parallel

3 tablespace lmt_uniform

4 as

5 select * from big_table_et;

Table created.

 

ops$tkyte@ORA10GR1> create table autoallocate_test

2 parallel

3 tablespace lmt_auto

4 as

5 select * from big_table_et;

Table created.

在我的系统上(有4CPU),使用了8个并行执行服务器和一个协调器来执行这些CREATE TABLE语句。运行这些语句时,可以通过查询与并行执行有关的一个动态性能视图V$PX_SESSION来验证这一点:

sys@ORA10GR1> select sid, serial#, qcsid, qcserial#, degree

2 from v$px_session;

 

           SID SERIAL# QCSID    QCSERIAL# DEGREE

    ----------      ---------- ----------              ----------      ----------

           137              17        154                    998                 8

           139              13        154                    998                 8

           141              17        154                    998                 8

           150            945        154                    998                 8

           161            836        154                    998                 8

           138                 8        154                    998                 8

           147              15        154                    998                 8

           143              41        154                    998                 8

           154            998        154

9 rows selected.

注意      创建UNIFORM_TESTAUTOALLOCATE_SET表时,我们只是在每个表上指定了“并行“(parallel),而由Oracle选择并行度。在这个例子中,我是这台机器的惟一用户(所有资源都可用),所以根据CPU数(4)和PARALLEL_THREADS_PER_CPU参数设置(默认为2),Oracle会将并行度默认为8

SIDSERIAL#是并行执行会话的标识符,QCSIDQCSERIAL#是并行执行查询协调器的标识符。因此,如果运行着8个并行执行会话,我们可能想看看空间是如果使用的。通过对USER_SEGMENTS的一个快速查询,就能清楚地知道:

ops$tkyte@ORA10GR1> select segment_name, blocks, extents

2 from user_segments

3 where segment_name in ( 'UNIFORM_TEST', 'AUTOALLOCATE_TEST' );

 

SEGMENT_NAME               BLOCKS     EXTENTS

---------------                               ----------          ----------

UNIFORM_TEST                    204800                   16

AUTOALLOCATE_TEST      145592                714

由于我们使用的块大小是8KB,这说明二者相差大约462MB,或者从比例来看,AUTOALLOCATE_TEST的已分配空间大约是UNIFORM_TEST70%。如果查看实际使用的空间:

ops$tkyte@ORA10GR1> exec show_space('UNIFORM_TEST' );

Free Blocks.............................                      59,224

Total Blocks............................                    204,800

Total Bytes.............................         1,677,721,600

Total MBytes............................                       1,600

Unused Blocks...........................                            0

Unused Bytes............................                             0

Last Used Ext FileId....................                          6

Last Used Ext BlockId...................                        9

Last Used Block.........................                  12,800

PL/SQL procedure successfully completed.

 

ops$tkyte@ORA10GR1> exec show_space('AUTOALLOCATE_TEST' );

Free Blocks.............................                              16

Total Blocks............................                    145,592

Total Bytes.............................         1,192,689,664

Total MBytes............................                       1,137

Unused Blocks...........................                            0

Unused Bytes............................                             0

Last Used Ext FileId....................                          8

Last Used Ext BlockId...................                      41

Last Used Block.........................                            8

PL/SQL procedure successfully completed.

注意      SHOW_SPACE过程在本身开头的“环境配置“中已经介绍过。

可以看到,如果去掉UNIFORM_TESTfreelist所占的块数(59,224个空闲块),这两个表实际所占的空间几乎是一样的,不过,UNIFORM表空间总共需要的空间确实大多了。这完全归因于没有区段截断。如果查看UNIFORM_TEST,可以很清楚地看出:

ops$tkyte@ORA10GR1> select segment_name, extent_id, blocks

2 from user_extents where segment_name = 'UNIFORM_TEST';

 

SEGMENT_NAME     EXTENT_ID         BLOCKS

---------------                           ----------            ----------

UNIFORM_TEST                           0              12800

UNIFORM_TEST                           1              12800

UNIFORM_TEST                           2              12800

UNIFORM_TEST                           3              12800

UNIFORM_TEST                           4              12800

UNIFORM_TEST                           5              12800

UNIFORM_TEST                           6              12800

UNIFORM_TEST                           7              12800

UNIFORM_TEST                           8              12800

UNIFORM_TEST                           9              12800

UNIFORM_TEST                         10              12800

UNIFORM_TEST                         11              12800

UNIFORM_TEST                         12              12800

UNIFORM_TEST                         13              12800

UNIFORM_TEST                         14              12800

UNIFORM_TEST                         15              12800

16 rows selected.

每个区段的大小都是100MB。再来看AUTOALLOCATE_TEST,如果把所有714个区段都列出来就太浪费篇幅了,所以下面汇总起来看:

ops$tkyte@ORA10GR1> select segment_name, blocks, count(*)

2 from user_extents

3 where segment_name = 'AUTOALLOCATE_TEST'

4 group by segment_name, blocks

5 /

 

SEGMENT_NAME           BLOCKS       COUNT(*)

-----------------                         ----------            ----------

AUTOALLOCATE_TEST              8                  128

AUTOALLOCATE_TEST         128                  504

AUTOALLOCATE_TEST         240                       1

AUTOALLOCATE_TEST         392                       1

AUTOALLOCATE_TEST         512                       1

AUTOALLOCATE_TEST         656                       1

AUTOALLOCATE_TEST         752                       5

AUTOALLOCATE_TEST         768                       1

AUTOALLOCATE_TEST       1024                     72

9 rows selected.

对于使用AUTOALLOCATELMT,通常都是这样来分配空间。这里的8块、128块和1,024块的区段是“正常“区段:使用AUTOALLOCATE时,总能观察到这样一些区段。不过,余下的区段就不那么”正常“了,观察时不一定能看到。这是因为发生了区段截断。有些并行执行服务器完成了它们的那部分加载后,取其最后的8MB1,024块)区段,并对其截断,这就留下了一些多余的空间。这样,如果另外某个并行执行会话需要空间,就可以使用这部分多余的空间。陆续地,当另外这些并行执行会话处理完自己的加载时,也会截断其最后一个区段,而留下一些多余的空间。

应该使用哪种方法呢?如果你的目标是尽可能经常并行地执行直接路径加载,我建议你采用AUTOALLOCATE作为区段管理策略。诸如此类的并行直接路径操作不会使用对象HWM以下的已用空间,即不会用freelits上的空间。除非你还要对表执行一些传统路径插入,否则UNIFORM分配方法会在这些表中永久地保留一些从不使用的额外空闲空间。除非你能把UNIFORM LMT的区段大小定得更小,否则,过一段时间后,就会看到我所说的过度浪费的情况:而且要记住,这些空间与段有关,所以对表进行完全扫描时也必须扫描这些空间。

为了说明这一点,下面使用同样的输入再对这两个表执行另一个并行直接路径加载:

ops$tkyte@ORA10GR1> alter session enable parallel dml;

Session altered.

 

ops$tkyte@ORA10GR1> insert /*+ append */ into UNIFORM_TEST

2 select * from big_table_et;

10000000 rows created.

 

ops$tkyte@ORA10GR1> insert /*+ append */ into AUTOALLOCATE_TEST

2 select * from big_table_et;

10000000 rows created.

 

ops$tkyte@ORA10GR1> commit;

Commit complete.

在这个操作之后,比较两个表的空间利用情况,如下:

ops$tkyte@ORA10GR1> exec show_space( 'UNIFORM_TEST' );

Free Blocks.............................                   118,463

Total Blocks............................                    409,600

Total Bytes.............................         3,355,443,200

Total MBytes............................                       3,200

Unused Blocks...........................                            0

Unused Bytes............................                             0

Last Used Ext FileId....................                          6

Last Used Ext BlockId...................           281,609

Last Used Block.........................                  12,800

PL/SQL procedure successfully completed.

 

ops$tkyte@ORA10GR1> exec show_space( 'AUTOALLOCATE_TEST' );

Free Blocks.............................                              48

Total Blocks............................                    291,184

Total Bytes.............................         2,385,379,328

Total MBytes............................                       2,274

Unused Blocks...........................                            0

Unused Bytes............................                             0

Last Used Ext FileId....................                          8

Last Used Ext BlockId...................           140,025

Last Used Block.........................                            8

PL/SQL procedure successfully completed.

可以看到,随着我们使用并行直接路径操作向表UNIFORM_TEST加载越来越多的数据,过一段时间后,空间利用情况会变得越来越糟糕。对此,我们可能希望使用一个更小的统一区段大小,或者使用AUTOALLOCATE。一段时间后,AUTOALLOCATE也可能生成更多的区段,但是由于会发生区段截断,所以空间利用情况要好得多。

1.5   并行恢复

Oracle中还有另一种形式的并行执行,即能够执行并行恢复。并行恢复(parallel recovery)可以在实例级完成,使得软件、操作系统或一般系统失败后可以更快地执行所需的恢复。还可以在介质恢复中应用并行恢复(例如,从备份恢复)。这本书并不打算讨论与恢复有关的内容,所以在此只简单提一下,只要知道存在并行恢复就可以了。如果要进一步了解有关内容,建议阅读以下Oracle手册:

q         Oracle Backup and Recovery Basics,其中涵盖了有关并行介质恢复的信息。

q         Oracle Performance Tuning Guide,其中包括有关并行实例恢复的信息。

1.6   过程并行化

在此要讨论两种类型的过程并行化:

并行管道函数(parallel pipelined function),这是Oracle的一个特性。

DIY并行化“(DIY parallelism),这是指在你自己的应用上使用Oracle执行并行全表扫描所采用的技术。与其说DIY并行化是一种直接内建的Oracle中的特性,不如说是一种开发技术。

你可能经常看到,设计为串行执行的应用(一般是批处理应用)往往类似于以下过程:

Create procedure process_data

As

Begin

For x in ( select * from some_table )

       Perform complex process on X

       Update some other table, or insert the record somewhere else

End loop

end

在这种情况下,Oracle的并行查询或PDML没有什么帮助(在这里,实际上OracleSQL并行执行可能只会导致数据库占用更多的资源,而且花费更长的时间)。如果Oracle并行地执行简单的SELECT * FROM SOME_TABLE,可能不会提供任何显著的速度提升。如果Oracle在完成复杂的处理之后再并行地执行UPDATEINSERT,则没有任何好处(毕竟,这里只会UPDATE/INSERT一行)。

在此显然可以做这样一件事:完成复杂处理之后,对UPDATE/INSERT使用批量处理。不过,这样不会是运行时间减少50%(或更多),而通常这才是你的目标。别曲解我的意思,我是说:在此你可能想对修改实现批量处理,但是这样做并不会使处理速度提高2倍、2倍或更多倍。

现在,假设你晚上在一台4CPU的机器上运行这个过程,机器上只运行着这一个过程,而没有其他活动。你会观察到,此时只会不充分地使用这个系统上的一个CPU,而且根本没有用多少磁盘。不仅如此,这个过程的执行要花费数小时,随着增加更多的数据,每天需要的时间会越来越长。你需要把运行时间减少几倍,它的速度应该是现在的4倍或8倍才行,所以稍稍地改善百分之几只能算杯水车薪,是远远不够的。能你该怎么做呢?

对此有两种办法。一种方法是实现一个并行管道函数,Oracle会确定一个合适的并行度(这是推荐的做法)。Oracle会创建会话,进行协调,并运行这些会话,这与前面使用了CREATE TABLE AS SELECT OR INSERT /*+ APPEND */的并行DDL例子很相似。Oracle会为我们完全自动地实现并行直接路径加载。另一种方法是DIY并行化。下面几节将分别介绍这两种方法。

1.6.1             并行管道函数

还是用前面那个串行进程PROCESS_DATA,不过这一次让Oracle为我们并行地执行这个进程。为此,需要把这个例程“倒过来“。并非从某个表中选择行,处理这些行,再插入到另一个表,而是向另一个表中插入获取某些行并对其处理的结果。我们要删除循环最下面的INSERT,而代之以一个PIPE ROW子句。PIPE ROW子句允许这个PL/SQL例程生成表数据作为输出,这样我们就能从这个PL/SQL过程SELECT数据。原本处理数据的过程性PL/SQL例程实际上变成了一个表,我们获取并处理的行就是输出。其实这种情况在这本书中已经屡屡出现,每次执行以下语句时都是如此:

Select * from table(dbms_xplan.display);

这是一个PL/SQL例程,它要读PLAN_TABLE;重建输出,甚至会增加一些行;然后使用PIPE ROW输出这个数据,将其发回给客户。我们在这里实际上要做同样的事情,只不过允许并行地处理。

这个例子中要使用两个表:T1T2T1是先读的表,T2表用来移入这个信息。假设这是一种ETL过程,我们要运行这个过程获得每天的事务性数据,并将其转换,作为第二天的报告信息。我们要用的两个表如下:

ops$tkyte-ORA10G> create table t1

2    as

3    select object_id id, object_name text

4    from all_objects;

Table created.

 

ops$tkyte-ORA10G> begin

2           dbms_stats.set_table_stats

3           ( user, 'T1', numrows=>10000000,numblks=>100000 );

4    end;

5    /

PL/SQL procedure successfully completed.

 

ops$tkyte-ORA10G> create table t2

2    as

3    select t1.*, 0 session_id

4    from t1

5    where 1=0;

Table created.

这里使用DBMS_STATS来“骗过“优化器,让它以为输入表中有10,000,000行,而且占用了100,000个数据库块。在此我们想模拟一个大表。第二个表T2是第一个表的一个副本,只是在结构中增加了一个SESSION_ID列。这个列很有用,可以通过它具体”看到“发生了并行化。

接下来,需要建立管道函数返回的对象类型。对于我们正在转换的这个过程,对象类型只是其“输出“的一种结构化定义。在这个例子中,对象类型类似于T2

ops$tkyte-ORA10G> CREATE OR REPLACE TYPE t2_type

2 AS OBJECT (

3           id number,

4           text varchar2(30),

5           session_id number

6    )

7 /

Type created.

 

ops$tkyte-ORA10G> create or replace type t2_tab_type

2    as table of t2_type

3    /

Type created.

现在来看管道函数,这只是重写了原来的PROCESS_DATA过程。现在这个过程是一个生成行的函数。它接收数据作为输入,并在一个引用游标(ref cursor)中处理。这个函数返回一个T2_TAB_TYPE,这就是我们刚才创建的对象类型。这是一个PARALLEL_ENABLED(启用子并行)的管道函数。在此使用了分区(partition)子句,这就告诉Oracle:“以任何最合适的方式划分或分解数据。我们不需要对数据的顺序做任何假设。“

还可以在引用游标中对特定列使用散列或区间分区。这就要使用一个强类型化的引用游标,从而使编译器知道哪些列是可用的。根据所提供的散列,散列分区会向各个并行执行服务器发送同样多的行来进行处理。区间分区则基于分区键向各个并行执行服务器发送不重叠的数据区间。例如,如果在ID上执行区间分区,每个并行执行服务器可能会得到区间1…10001001…2000020001…30000等(该区间中的ID值)。

在此,我们只想划分数据。数据如何划分对于我们的处理并不重要,所以定义如下:

ops$tkyte-ORA10G> create or replace

2 function parallel_pipelined( l_cursor in sys_refcursor )

3 return t2_tab_type

4 pipelined

5 parallel_enable ( partition l_cursor by any )

我们想查看哪些行由哪个并行执行服务器处理,所以声明一个局部变量L_SESSION_ID,并从V$MYSTAT对其进行初始化:

6

7 is

8    l_session_id number;

9    l_rec t1%rowtype;

10 begin

11 select sid into l_session_id

12 from v$mystat

13 where rownum =1;

现在可以处理数据了。在此要获取一行(多多行,因为这里当然可以使用BULK COLLECT来实现对引用游标的批量处理),执行复杂的处理,并输出。引用游标处理完成数时,将关闭这个游标,并返回:

14 loop

15        fetch l_cursor into l_rec;

16        exit when l_cursor%notfound;

17        -- complex process here

18        pipe row(t2_type(l_rec.id,l_rec.text,l_session_id));

19 end loop;

20 close l_cursor;

21 return;

22 end;

23 /

Function created.

这样就创建了函数。我们准备并行地处理数据,让Oracle根据可用的资源来确定最合适的并行度:

ops$tkyte-ORA10G> alter session enable parallel dml;

Session altered.

 

ops$tkyte-ORA10G> insert /*+ append */

2 into t2(id,text,session_id)

3           select *

4           from table(parallel_pipelined

5           (CURSOR(select /*+ parallel(t1) */ *

6                  from t1 )

7           ))

8 /

48250 rows created.

 

ops$tkyte-ORA10G> commit;

Commit complete.

为了查看这里发生了什么,可以查询新插入的数据,并按SESSION_ID分组,先来看使用了多少个并行执行服务器,再看每个并行执行服务器处理了多少行:

ops$tkyte-ORA10G> select session_id, count(*)

2 from t2

3 group by session_id;

 

      SESSION_ID    COUNT(*)

                ----------          ----------

                       241              8040

                       246              8045

                       253              8042

                       254              8042

                       258              8040

                       260              8041

6 rows selected.

显然,对于这个并行操作的SELECT部分,我们使用了6个并行执行服务器,每个并行执行服务器处理了大约8,040记录。

可以看到,Oracle对我们的过程进行了并行化,但是为此需要从头重写原先的过程。从最初的实现(串行过程)到现在(可以并行执行的过程),这实在是一条漫长的道理。所以,尽管Oracle可以并行地处理我们的例程,但是并非所有例程都能编写为完成并行化。如果大规模重写你的过程不太可行,你可能会对下一种实现感兴趣:DIY并行化。

1.6.2             DIY并行化

假设与上一节一样,也有同样的一个简单的串行过程。重写这个过程实现让我们难以承受,这实在太费劲了,但是我们又希望并行地执行这个过程。该怎么做呢?我通常采用的方法是:使用rowid区间将表划分为多个不重叠的区间(但要覆盖整个表)。

从概念上讲,这与Oracle执行并行查询的做法很相似。考虑一个全表扫描,Oracle处理全表扫描时会提出某种方法将这个表划分为多个“小表“,每个小表分别由一个并行执行服务器处理。我们要用rowid区间来做同样的事情。在较早的版本中,Oracle的并行实现实际上使用了rowid区间。

我们要使用一个1,000,000含更多BIG_TABLE,因为这种技术最适用于有大量区段的大表,创建rowid区间所用的方法取决于区段边界。使用的区段越多,数据分布就越好。所以,创建1,000,000行的BIG_TABLE之后,我们将如下创建T2

big_table-ORA10G> create table t2

2 as

3 select object_id id, object_name text, 0 session_id

4 from big_table

5 where 1=0;

Table created.

这里将使用数据库内置的任务队列来并行处理我们的过程。我们将调度2数目的任务。每个认为都是对我们的过程稍加修改,只处理某个给定rowid区间中的行。

注意      Oracle 10g中,这种简单的工作可以使用调度工具完成,但是为了使这个例子与9i兼容,这里使用了任务队列。

为了高效地支持任务队列,我们要使用一个参数表,向任务传递输入:

big_table-ORA10G> create table job_parms

2 ( job number primary key,

3 lo_rid rowid,

4 hi_rid rowid

5 )

6 /

Table created.

这样一来,我们就能只向过程传入任务ID2再查询表来得到所要处理的rowid区间。再来看我们的过程。粗体显示的代码是新增的:

big_table-ORA10G> create or replace

2 procedure serial( p_job in number )

3 is

4        l_rec job_parms%rowtype;

5 begin

6        select * into l_rec

7        from job_parms

8        where job = p_job;

9

10         for x in ( select object_id id, object_name text

11                from big_table

12            where rowid between l_rec.lo_rid

13                  and l_rec.hi_rid )

14         loop

15                -- complex process here

16                insert into t2 (id, text, session_id )

17                values ( x.id, x.text, p_job );

18         end loop;

19

20         delete from job_parms where job = p_job;

21         commit;

22 end;

23 /

Procedure created.

可以看到,改动并不大。大多数新增的代码只是用于得到输入和要处理的rowid区间。对逻辑只有一处修改:就是在第12行和第13行增加了谓词。

下面来调度任务。在此使用一个相当复杂的查询,它使用分析函数来划分表,在这种情况下,第1926行上的最内层查询将数据划分为8组。第22行上的第一个求和用于计算块数的累计总计;第23行上的第二个求和得出总块数。如果将累计总和整除所需的“块大小“(chunk size,在这里就是总计大小除以8),可以创建覆盖相同数量数据的文件/块组。第828行上的查询按GRP找出最高和最低文件号以及块号,并返回不同的grp。这就建立了输入,可以把这个输入发送给DBMS_ROWID,创建Oracle所要的rowid。得到该输出,并使用DBMS_JOB提交一个任务来处理这个rowid区间:

big_table-ORA10G> declare

2           l_job number;

3 begin

4           for x in (

5                  select dbms_rowid.rowid_create

                             ( 1, data_object_id, lo_fno, lo_block, 0 ) min_rid,

6                          dbms_rowid.rowid_create

                             ( 1, data_object_id, hi_fno, hi_block, 10000 ) max_rid

7                  from (

8                          select distinct grp,

9                                   first_value(relative_fno)

                                        over (partition by grp order by relative_fno, block_id

10                                 rows between unbounded preceding and unbounded following) lo_fno,

11                                 first_value(block_id )

                                        over (partition by grp order by relative_fno, block_id

12                                 rows between unbounded preceding and unbounded following) lo_block,

13                                 last_value(relative_fno)

                                        over (partition by grp order by relative_fno, block_id

14                                 rows between unbounded preceding and unbounded following) hi_fno,

15                                 last_value(block_id+blocks-1)

                                        over (partition by grp order by relative_fno, block_id

16                                 rows between unbounded preceding and unbounded following) hi_block,

17                                 sum(blocks) over (partition by grp) sum_blocks

18                        from (

19                                 select relative_fno,

20                                            block_id,

21                                            blocks,

22                                            trunc( (sum(blocks) over (order by relative_fno, block_id)-0.01) /

23                                            (sum(blocks) over ()/8) ) grp

24                                 from dba_extents

25                                 where segment_name = upper('BIG_TABLE')

26                                            and owner = user order by block_id

27                        )

28                ),

29                (select data_object_id

                             from user_objects where object_name = upper('BIG_TABLE') )

30                )

31                loop

32                        dbms_job.submit( l_job, 'serial(JOB);' );

33                        insert into job_parms(job, lo_rid, hi_rid)

34                                 values ( l_job, x.min_rid, x.max_rid );

35                end loop;

36         end;

37 /

PL/SQL procedure successfully completed.

这个PL/SQL块会为我们调度8个任务(如果由于区段或空间大小不足,以至于无法将表划分为8个部分,调度的任务可能会少一些)。如下可以看到调度了多少任务以及这些任务的输入:

big_table-ORA10G> select * from job_parms;

 

       JOB      LO_RID                                     HI_RID

----------      ------------------                           ------------------

       172      AAAT7tAAEAAAAkpAAA       AAAT7tAAEAAABQICcQ

       173      AAAT7tAAEAAABQJAAA      AAAT7tAAEAAABwICcQ

       174      AAAT7tAAEAAABwJAAA      AAAT7tAAEAAACUICcQ

       175      AAAT7tAAEAAACUJAAA      AAAT7tAAEAAAC0ICcQ

       176      AAAT7tAAEAAAC0JAAA      AAAT7tAAEAAADMICcQ

       177      AAAT7tAAEAAADaJAAA      AAAT7tAAEAAAD6ICcQ

       178      AAAT7tAAEAAAD6JAAA      AAAT7tAAEAAAEaICcQ

       179      AAAT7tAAEAAAEaJAAA       AAAT7tAAEAAAF4ICcQ

8 rows selected.

 

big_table-ORA10G> commit;

Commit complete.

一旦提交就会开始处理我们的任务。在参数文件在我们将JOB_QUEUE_PROCESSES设置为8,所有8个任务都开始运行,并很快完成。结果如下:

big_table-ORA10G> select session_id, count(*)

2 from t2

3 group by session_id;

 

    SESSION_ID     COUNT(*)

              ----------     ----------

                     172     130055

                     173     130978

                     174     130925

                     175     129863

                     176     106154

                     177     140772

                     178     140778

                     179     90475

8 rows selected.

在这里,虽然不如Oracle内置并行化分布那么均匀,但是已经很不错了。如果还记得,前面你曾经看到过每个并行执行服务器处理了多少行,使用内置并行化时,行数彼此之间非常接近(只相差12)。在此,有一个任务只处理了90,475行,而另一个任务处理了多达140,778行,另外的大多数任务都处理了大约130,000行。

不过,假设你不想使用rowid处理,允许是因为查询不像SELECT * FROM T那么简单,而涉及联结和其他构造,这使得使用rowid是不切实际的。此时就可以使用某个表的主键。例如,假设你下把这个BIG_TABLE划分为10个部分,按主键并发地处理。使用内置NTILE分析函数就能很轻松地做到。这个过程相当简单:

big_table-ORA10G> select nt, min(id), max(id), count(*)

2 from (

3           select id, ntile(10) over (order by id) nt

4           from big_table

5 )

6 group by nt;

 

        NT     MIN(ID)    MAX(ID) COUNT(*)

         ----------    ----------    ----------    ----------big_table-ORA10G> select nt, min(id), max(id), count(*)

2 from (

3 select id, ntile(10) over (order by id) nt

4 from big_table

5 )

6 group by nt;

NT MIN(ID) MAX(ID) COUNT(*)

---------- ---------- ---------- ----------

1 1 100000 100000

2 100001 200000 100000

3 200001 300000 100000

4 300001 400000 100000

5 400001 500000 100000

6 500001 600000 100000

7 600001 700000 100000

8 700001 800000 100000

9 800001 900000 100000

10 900001 1000000 100000

10 rows selected.

 

           1                 1     100000     100000

           2     100001     200000     100000

           3     200001     300000     100000

           4     300001     400000     100000

           5     400001     500000     100000

           6     500001     600000     100000

           7     600001     700000     100000

           8     700001     800000     100000

           9     800001     900000     100000

         10     900001   1000000     100000

10 rows selected.

现在就有了10个互不重叠的主键区间,大小都一样,可以使用这些区间来实现如前所示的DBMS_JOB技术,从而并行化你的过程。

1.7   小结

在这一章中,我们分析了Oracle中并行执行的概念。首先我给出了一个类比,帮助你了解可以在哪里应用并行执行,以及何时应用;具体来讲,如果有长时间运行的语句或过程,而且有充足的可用资源,就可以使用并行执行。

然后我们介绍了Oracle如何使用并行化。首先讨论了并行查询,并说明了Oracle如何将大型串行操作(如全面扫描)分解为可以并发运行的小部分。接下来讨论了并行DMLPDML),并指出了与之相关的一大堆限制。

接下来,我们介绍了并行操作的闪光点:并行DDL。并行DDLDBA和开发人员可用的一种工具,可以很快地执行大规模的维护操作(通常在非高峰时间而且有足够可用的资源时执行)。随后简要地谈到Oracle提供了并行恢复,然后开始讨论过程并行化。在此我们了解了两种实现过程并行化的技术:一种是由Oracle完成,另一种是我们自己来完成。

如果从头开始设计一个过程,可以考虑将其设计为允许Oracle为我们完成并行化,这样随着将来资源的增减,就能很容易地改变并行度。不过,如果已经有一些代码,需要很快地“修正“为能够并行执行,则可以采用DIY并行化,对此我们也介绍了两种技术:使用rowid区间和使用主键区间,它们都使用DBMS_JOB在后台为我们并行地执行任务。


原创粉丝点击