使用数据库游标的好处是什么?

57

这是我所面临的面试问题。

非常简短的定义可以是

可用于操作查询返回的行。

除了使用游标(有关点列在MSDN上这里), 我有一个疑问,如果我们可以使用查询或存储过程执行所有操作(如果我没错,就像我们可以使用Transact-SQL进行MS-SQL),那么我们应该使用游标的任何具体要点吗?


3
Quassnoi的链接包含一个很好的摘要段落: “游标可以作为foreach的替代品,并为基于集合的语言(SQL)添加一些过程能力。对于不熟悉SQL的程序员来说,他们往往会滥用这个功能,因为他们不习惯SQL的集合范式,而是试图做他们所学的:打开循环,遍历它,对变量进行操作,关闭循环,测试、调试、检查并记录,然后下班回家。” - JsonStatham
5个回答

77
使用游标而非大结果集,就像使用视频流而非一次性下载视频,并在下载完成后观看。如果你下载,你需要几个G的空间和等待下载完成的耐心。现在,不管你的机器或网络有多快,每个人都是以相同的速度观看电影。
通常情况下,任何查询都会被发送到服务器,执行并在一次活动中将结果集发送到你那里。游标将为您提供访问数据行的权限,并且只有在请求时(实际查看它时)才会流式传输每一行。
游标可以节省您的时间-因为您不需要等待处理和下载完整的记录集。它将节省您的内存,既在服务器端又在客户端上,因为它们不必专门为结果集分配一个大块内存。平衡负载,同时工作在“突发”模式下通常更加有效,但它可能会完全阻塞您的服务器和网络。这种延迟很少对多用户环境有益。流式传输给其他操作留出了余地。
允许对查询的表进行操作(在某些条件下),这些操作不会直接影响您的游标。因此,在您持有一行游标时,其他进程可以读取、更新甚至删除其他行。这在非常繁忙的表格、许多并发读写时特别有帮助。
这就带来了一些警告,然而:
一致性:使用游标时,您(通常)不是在操作数据的一致快照上,而是在行上。因此,您的并发/一致性/隔离保证从整个数据库(ACID)降至一个行。通常情况下,您可以告知您的DBMS您想要什么级别的并发性,但如果您太挑剔(锁定您所在的完整表),您将扔掉服务器端的许多资源节省。
  • 将每行数据单独传输可能非常低效,因为每个数据包都有协商开销,您可以通过发送大块且可能压缩的数据来避免这种情况。 (没有DB服务器或客户端库会愚蠢到单独传输每一行,双方都有缓存和分块,但仍然相关)

  • 游标更难正确处理。考虑一个具有大结果集的查询,激励您使用游标,该查询使用带有聚合函数的GROUP BY子句。 (此类查询在数据仓库中很常见)。 GROUP BY可能会完全破坏您的服务器,因为它必须一次生成并存储整个结果集,甚至可能在其他表上持有锁。

  • 经验法则:

    • 如果您处理小而快速创建的结果集,请勿使用游标。
    • 游标在顺序性质的自由形式和复杂(引用)查询中表现优异,其具有大量结果集和较低一致性要求。

    “顺序性质”意味着您的查询中没有在重型GROUP BY子句中的聚合函数。 服务器可以懒惰地决定从高速缓存中计算供您的光标消耗的10行,并在此期间执行其他操作。

    希望对您有所帮助


    它将节省您的内存,无论是在服务器上还是在客户端上,因为它们不必将大块内存专用于结果集。如果我的数据不适合我的内存,我该怎么办?如果我必须以某种方式聚合它,为什么不使用SQL来做呢?如果我想显示所有结果,我仍然需要将其放入内存中。如果我想显示其中的一个子集-我可以使用SQL来获取子集。您能否提供一个真正需要使用游标的示例? - Ivan Virabyan
    4
    谢谢。听起来像Python中的生成器。 - Netro
    之前我一直以为游标(cursor)里面包含了数据,但实际上它只是指向内存中的数据,对吗? - Abhinav Chauhan
    @AbhinavChauhan:更好的说法是它指向表格/关系/结果集中的数据。 - AndreasT
    我喜欢这部分。非常有信息量。 - Willa

    31

    游标(cursor)是一种工具,允许您迭代一组记录。它具有“顺序”和“当前记录”的概念。

    一般来说,SQL 操作的是多集合(multiset):这些是无固定顺序可能重复的记录的集合,作为一个整体处理。

    比如,以下查询:

    SELECT  *
    FROM    a
    JOIN    b
    ON      b.a = a.id
    

    操作多重集合ab

    这个查询并不对记录的顺序、存储方式或访问顺序等做出任何假设。

    这允许抽象掉实现细节,让系统尝试选择最佳查询算法。

    然而,在你转换所有数据之后,最终你需要以一种有序的方式逐一访问记录。

    你并不关心电话簿条目在硬盘上的存储方式,但是打印机需要按字母顺序输入它们;并且格式化标记应该逐个应用于每个记录。

    这正是游标发挥作用的地方。每次在客户端处理结果集时,你都会使用一个游标。你不会从服务器获取几兆字节未排序的数据:你只获取一个微小的变量:一个结果集描述符,并且像这样写:

    while (!rs.EOF) {
       process(rs);
       rs.moveNext();
    }
    

    这就是实现所有这些的光标。

    当然,这涉及到数据库客户端的交互。

    至于数据库本身:数据库内部,你很少需要使用游标,因为正如我上面所说,几乎所有的数据转换都可以使用集合操作更有效地实现。

    但是,也有例外:

    • 分析操作SQL Server 中执行非常糟糕。例如,使用游标比使用基于集合的操作更高效地计算累积总和。
    • 以块为单位处理数据。有时需要将基于集合的操作顺序应用于集合的一部分,并且每个块的结果应该独立提交。虽然仍然可以使用基于集合的操作来完成此操作,但是使用游标通常更可取。
    • 在不支持递归的系统中进行递归

    您可能还会发现这篇文章值得一读:


    你的意思是客户端每次想要新记录都发送FETCH命令给服务器吗?这一定非常低效。为什么不一次性将所有结果加载到客户端呢? - Ivan Virabyan
    @IvanVirabyan:你会对这些数据做什么? - Quassnoi
    在大多数情况下,我会以某种方式显示它。 - Ivan Virabyan
    @IvanVirabyan:所有的100M行都在一个FETCH中吗? - Quassnoi
    我认为没有人需要显示一亿行数据。无论是逐行获取还是一次性获取,我都需要将数据适配到内存中才能显示出来。 - Ivan Virabyan
    @IvanVirabyan:没错,这就是游标存在的原因。 - Quassnoi

    4
    使用游标可以以编程方式顺序读取一组数据,因此它的行为类似于传统文件访问,而不是 SQL 的基于集合的行为特征。
    以下是可能使用游标的情况:
    1. 需要模拟基于文件的记录访问行为 - 例如,将关系型数据库用作以前用于数据存储的索引文件的代码的数据存储机制。 2. 需要按顺序处理数据 - 一个简单的例子可能是为特定客户计算运行总余额。(现在有许多关系型数据库,如 Oracle 和 SQLServer,具有分析扩展功能,应大大减少这种需求。)
    不可避免地,维基百科有更多信息:http://en.wikipedia.org/wiki/Database_cursor

    1

    使用游标可以一次访问一行。因此,当您想要操作大量行,但每次只操作一行时,最好使用它。

    在我的课程中,我被告知使用游标的原因是您想要访问超过您内存容量的多个行 - 因此您无法将所有行都收集到一个集合中然后循环遍历。


    我在课堂上被告知使用游标的原因是想要访问比内存容量更多的行,但是告诉你这种说法的人不应该当老师。 - user359040
    1
    为什么结果集不需要在内存中,或者我错了吗? - Hurda
    1
    这取决于您是指服务器(即数据库)还是客户端(即应用程序)内存。如果是前者,则该语句是无意义的,因为服务器必须保存游标的内容。如果是后者,则该语句有一定道理,尽管这种结果分页更可能是出于网络带宽或用户方便等原因,而不是因为客户端内存限制,现在这样做更为常见。 - user359040

    1
    有时候,基于集合的逻辑可能会变得非常复杂和难以理解。在这些情况下,如果性能不是问题,可以使用服务器端游标来替换关系型逻辑,从而将更易于维护的过程化逻辑(对于非关系型思考者而言)引入其中。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接