您的位置:首页 > 数据库 > Oracle

学习《Oracle 9i10g编程艺术》的笔记 (四)

2009-11-27 10:21 393 查看
1.数据库独立性

将应用从数据库A 移植到数据库B 时,我时常遇到这种问题:应用在数据库A 上原本无懈可击,到了
数据库B 上却不能工作,或者表现得很离奇。看到这种情况,我们的第一个想法往往是,数据库B 是一个
“不好的”数据库。而真正的原因其实是数据库B 的工作方式完全不同。没有哪个数据库是错的或“不好
的”,它们只是有所不同而已。应当了解并理解它们如何工作,这对于处理这些问题有很大的帮助。将应用
从Oracle 移植到SQL Server 时,也会暴露SQL Server 的阻塞读和死锁问题,换句话说,不论从哪个方向
移植都可能存在问题。
例如,有人请我帮忙将一些Transact-SQL(T-SQL,SQL Server 的存储过程语言)转换为PL/SQL。
做这个转换的开发人员一直在抱怨Oracle 中SQL 查询返回的结果是“错的”。查询如下所示:
这个查询的目标是:在T 表中,如果不满足某个条件,则找出x 为NULL 的所有行;如果满足某个条
件,就找出x 等于某个特定值的所有行。
开发人员抱怨说,在Oracle 中,如果L_SOME_VARIABLE 未设置为一个特定的值(仍为NULL), 这个
查询居然不返回任何数据。但是在Sybase 或SQL Server 中不是这样的,查询会找到将x 设置为NULL 值的
所有行。从Sybase 或SQL Server 到Oracle 的转换中,几乎都能发现这个问题。SQL 采用一种三值逻辑来
操作,Oracle 则是按ANSI SQL 的要求来实现NULL 值。基于这些规则的要求,x 与NULL 的比较结果既不为
true 也不为false,也就是说,实际上,它是未知的(unknown)。从以下代码可以看出我的意思:
declare
l_some_variable varchar2(25);
begin
if ( some_condition )
then
l_some_variable := f( ... );
end if;
for C in ( select * from T where x = l_some_variable )
loop
...

第一次看到这些结果可能会被搞糊涂。这说明,在Oracle 中,NULL 与NULL 既不相等,也不完全不
相等。默认情况下,SQL Server 则不是这样处理;在SQL Server 和Sybase 中,NULL 就等于NULL。不能
说Oracle 的SQL 处理是错的,也不能说Sybase 或SQL Server 的处理不对,它们只是方式不同罢了。实际
上,所有这些数据库都符合ANSI,但是它们的具体做法还是有差异。有许多二义性、向后兼容性等问题需
要解决。例如, SQL Server 也支持ANSI 方法的NULL 比较,但这不是默认的方式(如果改成ANSI 方法的
NULL 比较,基于SQL Server 构建的数千个遗留应用就会出问题)。
在这种情况下,一种解决方案是编写以下查询:
不过,这又会带来另一个问题。在SQL Server 中,这个查询会使用x 上的索引。Oracle 中却不会这
样,因为B*树索引不会对一个完全为NULL 的项加索引(索引技术将在第12 章介绍)。因此,如果需要查
找NULL 值,B*树索引就没有什么用处。
这里,为了尽量减少对代码的影响,我们的做法是赋给x 某个值,不过这个值并没有实际意义。在此,
根据定义可知,x 的正常值是正数,所以可以选择–1。这样一来,查询就变成:
由此创建一个基于函数的索引:
只需做最少的修改,就能在Oracle 中得到与SQL Server 同样的结果。从这个例子可以总结出以下几
个要点:
数据库是不同的。在一个数据库上取得的经验也许可以部分应用于另一个数据库,但是你
ops$tkyte@ORA10G> select * from dual where null=null;
no rows selected
ops$tkyte@ORA10G> select * from dual where null <> null;
no rows selected
ops$tkyte@ORA10G> select * from dual where null is null;
D
-
X
select *
from t
where ( x = l_some_variable OR (x is null and l_some_variable is NULL ))
select * from t where nvl(x,-1) = nvl(l_some_variable,-1)
create index t_idx on t( nvl(x,-1) );

必须有心理准备,二者之间可能存在一些基本差别,可能还有一些细微的差别。
细微的差别(如对NULL 的处理)与基本差别(如并发控制机制)可能有同样显著的影响。
应当了解数据库,知道它是如何工作的,它的特性如何实现,这是解决这些问题的惟一途
径。
常有开发人员问我如何在数据库中做某件特定的事情(通常这样的问题一天不止一个),例如“如何
在一个存储过程中创建临时表?”对于这些问题,我并不直接回答,而是反过来问他们“你为什么想那么
做?”给我的回答常常是:“我们在SQL Server 中就是用存储过程创建临时表,所以在Oracle 中也要这么
做。”这不出我所料,所以我的回答很简单:“你根本不是想在Oracle 中用存储过程创建临时表,你只是以
为自己想那么做。”实际上,在Oracle 中这样做是很不好的。在Oracle 中,如果在存储过程中创建表,你
会发现存在以下问题:
DDL 操作会阻碍可扩缩性。
DDL 操作的速度往往不快。
DDL 操作会提交事务。
必须在所有存储过程中使用动态SQL 而不是静态SQL 来访问这个表。
PL/SQL 的动态SQL 没有静态SQL 速度快,或者说没有静态SQL 优化。
关键是,即使真的需要在Oracle 中创建临时表,你也不愿意像在SQL Server 中那样在过程中创建
临时表。你希望在Oracle 中能以最佳方式工作。反过来也一样,在Oracle 中,你会为所有用户创建一个
表来共享临时数据;但是从Oracle 移植到SQL Server 时,可能不希望这样做,这会影响SQL Server 的可
扩缩性和并发性。所有数据库创建得都不一样,它们存在很大的差异。

2.关于唯一键

许多数据库应用都有一个功能,即为每一行生成一个惟一的键。插入行时,系统应自动生成一
个键。为此,Oracle 实现了一个名为SEQUENCE 的数据库对象。Informix 有一个SERIAL 数据类型。Sybase
和SQL Server 有一个IDENTITY 类型。每个数据库都有一个解决办法。不过,不论从做法上讲,还是从输
出来看,各个数据库的方法都有所不同。所以,有见识的开发人员有两条路可走:
开发一个完全独立于数据库的方法来生成惟一的键。
在各个数据库中实现键时,提供不同的实现,并使用不同的技术。
从理论上讲,第一种方法的好处是从一个数据库转向另一个数据库时无需执行任何修改。我把它称为
“理论上” 的好处,这是因为这种实现实在太庞大了,所以这种方案根本不可行。要开发一个完全独立于
数据库的进程,你必须创建如下所示的一个表:
然后,为了得到一个新的键,必须执行以下代码:
ops$tkyte@ORA10G> create table id_table
2 ( id_name varchar2(30) primary key,
3 id_value number );
Table created.
ops$tkyte@ORA10G> insert into id_table values ( 'MY_KEY', 0 );
1 row created.
ops$tkyte@ORA10G> commit;
Commit complete.

看上去很简单,但是有以下结果(注意结果不止一项):
一次只能有一个用户处理事务行。需要更新这一行来递增计数器,这会导致程序必须串行
完成这个操作。在最好的情况下,一次只有一个人生成一个新的键值。
在Oracle 中(其他数据库中的行为可能有所不同),倘若隔离级别为SERIALIZABLE,除第
一个用户外,试图并发完成此操作的其他用户都会接到这样一个错误:“ORA-08177: can't
serialize access for this transaction”(ORA-08177:无法串行访问这个事务)。
例如,使用一个可串行化的事务(在J2EE 环境中比较常见,其中许多工具都自动将SERIALIZABLE
用作默认的隔离模式,但开发人员通常并不知道),你会观察到以下行为。注意SQL 提示符(使用SET
SQLPROMPT SQL*Plus 命令)包含了活动会话的有关信息:
ops$tkyte@ORA10G> update id_table
2 set id_value = id_value+1
3 where id_name = 'MY_KEY';
1 row updated.
ops$tkyte@ORA10G> select id_value
2 from id_table
3 where id_name = 'MY_KEY';
ID_VALUE
----------
1
OPS$TKYTE session(261,2586)> set transaction isolation level serializable;
Transaction set.
OPS$TKYTE session(261,2586)> update id_table
2 set id_value = id_value+1
3 where id_name = 'MY_KEY';
1 row updated.
下面,再到另一个SQL*Plus 会话完成同样的操作,并发地请求惟一的ID:
此时它会阻塞,因为一次只有一个事务可以更新这一行。这展示了第一种可能的结果,即这个会话会
阻塞,并等待该行提交。但是由于我们使用的是Oracle,而且隔离级别是SERIALIZABLE,提交第一个会话
的事务时会观察到以下行为:
第二个会话会立即显示以下错误:
OPS$TKYTE session(261,2586)> select id_value
2 from id_table
3 where id_name = 'MY_KEY';
ID_VALUE
----------
1
OPS$TKYTE session(271,1231)> set transaction isolation level serializable;
Transaction set.
OPS$TKYTE session(271,1231)> update id_table
2 set id_value = id_value+1
3 where id_name = 'MY_KEY';
OPS$TKYTE session(261,2586)> commit;
Commit complete.
OPS$TKYTE session(271,1231)> update id_table
2 set id_value = id_value+1
3 where id_name = 'MY_KEY';
update id_table
*
所以,尽管这个逻辑原本想做到独立于数据库,但它根本不是数据库独立的。取决于隔离级别,这
个逻辑甚至在单个数据库中都无法可靠地完成,更不用说跨数据库了!有时我们会阻塞并等待,但有时却
会得到一条错误消息。说得简单些,无论是哪种情况(等待很长时间,或者等待很长时间后得到一个错误),
都至少会让最终用户不高兴。
实际上,我们的事务比上面所列的要大得多,所以问题也更为复杂。实际的事务中包含多条语句,上
例中的UPDATE 和SELECT 只是其中的两条而已。我们还要用刚生成的这个键向表中插入行,并完成这个事
务所需的其他工作。这种串行化对于应用的扩缩是一个很大的制约因素。如果把这个技术用在处理订单的
网站上,而且使用这种方式来生成订单号,可以想想看可能带来的后果。这样一来,多用户并发性就会成
为泡影,我们不得不按顺序做所有事情。
对于这个问题,正确的解决方法是针对各个数据库使用最合适的代码。在Oracle 中,代码应该如下
(假设表T 需要所生成的主键):
其效果是为所插入的每一行自动地(而且透明地)指定一个惟一键。还有一种性能更优的方法:
也就是说,完全没有触发器的开销(这是我的首选方法)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: