首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

15.3  游标的类型和扩展的声明语法

游标有多种不同的类型(我们将在本章结束之前全部接触到)。默认的游标类型是只进的(在记录中只能前进,而不能后退)和只读的,但是,游标也能够是可滚动的和可更新的。另外,对于其他进程对底层数据的修改,它们有多种敏感性级别。

不仅对于SQL Server本身的游标引擎,而且对于我所遇到过的几乎所有的游标模型来说,默认的游标类型都是只进、只读游标。与其他游标类型比较而言,这种游标的开销非常小,并且,由于它们列举数据所能达到的速度,通常把这种游标称为“流水”游标。就像流水模式一样,它知道如何只以一个方向来堆放数据(你不可能使水龙带里的水倒流,难道不是吗?)。在多数情况下,流水游标只是比其他基于游标的选项快,但是,别误以为它的性能超越了行集操作——流水游标比大多数等效的行集操作慢。

现在,让我们先来看一下更多关于游标的语法,然后,逐一讨论所有这些选项:

DECLARE <游标名> CURSOR

[LOCAL|GLOBAL]

[FORWARD_ONLY|SCROLL]

[STATIC|KEYSET|DYNAMIC|FAST_FORWARD]

[READ_ONLY|SCROLL_LOCKS|OPTIMISTIC]

[TYPE_WARNING]

FOR <SELECT 语句>

[FOR UPDATE [OF <列名>[,...n]]]

乍看之下,关于游标的语法真是很多,并且,在声明游标时,也的确要考虑许多事情(正如前面讲过,或许最重要的事情是顺着这一问题而来的——“这里确实需要使用游标吗?”)。好的一面是,这些选项中的某些意味着另一个,因此,一旦做出了决定,其他的选项也很快能够落实。

接下来一步一步地应用具体的语法,逐步了解与之相关的重要概念。

15.3.1  作用域

LOCAL与GLOBAL选项决定着游标的作用域,即什么连接和进程能够“看到”游标。对于大多数有作用域的项目来说,其作用域默认为更保守的方式,即默认为最小作用域(这种情况下默认将是LOCAL)。但是,SQL Server游标算是某种例外——实际上,它的默认作用域是GLOBAL。在深入讨论LOCAL与GLOBAL作用域方面的问题之前,最好先暂时离开一下话题,阐释一下这里所言的局部和全局的含义。

我们已经讨论了游标与其他事物的例外之处,即游标的默认作用域被设置为全局范围而非更保守的局部范围。但例外还不仅限于此。在SQL Server中,事物是全局的或局部的通常是指,它能够被所有的连接看见而非仅限于当前的连接。然而,对于游标声明的目的来说,是指当前连接中所有的进程(批处理、触发器、存储过程)是否能看见它,或者只是当前的进程能看见它。

游标的作用域如图15-1所示。

当前连接

 

整个SQL Server

 

当前连接

 

整个SQL Server

 

当前进程

 

当前进程

 

作用域外

 

作用域内

 

作用域外

 

作用域内

 

全局

 

局部

 

图  15-1

现在,考虑一下作用域的意思,并加以试验。

正如你可能想见的,默认作用域为全局范围,这既有好的一面,也有不好的一面。游标的作用域为全局的,这意味着能够在一个存储过程中创建游标,然后,从另一个不同的存储过程中引用它——不必传递对它的引用。但是,全局作用域不利的方面是,如果试图以同样的名字创建另一个游标,则将得到一个错误。

下面用一个简短的例子来试验。这里要做的是,创建一个能为我们产生游标的存储过程:

在该存储过程中要注意几件事情。首先,声明了用于存储的变量,以便为我们完成一些事情。第一个存储变量@Counter将只用来保存事物上的标号,这样我们就只需在几条记录间移动,而不必在整个记录集上移动。当我们在结果集中逐行检索时,第二个和第三个变量@OrderID和@CustomerID分别用于保存从查询中检索到的值。

其次,声明了实际的游标。注意,这里明确地设置了作用域。默认情况下,如果省略GLOBAL关键字,依然会得到全局作用域的游标。

不必非要以全局作用域为默认值。可以使用sp_dboption或ALTER DATABASE,将“default to local cursor”选项设置为True(如果想要回到以全局范围为默认作用域,将该选项设置为False即可)。

这恰好又是一个很好的例子,它说明了总是明确地声明所需要的选项是很有意义的——别指望默认值。设想一下,如果你依赖于默认的GLOBAL,可是,后来某人更改了系统中的该选项!或许很多人会说,“噢,绝对没人会修改那个选项。”错!人们为了解决别处的某些问题,在这里做修改只是一种“很小的改动”。在使用游标时,若依赖不明确的选项值,可能会在数周后遇到麻烦——到那时你已经完全忘记做了修改。

我们继续讲述,接下来打开游标,在数条记录中一步一步地浏览。然而要注意,没有关闭或释放游标——在离开存储过程时,我们就任由游标打开着并保持可用。

此时,我不由得想到老电影《迷失太空》(Lost in Space),机器人不断地喊着“危险Will Robinson!危险!”让游标乱七八糟地打开着将给你带来痛苦、挫折和极度的沮丧。

这里之所以这样做,是为了充分阐明作用域的概念,但是,对于这样的使用一定得加倍小心。在调用存储过程时,若没有意识到存储过程结束后不会把其自身处理妥当,这将是有危险的。如果没有在存储过程外面把游标清理干净(关闭并释放),那么,这种被遗弃的但依然处于活动状态的游标,会造成资源泄漏。另外,如果再次调用同一个存储过程(它会再次声明并打开游标,而这个游标已经存在了),将可能产生错误。

当我们声明游标用于输出时,在想要允许外部与游标进行交互的情形下,将会发现大量更显而易见且更好的选择。

现在,我们已经列举了数条记录,并证实了存储过程是可行的,然后,要退出存储过程(要记住,我们没有关闭或释放游标)。接下来,从存储过程外部引用游标:

来看看这里都发生了什么。

首先,执行存储过程。正如我们已经看到过的,存储过程创建了游标,随后列举了数行。然后,任由游标保持打开,退出存储过程。

接着,声明变量,声明的变量与存储过程中所声明的变量完全一样。为什么必须再次声明变量,但却不声明游标?因为只有游标是默认作用域为全局范围的事物。即是说,当存储过程离开作用域时,变量消失了——我们无法继续引用这些变量,否则将得到变量未定义的错误。因而必须重新声明它们。

接下来的代码结构看起来与存储过程中的几乎一样——我们又一次循环浏览以列举出数条记录。

最后,当证实了游标在存储过程之外依然是活动的这一观点后,我们要准备关闭并释放游标。只有在关闭了游标之后,才释放了游标中使用的结果集所占用的内存或tempdb空间,并且,只有在释放了游标之后,才释放了游标变量及查询定义所占用的内存。

现在,在系统中创建存储过程(如果尚未创建)并执行脚本。你将得到如下所示的结果:

那么,可以看出游标依然是打开的,存储过程外的循环能够在存储过程里的代码已经停止的地方继续拣选。

此刻,如果把存储过程里的游标声明为局部作用域,看看将发生什么:

看起来似乎上面的修改只是很小的改动,但是,当我们再次执行脚本时,会发现结果迥异:

当位于存储过程内时,一切都与以前一样。然而,当来到存储过程外,此时游标不再在其作用域内,因此无法再引用它,于是脚本的运行中出现了数个错误。在本章的后面,我们将了解,如何从创建局部作用域游标的存储过程外访问该游标。

通过本节的讲述,最应当明白的事情是,必须考虑游标的作用域。与其他使用DECLARE语句声明的事情相比,游标的作用方式有些不同。

15.3.2  可滚动性

就像本章中讨论的大多数概念一样,可滚动性适用于可能面对的几乎所有的游标模型。实际上,其概念非常简单:相比而言,能够在任何方向导航呢,还是限制为只能向前访问?默认为不可滚动的——只能向前访问。

1.只进

只进游标这个概念可以说是名副其实。由于只进是默认的方式,因此,在得知它是我们到现在为止唯一使用过的游标类型时,或许你不会太过惊讶。当使用只进游标时,唯一有效的导航选项是FETCH NEXT。在接着前往下一条记录之前,必须确保已经彻底使用完每一条记录,因为一旦前往下一条记录,则无法再回到之前访问过的记录上,除非关闭并重新打开游标。

2.可滚动

可滚动游标这个概念也是名如其实。在需要时,能够向前和向后“滚动”游标。如果你在使用某一种API(ODBC、OLE DB、DB-Lib),那么,依赖当前正处理的对象模型,通常能够径直导航到指定的记录。实际上,通过ADO和ADO.NET,甚至能够对数据进行重新排序和添加额外的筛选器。

滚动的基石是FETCH关键字。可以使用FETCH在游标中向前和向后滚动,也可以移动到指定的位置。对于FETCH而言,主要的参数是:

l    NEXT——移动到下一条记录;

l    PRIOR——移动到上一条记录;

l    FIRST——移动到第一条记录;

l    LAST——移动到最后一条记录。

在本章的后面,将更深入地探讨FETCH,但眼下,要意识到FETCH的存在,并明白FETCH是用来控制如何在游标集中导航的事物。

下面,用一个简明的例子来说明可滚动游标的概念。实际上,这里使用的不过是在本章前面用过的存储过程的一个小小的变种。

最大的不同是:

l    游标是以SCROLL选项来声明的;

l    在原先使用NEXT的地方增加了一个新的关键字——PRIOR;

l    这里会继续处理,在存储过程中关闭并释放游标,而不是使用外部的过程(在那里,完成对游标的清理)。

在结果中会出现有趣的事情。这里不需要奇特的测试脚本——直接执行它:

将看到订单值前后滚动:

正如你可以看出的,我们不仅能像前面那样,成功地向前导航游标,而且也能够向后导航。

只进游标无疑是这两种选项中效率更高的选择。考虑一下开销,如果游标是只进的,则SQL Server只需要了解下一条记录——按照链表的方式。在需要以其他方式配置游标的情况下,为了能够找出被请求的行,必须存储额外的信息。具体如何实现取决于所选择的游标类型。

一些游标类型是隐式可滚动的,另一些则不是。某些类型的游标对于数据的修改是敏感的,而一些则不是。在接下来的一节中,我们将对这些问题中的一部分进行讨论。

15.3.3  游标类型

通常,各种API把游标划分为四种类型:

l    静态游标;

l    由键集驱动的游标;

l    动态游标;

l    只进游标。

有时,这四种类型具体如何实现(以及如何称谓)将随着API和对象模型的不同而有些微的变化,但是,它们的本质几乎是一样的。

使这些游标类型不同的是,它们是否能够滚动,以及它们对于游标生命周期中发生在数据库上的改动是否是敏感的。我们已经了解了可滚动是什么意思,但是,就“敏感”一词来说,或许听上去更像是畅销书《男人来自火星,女人来自金星》中的内容,而非讲述编程的书中的词语。尽管如此,在选择游标类型时,敏感性是要考虑的一个相当关键的概念。

游标是否敏感,这是指在游标被打开后,它是否会注意到发生在数据库中的修改。另外,这也表明了一旦察觉到发生了改变,它要如何做。让我们来看关于这方面的两个最极端的版本——静态游标与动态游标。静态游标一旦创建,就绝对不会注意到对数据库的任何改变。然而,动态游标只要还处于打开状态,就能意识到对数据库所做的每一个修改(插入记录、删除、更新等等)。在讨论每一种游标类型时,将探究有关敏感性的问题。

1.静态游标

静态游标代表了时间上的一个“快照”。实际上,至少一个数据访问对象模型把它称为快照记录集,而非静态游标。

当创建静态游标时,整个记录集创建在了tempdb里的临时表中。在完成了对静态游标的创建后,没有任何人或任何事能够修改静态游标。即是说,它是固定不变的。在各种各样的对象模型中,一些将使你能够更新静态游标中的信息,而一些则不允许,但底线总是相同的:不能通过静态游标把更新写入到数据库中。

在更深入地探究这种游标类型前,我将告知你,在服务器端,需要使用静态游标的情形非常稀少。我并不是说这种情况不存在(它们的确存在),只不过这样的情形确实非常罕见。

当你打算在服务器端使用静态游标时,问自己如下的问题:

l    能够通过临时表来实现吗?

l    能够完全在客户端实现吗?

要记住,SQL Server把静态游标保存在tempdb里的私有表中。如果SQL Server无论如何都要使用临时表,为什么不由你亲自使用临时表呢?有时,临时表不能满足需要(记录操作而非行集操作)。不过,如果并不想实现基于记录的操作,只是要使用时间上的快照,那么,可以使用SELECT INTO创建你自己的临时表,这能够为你(和SQL Server)省下大量的开销。

如果工作在客户端—服务器架构中,通常,在客户端处理静态游标会更好些。通过把整个操作移至客户端,能极大地削减网络往返的次数。由于已经知道,对数据库的修改不会影响到游标(毕竟,这不就是首先选择静态游标的原因吗),因此,当已经创建了游标后,没有理由再让游标与服务器进行联系。

那么,来看一个静态游标的例子。在本例中,将涉及创建静态游标的概念,然后进行修改并查看会发生什么。在本章这一部分剩余的讲述中,当讨论每一种游标类型时,都会用到该例的变种。

首先,先创建一个用来测试的表,然后创建游标并使用它,以了解游标中有什么。

看看我们从以上的代码中得到了什么(注意,为了能更便利地把结果集与PRINT消息显示在一起,我把显示结果的方式切换到了“以文本格式显示结果”选项):

关于运行脚本的过程中发生了什么,有几件事情需要注意:

l    第一,尽管表上有结果集打开着,我们依然能够执行更新。在这种情况下,这是由于我们使用的是静态游标——一旦创建了游标,就与实际的记录分离开,并不再维持任何锁。

l    第二,虽然,无疑能看到确实在实际的表上发生了更新,但更新不会影响到游标中的数据。同样,这是由于一旦创建了游标,游标就有了它自己的生命——不再以任何方式与原始的数据发生关联。

l    第三,也许你注意到了,我们对FETCH关键字使用了一个新的参数——这一次,通过使用FETCH FIRST,回到了结果集的最前端。

2.由键集驱动的游标

当与游标一起谈论键集时,我们不是在讨论你本地的锁匠。实际上,这里讨论的是对一组数据的维护,这些数据在数据库中唯一标识全部的行。

由键集驱动的游标有如下一些要点:

l    它们要求在讨论中的表上存在唯一索引;

l    存储在tempdb中的只有键集,而非整个数据集;

l    它们能感知到对已经是键集一部分的行所做的修改,包括这些行已被删除的可能;

l    然而,它们对于在创建了游标之后加入的新行是不敏感的;

l    对于将对数据执行更新的游标,可以用键集游标作为它们的基础。

现在,已经知道了其名字为“键集”,并且,我也已说过键集唯一标识每一行,那么,或许你不会惊讶于这样的事情:必须有某种形式的唯一索引(通常是主键,但是,也可以是任何明确地定义为唯一的索引),以便从中创建键集。

键全部存储在tempdb里专用的表中。当想要在游标中寻找特定的行时,SQL Server使用键作为找回数据的方法。这里要留意的一点是,在发出FETCH时,基于键提取了实际的数据。关于这一点,最重要的部分是,当提取指定的行时,那个特定行中的数据是最新的。由键集驱动游标的缺点(或优点,这取决于想要用游标来做什么)是,要使用已经创建的键集进行查找。这意味着一旦创建了键集,所有的行都包含在了游标中。在创建了游标之后加入的任何行都不会被游标看见(即使这些加入的行满足SELECT语句中WHERE子句的条件)。根据你所选择的游标选项,可以通过游标操作对已经是游标一部分的行进行更新。

接下来,修改一下前面的脚本,以展示在使用由键集驱动的游标时的敏感性问题:

这里所做的修改并非十分显著。我们添加了必需的唯一索引。这里刚好选择了以主键作为所需的唯一索引,因为在我们要提取信息的表中,它最为合适,但是,也可以使用并非主键的唯一索引。另外,还添加了一些东西用来插入数据行,这样就能够清楚地观察到键集看不见该行。

或许,在已做的修改中,最重要的事情是改变了最后在游标上运行的WHILE循环的条件。技术上讲,应该对两个循环都进行这样的修改,但是,对于本例中第一次进行的循环来说,不存在任何删除记录的风险,并且,我想要在同一个脚本中显示出一些差异。

做出这样的修改是为了处理新加入的一些事情——有可能会发现一条记录已经没有了。这很可能是有人删除了它。

那么,来看一下运行脚本后得到的结果:

看看这里的一些重要之处。

一开始,与以前几乎相同。像上一次一样,在第一个结果集中,我们看到了同样的5条记录。然后,看到了几条额外的“affected by”(受影响)消息——这是由于我们添加了INSERT、UPDATE和DELETE语句。接下来是第二个结果集。从这里开始,事情变得有趣了一些。

在这一接下来的结果集中,我们看到了UPDATE、INSERT和DELETE语句的实际结果。正如我们认为已经完成的那样,SalesOrderID 43664已经被删除,并且,插入了SalesOrderID为-99999的新的记录。这正是表中的数据,但是,在游标中,事情并没有这么轻松。

接下来的(和最后的)结果集说明,呈现在游标中的事情与重新运行查询的一些不同之处。运行这些语句时,碰巧得到了5条记录——就好像我们一开始运行SELECT语句显示出的实际表中的记录一样。但是,那完全是巧合。

事实上,在游标显示的记录和表显示的记录之间,有几个关键的不同之处。第一点很明显——实际上,结果集知道有一条记录不见了。可以看到,游标继续显示键集中键的位置,只不过当它去查找数据时,数据已经不在那里了。@@FETCH_STATUS被设置为-2,我们能检测到它并报告这种情况。SELECT语句显示实际在那里的数据,对于曾经存在的数据没有任何印象。另一方面,INSERT对于游标来讲是个未知量。在创建游标时记录不在那里,因此游标对它的存在一无所知——它不会显示在我们的结果集中。

当需要感知到对数据的修改、但不必了解最新发生的所有插入时,键集游标能够很好地处理这种情况。另外,根据要找寻的结果集的性质以及键集的性质,键集游标能够极大减少必须复制和存储到tempdb中的数据量——这对于整个服务器的性能可能会产生良好的影响。

警告!!!如果在没有唯一索引的表上定义了类型为KEYSET的游标,那么,SQL Server将把游标隐式地转换为STATIC类型。或许,行为上发生了改变这一事实足以让你感觉不快,但是,事情还不会就此打住——它并不会告知你发生了这样事情。是的,默认情况下,关于这里所发生的转变,你不会得到任何警告。幸运的是,通过使用游标中的TYPE_WARNING选项,能够注意到这类事情。在本章后面,将看到对这一选项的简单讲述。

3.动态游标

“动态游标有什么特别的?”答案似乎显示易见:“它们是动态的!”

嗯,基本正确。但是由于动态游标不会主动地告知你对于底层数据的修改,因此要说,它们还不算是动态的。它们对于底层数据上的所有修改是敏感的,从这一点说,它们非常接近动态。当然,像生活中的大多数事情一样,有得必有失。

如果想要把插入的记录添加到游标中,这没有问题。如果想要让更新的记录在游标中也能够完全更新,这没有问题。如果想要删除的记录从游标的数据集中删除掉,这没有问题(虽然,由于不会像在键集游标类型中那样看到缺失的记录,因而实际上无法说出删除了某些东西)。然而,如果想要拥有并发性,这会有很大的问题(由于让数据集更长时间地打开着,因此,更容易与其他用户发生冲突)。如果想要拥有较低的开销,这会有很大的问题(实际上,每一个FETCH都会重新进行查询)。是的,动态游标能够使性能招致很大的损害,但是,生活就是这样有得必有失的,不是吗?

总的来说,通常应该避免使用动态游标。

那么,为什么要这样大肆宣传?为了理解动态游标可能产生的影响,必须对它们是如何工作的有一些了解。可以看到,使用动态游标时,每发出一次FETCH,实际上都要重建游标。是的,本质上,构成查询基础的SELECT语句,连同与之相关联的WHERE子句,它们会再次运行。考虑一下处理大型数据集的情形。在这种情况下,只会有一个想法——真糟糕。确实太糟糕了。

从我接触RDBMS开始,就知道了动态游标是非常影响性能的——但我发现并不总是这样。当底层表不是很大时,这看似尤为正确。稍加思考,你可能会想出为什么动态游标在原始速度上实际上能够略快一些。

关于是什么导致了这一结果,我猜是由于为键集游标使用了tempdb。虽然,为了处理动态游标,对于每一个FETCH都要做更多的工作,但是,再次查询的数据常常是完全位于缓存中(这取决于系统的大小和加载情况)。这就意味着,动态游标的大部分工作来自于RAM中。另一方面,键集游标存储在tempdb中,对于大多数系统而言,tempdb位于硬盘上(即是说,这会使速度慢很多)。

当表的大小变大时,服务器上有更多不同的流量,分配给SQL Server的内存会变小,越是这样,由键集驱动的游标就将比动态游标越有优势。此外,原始速度并非一切——实际上,还必须考虑并发性问题(在本章的后面将更详细地考查用于并发性的选项),在动态游标中并发性会很成问题。如果处理的是具有较小数据集的服务器端游标,考虑动态游标时也不要只算上速度。

接下来,对最后一个脚本只做一个修改(把KEYSET改为DYNAMIC),然后重新运行它:

得到的结果是:

得到的结果是:

前两个记录集与上一次得到的完全一样。当查看第三个(且是最后一个)结果集时,有了改变:

l    尽管我们删除了一条记录,但是,对于失败的提取没有任何指示(没有通知);

l    被更新的记录显示出了更新(就如同使用键集时一样);

l    插入的记录现在显示在了游标集中。

动态游标是所有游标中最敏感的。对底层数据所做的所有事情都会影响动态游标。动态游标的缺点是,它们会带来额外的并发性问题,并且,在处理较大的数据集时会给系统造成沉重的负担。

技术上讲,动态游标与键集游标不同,动态游标能够运行于没有唯一索引的表中。要不惜一切代价避免这种情况(在我看来,应该防止这样做并抛出错误)。在某种情况下,由于动态游标无法追踪它在游标集中的位置,它很可能导致死循环。要避免这种情况,最万无一失的方法是,要么不使用动态游标,要么只在真正有唯一索引的表上使用动态游标。

4.快进游标

快进游标中的“快”(从游标的观点来看——查询使所有游标都显得像蜗牛一样慢)是最重要的。快进游标是通常在只进游标上使用的“流水游标”的缩影。我始终用这一比拟来暗示数据向前涌出的方式——一旦取出,就无法再将它放回。简言之,数据像潮水般涌出。使用FAST_FORWARD游标时,打开游标,然后只处理数据而不做任何其他的事情,接下来向前移动,再往后释放游标(注意,我没有说关闭游标)。

现在,认为把这种类型称为一种游标“类型”是一种用词不当,这样说是没有错的。虽然,在几种不同的情况下,这种游标类型会自动转换成其他的游标类型,但是,由于这种游标中的成员是固定的,因此我认为它们更像是一种由键集驱动的游标——一旦确定了游标的成员,就不会有新的记录加入其中。被删除的行将显示为丢失的记录(@@FETCH_STATUS为-2)。尽管如此,要记住,如果游标被转换为了其他的类型(通过自动转换),它将呈现出新游标类型的行为。

这里比较糟糕的地方是,如果没有在游标的定义中加入TYPE_WARNING选项,则SQL Server不会告知你发生了转换。

正如前面说过的,在许多情况下,FAST_FORWARD游标会隐式地转换为另外的游标类型。下表中概述了这些转换:

条    件

转  换  为

底层查询需要创建临时表

静态

底层查询本质上是分布式的

键集

游标被声明为FOR UPDATE

动态

存在一个将转换为由键集驱动的条件,但是,至少一个底层表上没有唯一索引

静态

听说在其他一些情况下也会发生游标转换,但是,我没有见到过任何相关的文档,并且,我自己没有遇到过这种情况。

如果你发现遇到了对于所有计算机相关领域来说最可怕的情形(不明确的结果),那么,可以使用sp_describe_cursor(一个系统存储过程)来列出游标当前的所有激活选项。

值得注意的是,所有的FAST_FORWARD游标本质上是只读的。你能够明确地将这种游标设置为具有FOR UPDATE选项,但是,正如前面的隐式转换表中所暗示的,这种游标将被隐式地转换为动态的。

那么,与声明为FORWARD_ONLY的游标相比,FAST_FORWARD游标到底具有什么不同之处呢?FAST_FORWARD游标将至少使用两种招数中的一种,在如下的方面有所帮助:

l    第一是预先提取数据。即是说,在打开游标的同时,将自动提取第一行——意味着如果是在使用ODBC的客户端—服务器环境中,这样做能省掉一次到服务器的往返。遗憾的是,这仅在ODBC下有用。

l    第二是一件确信无疑的事情——自动关闭游标。由于运行的是只进游标,因此,一旦到达了记录集的末尾,SQL Server可以假定你想要关闭游标。同样,这节省下了一次往返,并挤出了一点额外的性能。

构建游标时,对游标类型的选择是最至关重要的决定之一。在游标任务的实际输出中呈现出一点点差别的选择,在性能上可能大相径庭。在对修改的敏感性、并发性问题以及可更新上,能够看到其他的影响。

15.3.4  并发性选项

在事务和锁一章中,我们初次涉及了并发性问题。还记得吗,当两个或更多的进程试图在基本相同的时间访问同样的数据时,会涉及并发性问题。然而,在处理游标时,问题就变得更加麻烦。

问题是多方面的:

l    操作往往耗时更长(有更多的时间发生并发性问题);

l    提取数据时会读取每一行,但是,在你能进行更新前,某人可能试图编辑它;

l    你可以在结果集中向前或向后滚动,本质上,对于这样的操作在时间上没有限制(我希望你别这样做,但这样做确实是可以的)。

与所有的并发性问题一样,这种问题往往更容易出现在事务的环境中,而非出现在运行单语句的情形下。事务的持续时间越长,就越容易出现并发性问题。

为处理并发性问题,SQL Server给了我们三种不同的选项:

l    READ_ONLY;

l    SCROLL_LOCKS(在多数术语中,等同于Pessimistic);

l    OPTIMISTIC。

每一种选项都带来了它们特有的事情,因此,下面对它们逐一进行讨论。

1.READ_ONLY

在只读的情况下,不必担心游标是否将试图获取任何类型的更新锁或排他锁。同样,当你忙于修改自己的数据时,不必担心是否有其他人编辑了数据。这两者是事情变得相当的简单。

READ_ONLY正如它听上去那样。当选择该选项时,不能更新任何数据,但是,也完全跳过了大多数(但并非全部)并发性问题。

2.SCROLL_LOCKS

在多种API以及对象模型中,滚动锁更通常地等同于悲观锁定。在最简单的形式下,它意味着,只要你在编辑记录,就不允许任何人对其进行编辑。这期间具体的实现细节随下面两个方面而有所不同:

l    是否位于事务中;

l    设置的事务隔离级别是什么。

注意,这里看到的可能与我们在锁和事务一章中看到的有所不同。

使用更新锁,我们防止了其他用户更新数据。在事务的持续期间持有该锁。如果事务是一个单语句事务,那么,只有在所有受更新影响的行都完成后,才会释放锁。

滚动锁与更新锁几乎一样,只有一处不同——持有锁的持续时间不同。使用滚动锁时,根据游标是否参与多语句事务,将会有很大的不同。假定没有事务包围游标,则只在游标中当前的记录上持有锁——即是说,从首次提取该记录起直至提取下一条记录(或者到达结果集的末尾)为止。一旦来到下一条记录,前一条记录上的锁将被移除。

接下来,对在本章中大部分地方使用的脚本进行精简,并借其考查滚动锁:

由于在前面的代码块中只加入了一行,因而不会看到很多通常会看到的灰色(以显示在该行上做了修改)。其余的改动是对行进行了删除,因此,对于这些删除了的代码,我无法把它们显示为灰色以提醒你留意。如果你尝试运行这些代码,只需确保做了适当的修改即可。

这里所做的修改是为了去掉大多数要发生的事情,再次把注意力集中在游标上。可是,或许最需要注意的事情是那些我们故意忽略了的事情,如果在没有它们的情况下进行操作,通常会带来问题:

l    在游标上没有CLOSE,并且,这里也没有释放游标;

l    这里,在提取了第一行之后,甚至没有再进一步滚动游标。

我们让游标打开着是为了创造这样的情形:游标保持打开状态的时间足够长,以便能探讨有关锁的问题。此外,只提取第一行是为了确保当前有一个活动的行(这种方式能保证,在开始运行其他可能冲突的语句之前,不会到达数据集的末尾)。

这里想要做的是执行前面的代码,然后在AdventureWorks活动的情况下打开一个完全不同的连接窗口。接下来在新的连接窗口运行一个简单的测试:

如果你尚未领会本节讲述的内容,可能会对下面的结果感到有些惊奇:

根据我们已经了解的有关锁的知识(第12章),可能会认为前面的SELECT语句将被当前记录上的锁阻塞。但是,使用滚动锁时不会这样。锁只存在于当前在游标中的记录上,并且,或许更重要的是,锁只会阻止对记录的更新。任何SELECT语句(例如上面的语句)都能够毫无问题地看到游标中的内容。

现在,我们已经看到它是如何工作的,接下来返回原来的窗口并运行代码进行清理。这回归到了本章中大部分地方使用过的相同的代码上:

不要忘记运行上述的清理代码!!!如果忘记了,那么在终止连接前,在系统中将有一个打开的事务。当连接断开时,SQL Server会清理任何打开的事务(通过回滚这些事务),但是,我曾见到过这样的情形:在运行数据库一致性检查器(DBCC)时,发现有一些实在很老的事务——SQL Server遗漏了在其后进行清理。

3.乐观

乐观锁定创造了这样的情形:没有任何类型的滚动锁设置在游标上。这里的假设是,当进行更新时,依然希望人们能够获取数据。本质上,从你把数据提取到游标中到应用你的更新时,由于认为(或许,用希望一词会更好些)在这之间没有任何人会编辑你的数据,因此,这种方式是乐观的。

乐观不一定就不适当。如果有大量的记录并且没有太多的用户,那么,两个人试图在同一时间编辑同样的记录的几率会非常小(取决于业务流程的类型)。尽管如此,如果选择了乐观,则也必须为你是错误的这一可能性做好准备——即是说,在你执行提取和对数据库进行实际的更新之间,有人修改了数据。

如果恰好遇到了这样的问题,SQL Server将发出一个错误,该错误的@@ERROR为16394。当发生这种情况时,必须全部从游标中重新提取数据(这样才能知道做了什么修改),并且,要么回滚事务,要么尝试再次进行更新。

15.3.5  游标类型转换检测:TYPE_WARNING

这实在非常简单。如果在游标中添加该选项,那么,当在游标上发生了任何隐式转换时,你将会得到通知。如果不使用该语句,则不做任何通知就发生转换。如果转换并非预期的行为,那么,很可能会看到在所有计算机领域中都最为担心的事情(不可预知的结果)。

或许,通过一个例子能更好地理解这里的讲述,因此,我们回过头,对在本章中大部分地方使用过的游标再做些修改,并运行它。

在此处,我们将去掉用来为表创建键的代码。记得前面讲过,在没有唯一索引的表上,键集将被隐式地转换为静态游标:

此处并没有非常特殊之处。我认为这是一种完全的重写,只因为我们从原先的代码中删除了许多的东西,并且,从看见它以来已经有很长时间了。表和游标的创建与在本章很早的地方讲述键集游标时所做的事情几乎一样。主要的修改是移除了本例中不需要的代码,并在游标的声明中增加了TYPE_WARNING选项。

现在,得到了一些有趣的结果:

所有的事情毫无问题地运行着——我们只看到了一个完全是想作为警告的语句。如果转换了游标类型,那么结果可能并非你所希望的。

这样做的缺点是,只得到了一个发出的消息,但不会出现任何错误。从编程上讲,实际上,这里没有办法判断你收到了该消息——这使得该选项在生产环境中用处不大。尽管如此,当试图调试游标、以确定为什么它没有按照希望的方式运作时,该选项常常是非常便利的。

15.3.6  FOR <SELECT>

游标声明中的这部分是最本质的部分。即使在最基本的游标语法中,这部分也是必需的,因为它是确定应该把什么数据放入游标中的唯一的子句。

几乎所有的SELECT语句都是有效的——即使是包含ORDER BY子句的SELECT语句。只要SELECT语句提供单一的结果集,就没什么问题。可能产生问题的选项的例子是,任何汇总选项,如CUBE或ROLLUP等。

15.3.7  FOR UPDATE

默认情况下,任何可更新的游标是彻底可更新的——即如果能编辑一个列,那么所有的列都可被编辑。

FOR UPDATE <column list>选项允许指定游标中只有特定的列能被编辑。如果包含了该选项,则只允许更新在列的列表中的列。任何没有明确提及的列都被认为是只读的。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论