再上一篇:10.8嵌套表
上一篇:10.9临时表
主页
下一篇:10.11小结
再下一篇:11.1 Oracle索引概述
文章列表

10.10对象表

Oracle 9i 10g编程艺术:深入数据库体系结构

我们已经看到了一个不完整的对象表(带嵌套表)例子。对象表(object table)是基于一个TYPE 创建的表,而不是作为一个列集合。正常情况下,CREATE TABLE可能如下所示:

create table t ( x int, y date, z varchar2(25) ); 对象表创建语句则如下所示:

create table t of Some_Type;
T 的属性(列)由SOME_TYPE的定义得出。下面来简单地看一个例子,这里用到了几种类型,然后查 看所得到的数据结构:

ops$tkyte@ORA10G> create or replace type address_type
2 as object
3 ( city varchar2(30),
4 street varchar2(30),
5 state varchar2(2),
6 zip number
7 )
8 /
Type created.
ops$tkyte@ORA10G> create or replace type person_type
2 as object
3 ( name varchar2(30),
4 dob date,
5 home_address address_type,
6 work_address address_type
7 )
8 /
Type created.

ops$tkyte@ORA10G> create table people of person_type
2 /
Table created.
ops$tkyte@ORA10G> desc people
Name Null? Type
---------------------------------------- -------- ----------------------------

NAME

VARCHAR2(30)

DOB

DATE

HOME_ADDRESS

ADDRESS_TYPE

WORK_ADDRESS

ADDRESS_TYPE

就这么多。我们创建了一些类型定义,接下来可以创建这种类型的表。这个表有 4列,表示所创建的
PERSON_TYPE的4个属性。现在我们可以在这个对象表上执行DML来创建和查询数据了:

ops$tkyte@ORA10G> insert into people values ( 'Tom', '15-mar-1965',
2 address_type( 'Reston', '123 Main Street', 'Va', '45678' ),
3 address_type( 'Redwood', '1 Oracle Way', 'Ca', '23456' ) );
1 row created.
ops$tkyte@ORA10G> select * from people;
NAME DOB HOME_ADDRESS(CITY, S WORK_ADDRESS(CITY, S
----- --------- -------------------- -------------------- Tom 15-MAR-65 ADDRESS_TYPE('Reston ADDRESS_TYPE('Redwoo
', '123 Main Street' d', '1 Oracle Way',
, 'Va', 45678) 'Ca', 23456)
ops$tkyte@ORA10G> select name, p.home_address.city from people p; NAME HOME_ADDRESS.CITY
----- ------------------------------ Tom Reston
从 现在开始,可以看到处理对象类型必需的一些对象语法了。例如,在INSERT语句中,必须把 HOME_ADDRESS和WORK_ADDRESS用一个 CAST包装起来。我们将标量值强制转换为一个ADDRESS_TYPE。对 此也可以用另一种说法来解释,我们使用ADDRESS_TYPE对象的默认构 造函数为这一行创建了一个 ADDRESS_TYPE实例。
现 在,从外部来看这个表,表中只有4个列。由于我们已经了解到嵌套表内部有神奇的隐藏列,所 以可以猜测到,对象表可能也不会这么简单,也许还会做其他的事 情。Oracle把所有对象关系数据都存 储在普通的关系表中,最终还是存储在行和列中。如果挖掘“真正”的数据字典,可以看到这个表实际上 是什么样子:

ops$tkyte@ORA10G> select name, segcollength
2 from sys.col$
3 where obj# = ( select object_id
4 from user_objects
5 where object_name = 'PEOPLE' )
6 /
NAME SEGCOLLENGTH
------------------------- ------------ SYS_NC_OID$ 16
SYS_NC_ROWINFO$ 1
NAME 30
DOB 7
HOME_ADDRESS 1
SYS_NC00006$ 30
SYS_NC00007$ 30
SYS_NC00008$ 2
SYS_NC00009$ 22
WORK_ADDRESS 1
SYS_NC00011$ 30
SYS_NC00012$ 30
SYS_NC00013$ 2
SYS_NC00014$ 22
14 rows selected.
看上去这与DESCRIBE告诉我们的结果完全不同。显然,这个表中有14列,而不是4列。在这个例子 中,这些列分别是:
q SYS_NC_OID$:这是表中系统生成的对象ID。这是一个惟一的RAW(16)列。这个列上有一 个惟一约束,而且在这个列上还创建了一个相应的惟一索引。
q SYS_NC_ROWINFO$:这是嵌套表中已经研究过的那个“神奇”函数。如果从表中选择这个 列,它会把整行作为一列返回:
ops$tkyte@ORA10G> select sys_nc_rowinfo$ from people;
SYS_NC_ROWINFO$(NAME, DOB, HOME_ADDRESS(CITY,STREET,STATE,ZIP), WORK_ADDRESS
---------------------------------------------------------------------------- PERSON_TYPE('Tom', '15-MAR-65', ADDRESS_TYPE('Reston', '123 Main Street',
'Va', 45678), ADDRESS_TYPE('Redwood', '1 Oracle Way', 'Ca', 23456))

q NAME.DOB:这些是对象表的标量属性。与我们预期的一样,它们存储为常规的列。
q HOME_ADDRESS,WORK_ADDRESS:这些也是“神奇的”函数。它们把所表示的列集返回为一 个对象。这些不占用实际空间,只是为实际指示NULL或NOT NULL。
q SYS_NCnnnnn$: 这些是嵌入的对象类型的标量实现。由于PERSON_TYPE中嵌入了 ADDRESS_TYPE,Oracle需要留出空间将它们存储在适当类型的列中。系统生成的名字是必要的 , 因为列名必须惟一,我们很可能多次使用同一个对象类型(像这里一样),如果不采用系统生 成的名字,最后就完全有可能重复地使用相 同的名字,如有两个ZIP列。
所 以,就像嵌套表一样,这里完成了很多工作。首先增加了一个16字节的伪主键,而且为我们创建 了一个索引。关于如何为对象指定对象标识符的值,默认行为是可 以修改的,稍后将介绍。首先来看生成 这个表的完整SQL。同样,这是使用EXP/IMP生成的,因为我想轻松地看到依赖对象,包括重建这个对象 所需的全部 SQL。这是如下得到的:

[tkyte@localhost tkyte]$ exp userid=/ tables=people rows=n
Export: Release 10.1.0.3.0 - Production on Sun May 1 14:04:16 2005
Copyright (c) 1982, 2004, Oracle. All rights reserved.
Connected to: Oracle Database 10g Enterprise Edition Release 10.1.0.3.0 - Production
With the Partitioning, OLAP and Data Mining options
Export done in WE8ISO8859P1 character set and AL16UTF16 NCHAR character set
Note: table data (rows) will not be exported
About to export specified tables via Conventional Path ...
. . exporting table PEOPLE

Export terminated successfully without warnings.
[tkyte@localhost tkyte]$ imp userid=/ indexfile=people.sql full=y
Import: Release 10.1.0.3.0 - Production on Sun May 1 14:04:33 2005
Copyright (c) 1982, 2004, Oracle. All rights reserved.
Connected to: Oracle Database 10g Enterprise Edition Release 10.1.0.3.0 - Production
With the Partitioning, OLAP and Data Mining options
Export file created by EXPORT:V10.01.00 via conventional path
import done in WE8ISO8859P1 character set and AL16UTF16 NCHAR character set

Import terminated successfully without warnings. 分析people.sql文件,可以看到以下结果:

CREATE TABLE "OPS$TKYTE"."PEOPLE"

OF "PERSON_TYPE" OID 'F610318AC3D8981FE030007F01001464' OIDINDEX (PCTFREE 10 INITRANS 2 MAXTRANS 255

STORAGE(INITIAL 131072 NEXT 131072

MINEXTENTS 1 MAXEXTENTS 4096

PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1

BUFFER_POOL DEFAULT) TABLESPACE "USERS")

PCTFREE 10 PCTUSED 40

INITRANS 1 MAXTRANS 255

LOGGING STORAGE(INITIAL 131072 NEXT 131072

MINEXTENTS 1 MAXEXTENTS 4096

PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1

BUFFER_POOL DEFAULT) TABLESPACE "USERS" NOCOMPRESS

/

ALTER TABLE "OPS$TKYTE"."PEOPLE" MODIFY ("SYS_NC_OID$" DEFAULT SYS_OP_GUID())

/


由 此我们可以更深入地了解到这里到底发生了什么,现在明显地看到了OIDINDEX子句,而且看到了 对SYS_NC_OID$列的一个引用。这是这个表的隐 藏主键。函数SYS_OP_GUID与SYS_GUID相同,他们都返 回一个全局惟一的标识符,这是一个16字节的RAW字段。
OID’<big hex number>’语法在Oracle文档中没有相关说明。它的作用是在EXP和后续的IMP期间 , 确保底层类型PERSON_TYPE确实是相同的类型。这样当我们完成以下步骤时就能避免可能出现的错误:
(1) 创建PEOPLE表。
(2) 导出这个表。
(3) 删除这个表和底层PERSON_TYPE。
(4) 用不同的属性创建一个新的PERSON_TYPE。
(5) 导入原来的PEOPLE数据。 显然,原来导出的表无法导入到新结构中,因为结构不符。以上检查就能避免这种情况的出现。
如 果你还记得,我曾经提到过:为对象实例分配对象标识符的行为是可以修改的。可以不让系统为
我们生成一个伪主键,而是使用对象的自然键。最初这看上去有些弄 巧成拙——SYS_NC_OID$仍然出现在 SYS.COL$的表定义中,相对于系统生成的列来说,似乎这会占用更多的存储空间。不过,这里神奇再现。 如 果对象表所基于的是一个主键而不是系统生成的键,这个对象表的SYS_NC_OID$列就是虚拟列,并不占 用磁盘上的任何实际存储空间。
下面的例子显示了数据字典中发生了什么,并展示出SYS_NC_OID$没有占用物理存储空间。先来分析 系统生成的OID表:

ops$tkyte@ORA10G> create table people of person_type
2 /
Table created.
ops$tkyte@ORA10G> select name, type#, segcollength
2 from sys.col$
3 where obj# = ( select object_id
4 from user_objects
5 where object_name = 'PEOPLE' )

6 and name like 'SYS\_NC\_%' escape '\'
7 /
NAME TYPE# SEGCOLLENGTH
------------------------- ---------- ------------ SYS_NC_OID$ 23 16
SYS_NC_ROWINFO$ 121 1
ops$tkyte@ORA10G> insert into people(name)
2 select rownum from all_objects;
48217 rows created.
ops$tkyte@ORA10G> exec dbms_stats.gather_table_stats( user, 'PEOPLE' ); PL/SQL procedure successfully completed.
ops$tkyte@ORA10G> select table_name, avg_row_len from user_object_tables; TABLE_NAME AVG_ROW_LEN
------------------------------ -----------
PEOPLE 23
在此我们看到,行平均长度为23字节:16字节用于SYS_NC_OID$,7字节用于NAME。还是做同样的 事情,不过这一次使用NAME列上的一个主键作为对象标识符:

ops$tkyte@ORA10G> CREATE TABLE "PEOPLE"
2 OF "PERSON_TYPE"
3 ( constraint people_pk primary key(name) )
4 object identifier is PRIMARY KEY
5 /
Table created.
ops$tkyte@ORA10G> select name, type#, segcollength
2 from sys.col$

3 where obj# = ( select object_id
4 from user_objects
5 where object_name = 'PEOPLE' )
6 and name like 'SYS\_NC\_%' escape '\'
7 /
NAME TYPE# SEGCOLLENGTH
------------------------------ ---------- ------------ SYS_NC_OID$ 23 81
SYS_NC_ROWINFO$ 121 1
根据结果来看,现在SYS_NC_OID$列不是只有16字节,而变成一个81字节的大列!实际上,这里没 有存储任何数据,它是空的。系统会根据对象表、其底层类型和行本身中的值生成一个惟一 ID。如下所示 :

ops$tkyte@ORA10G> insert into people (name)
2 values ( 'Hello World!' );
1 row created.
ops$tkyte@ORA10G> select sys_nc_oid$ from people p;

SYS_NC_OID$

------------------------------------------------------------------------------- F610733A48F865F9E030007F0100149A00000017260100010001002900000000000C07001E01000

02A00078401FE000000140C48656C6C6F20576F726C642100000000000000000000000000000000

0000

ops$tkyte@ORA10G> select utl_raw.cast_to_raw( 'Hello World!' ) data
2 from dual; DATA
-------------------------------------------------------------------------------
48656C6C6F20576F726C6421
ops$tkyte@ORA10G> select utl_raw.cast_to_varchar2(sys_nc_oid$) data
2 from people; DATA
-------------------------------------------------------------------------------
<garbage bits and bytes..>Hello World!
如 果选择SYS_NC_OID$列,查看所插入串的HEX转储信息,可以看到行数据本身已经嵌入到对象ID 中。将对象ID转换为一个VARCHAR2,可以 更清楚地确认这一点。这是不是表示我们的数据要存储两次, 而且存在大量开销?不,并非如此,这正是神奇之处,只是在获取时才有这样的 SYS_NC_OID $列。Oracle 从表中选择SYS_NC_OID$时会合成数据。
下 面来表明我的观点。对象关系组件(嵌套表和对象表)就是我所说的“语法迷药”(syntactic sugar)。嵌套表和对象表总会转换为原来的关系行和列。我个人不把它们用作物理存储机制。”神奇之处 “太多了,而且它们的副作用并不是很清楚。你会得 到隐藏列、额外的索引、奇怪的伪列等。这并不是说 对象关系组件毫无用处,恰恰相反,我就经常在PL/SQL中使用这些组件。我会利用对象视图来得到对象关 系组件的功能。这么一来,不仅可以得到嵌套表结构的优点(同样能表示主表/明细表关系,但通过网络返 回(传输)的数据较少;概念上也更容易于使用,等 等),而且不存在任何物理存储问题。这是因为我可 以使用对象视图,从关系数据合成对象。这就解决了对象表/嵌套表的大多数问题,因为物理存储由我来指 定, 联结条件也由我建立,而且这些表可以很自然地用作关系表(这一点是许多第三方工具和应用所要求 的)。如果有人需要关系数据的对象视图,这是可以做到的;倘 若有人需要关系视图,也同样可以达到目 的。由于对象表实际上就是伪装的关系表,所以Oracle在底层为我们做的事情我们自己也可以做,而且还 能更高效地 完成工作,因为我们不必像对象表那样采用一般性的方式。例如,使用前面定义的类型,可以 很容易地使用以下命令创建对象视图:

ops$tkyte@ORA10G> create table people_tab
2 ( name varchar2(30) primary key,
3 dob date,
4 home_city varchar2(30),
5 home_street varchar2(30),
6 home_state varchar2(2),
7 home_zip number,
8 work_city varchar2(30),
9 work_street varchar2(30),
10 work_state varchar2(2),
11 work_zip number
12 )
13 /
Table created.
ops$tkyte@ORA10G> create view people of person_type
2 with object identifier (name)

3 as
4 select name, dob,
5 address_type(home_city,home_street,home_state,home_zip)
home_adress,
6 address_type(work_city,work_street,work_state,work_zip)
work_adress
7 from people_tab
8 /
View created.
ops$tkyte@ORA10G> insert into people values ( 'Tom', '15-mar-1965',
2 address_type( 'Reston', '123 Main Street', 'Va', '45678' ),
3 address_type( 'Redwood', '1 Oracle Way', 'Ca', '23456' ) );
1 row created. 无论如何,效果完全相同,我很清楚存储什么、如何存储,以及在哪里存储。对于更复杂的对象,可
能必须在对象视图上编写INSTEAD OF触发器,以允许通过视图修改数据。 对象表小结
Oracle中 的对象表用于实现对象关系模型。一个对象表一般会创建多个物理的数据库对象,并向模 式增加一些额外的列完成管理。对象表存在一些“神奇“的方面。利用对象 视图,你可以利用”对象“的 语法和语义,与此同时,还能对数据的物理存储有完全的控制,并允许对底层数据进行关系型访问。采用 这种方式,就能同时得到关系 世界和对象关系世界中最棒的特点。