游标概观
置信很多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编写脚本了。