游标概观
相信很多Delphi程序员都写过这样的代码:
...
begin
MyDataSet.Open;
MyDataSet.Frist;
while not ( MyDataSet.BOF or MyDataSet.EOF) do
begin
...
end;
MyDataSet.Close;
end;
...
很久以来,我们习惯了用这样的代码对数据库返回的数据进行逐行操作。在用客户端程序的代码打开数据集之前,我们把它当做是一个无序集合。不过,在需要时,我们在服务器端就可以直接以行操作形式处理数据集,这就是游标。
游标的的使用方法类似前面的Delphi代码,通常有如下四步:
一、声明游标:Declare Cursor,这一过程在前面的代码中没有体现出来。然而我们都知道,在使用一个数据集之前,我们总要定义它的种种属性,比如数据源、SQL语句、打开方式等等。在游标中,数据源一般是不用指定了(因为就在当前数据库中,MS SQL Server中也可以通过SQL语言来读取异构数据源)。不过我们要为它指定一个数据集,还可以为它指定各种打开方式的设置,比如是否允许写操作,是否可以随机读取等等。一般来说,数据库系统默认的游标是只读、单向、逐行读取的。
二、打开游标:Open Cursor,一个Delphi(或其它开发平台)的数据集组件,在指定了各种必要属性后,还一定要用一个打开指令(如前面的MyDataSet.Open)来打开它,才能得到我们所需要的数据集,对于游标,我们也一样需要一个Open指令来打开它,才能使用。
三、操作数据:这一步骤通常包括移动当前游标、读取当前数据、操作代码三部分。前面设置的游标属性,很大一部分是关于这里面所能进行什么样操作的。比如,对于双向游标,我们可以向前或向后一行,而随机游标,我们甚至可以以随机方式指定游标的操作位置,但最常见的游标,是单向、只读的那一种。而对于当前行数据,我们能常是通过定义一些变量来读取,或移动到适当位置再进行写操作,这一点和一般的开发工具是一样的。至于操作代码,这虽然不是SQL的强项,但一般的数据库系统也提供基本的过程化编码能力,可以让我们完成操作。
四、关闭结果集:Close Cursor 做事要有始有终,在Delphi中,如果忘了关闭自己打开的数据集,会带来很多麻烦,而在数据库系统中,如果打开一个游标没有关上(想想吧,游标操作是要对数据上锁的),如果有很多用户都在执行这段有问题的代码……所以,在游标的标准语法中,有明确的语句,用来关闭数据集,并释放所占用的各种资源。这一点更像是OO语言中的析构函数,比Delphi的数据集组件的Close方法要做的事情要多一些。
以上各个步骤,SQL标准都约定了相应的实现代码。但具体到各个DBMS平台的实现,却是大同小异。问题就在于这一点不同之处足以把人烦死。所以,我在这里不具体写出实现方法。读者完全可以查阅自己使用的系统所带的帮助文件,看看自己用的数据库是如何实现的游标,实现了哪些功能。
不合理的存在
我们可以看到,游标与SQL语言的其它部分有相当大的差别。它的实现和操作很复杂,而且由于要逐行操作,完成同样功能的情况下,它通常比集合操作要慢。差距会有多大呢?举一个极端的例子:我曾经试验用游标给一个表填充行号,结果执行了十二个小时都没有完,而同样的操作,用前面文章提到过的不等联接,只要不到三秒钟。我保证不是所有情况下差距都会有这么大,但这种情况的确存在。特别是当大量并发任务存在时,这种长时间的锁定是很危险的。复杂和低效,是游标的最大缺点,仅仅为这两点,就足以让我们对它抱有一种谨慎的态度。而且,一般来说,需要用游标进行的操作,都可以在客户端完成(能过所谓的宿主语言,Host Language)。
也许,在极端的关系模型拥护者看来,游标是一个丑恶的存在。在一个完美、优雅,以无序的集合来管理信息的体系中,我们为什么要安置一个以有序方式逐行操作信息的游标呢?然而,正如《龙枪编年史》中,伊斯塔城神圣的帕拉丁神殿,却游荡着有史以来最黑暗的黑衣法师费斯坦但提勒斯,在庞大严谨的关系数据库中,有游标这样的另类存在。这是创造关系世界的众神之旨意,自有其道理。
存在即合理
游标虽有如此的缺憾,但它也有存在的价值。首先,当需要有序操作的数据集很大时,特别是最终的运算结果相对很小时,如果还要发到前台做,对网络资源的浪费就太大了。而且,一个很大的数据集传过来,宿主语言也不一定能支持这么庞大的数据结构(比如Delphi的VCL容器在这方面就倍受指责),这一点也限制了我们用宿主语言来扩充系统功能 (比如MS SQL Server和Interbase本来可以写扩展函数和扩展存储过程)。另外,如果要大量的逐行的写操作,与前台交互通常效率更低。游标的确不是好方法,但没有更好的方法时,它就是最好的方法。再就是以我的经验,以脚本写就的游标要比宿主语言编译后的二制代码的可维护性和可调试性要强。
俗世之中,是没有绝对的黑暗和光明的。关键,在于我们是否正确的利用它。
正确使用游标
游标本身没有所谓的对错,但在使用它时,我们应先三思而后行。
很多时候,游标未必是你想像中的唯一方法。我见过太多的游标脚本,本来都可以用更简洁高效的结构化操作完成。只要简单语句可以达到同样的效果,就不要用游标。《程序员》上读到过一句话:Simple is smart。这是软件开发的真理。
游标中,显然只读、单向的游标速度最快,而且也不容易造成死锁,尽可能用它吧。
在游标使用的表上,建立适当的索引,这么做带来的效率提高会比一般的SQL语句更明显,尤其是执行写操作的游标。
游标操作的结果集,要尽可能的小。
如果游标代码中有大量的运算,那么考虑是不是把它分散开,放到其它服务器或客户端。
对游标代码要进行充分的测试和验证,再投入使用,尤其是优化程序和稳定性。这方面不能相信系统。比如你写一个游标,每读一行把一个变量累加一,系统永远也不会主动把它优化成count(*)。
有些系统可以把当前事务打开的游标保持到以后的事务中,直到显示地关闭它。不过最好不要随便使用它。这个功能当然看起来很酷,不过滥用它会给我们带来无穷无尽的麻烦。你真的需要这种功能吗?
适当的时候,把它写成扩展存储过程或扩展存储函数,以二进制代码的形式链接进数据库系统。这样做的缺点是失去了灵活性,换来的是效率的提升。
附:
以前一直不会在Interbase的ISQL中输入多条成批执行的语句,所以什么存储过程、触发器、甚至于游标,都建不起来。直到有一天,注意了一下,发现在ISQL中是这样做的:
set term^;
...
^
...
^
...
set term;^
从第一行set term^;开始,ISQL会把用^分隔开的语句成组地发至后台执行,直至set term;^为止。有点像MS SQL Server的查询分析器的“GO”。这样,我们就可以自如地用ISQL编写脚本了。