人们为什么如此讨厌SQL游标?[关闭]


127

我知道可以避免由于开销和不便而不得不使用游标,但是看起来有些严重的游标恐惧症正在使人们竭尽全力避免不得不使用游标。

例如,一个问题询问如何使用游标做明显的琐碎事情,以及使用带有递归自定义函数的通用表表达式(CTE)递归查询建议的可接受答案,即使这将可处理的行数限制为32 (由于sql server中的递归函数调用限制)。这使我震惊,这是系统使用寿命的一个糟糕解决方案,更不用说为避免使用简单的游标而付出的巨大努力。

如此疯狂的仇恨是什么原因?是否有一些“权威”对游标发布了裁决书?游标的心脏中是否有一些无法形容的邪恶潜伏,破坏了儿童或某些事物的道德?

维基问题,比代表对答案更感兴趣。

相关资料:

SQL Server快进游标

编辑:让我更加精确:我理解不应使用游标代替常规的关系操作;这是显而易见的。我不理解的是,即使游标是一种更简单和/或更有效的解决方案,人们也会竭尽全力地避免游标,例如他们拥有笨拙的东西。使我困惑的是非理性的仇恨,而不是明显的技术效率。


1
我认为您的Edit能够说明一切...在几乎所有情况下(我遇到过),都有一种方法可以将光标替换为性能更好的基于集合的情况。您说的很简单,但您会理解差异。
StingyJack

7
我喜欢这个问题上的标签!
08年

2
关于递归CTE限制的部分32是胡说八道。想必你是想递归触发器的和最大@@NESTLEVEL32。可以在查询中将其设置OPTION (MAXRECURSION N)为默认值,1000表示无限制。
马丁·史密斯

@MartinSmith:默认限制现在为100,最大限制为32K sql-server-helper.com/error-messages/msg-310.aspx
Steven A. Lowe

不,它仍然与我发表评论时以及在所有支持递归CTE的SQL Server版本中完全相同。如您的链接所述:“指定0时,不应用限制。”
马丁·史密斯

Answers:


74

带有游标的“开销”仅是API的一部分。游标是RDBMS的一部分在后台运行的方式。通常CREATE TABLEINSERTSELECT语句,并且实现是明显的内部游标实现。

使用更高级别的“基于集合的运算符”可将光标结果捆绑到一个结果集中,从而减少了往返API的次数。

游标早于提供一流收藏的现代语言。旧的C,COBOL,Fortran等必须一次处理一行,因为没有“收集”的概念可以广泛使用。Java,C#,Python等具有一流的列表结构来包含结果集。

慢的问题

在某些圈子中,关系联接是一个谜,人们将编写嵌套的游标而不是简单的联接。我已经看到了真正的史诗般的嵌套循环操作,写成很多游标。击败RDBMS优化。而且运行真的很慢。

简单的SQL重写可以用联接替换嵌套的游标循环,而单个平坦的游标循环可以使程序运行100次。[他们以为我是优化之神。我所做的就是用联接替换嵌套循环。仍在使用的游标。]

这种混乱常常导致对光标的指示。但是,问题不在于游标,而在于滥用游标。

尺寸问题

对于真正的史诗级结果集(即,将表转储到文件中),游标是必不可少的。基于集合的操作无法将非常大的结果集实现为内存中的单个集合。

备择方案

我尝试尽可能多地使用ORM层。但这有两个目的。首先,游标由ORM组件管理。其次,将SQL与应用程序分离成一个配置文件。并不是说光标不好。对所有这些打开,关闭和提取进行编码不是增值编程。


3
“光标是RDBMS在后台运行的方式。” 如果您是专门指SQL Server,那么很好,我对此一无所知。但是我已经研究了多个RDBMS(和ORDBMS)的内部结构(在Stonebraker的领导下),但没有一个这样做。例如:Ingres在内部使用等于“结果集”的元组。
理查德·T

@Richard T:我正在研究有关RDBMS源的二手信息;我将修改声明。
S.Lott

2
“我看到了真正的史诗般的嵌套循环操作,写成很多游标。” 我也一直看到他们。很难相信。
RussellH

41

游标使人们过度地将过程思维方式应用于基于集合的环境。

他们很

SQLTeam

请注意,游标是访问SQL Server内部数据的最慢方法。仅当您确实需要一次访问一行时,才应使用。我能想到的唯一原因是在每一行上调用一个存储过程。在“ 游标性能”一文中,我发现游标的速度比基于集合的游标慢30倍以上


6
那篇文章已有7年历史了,您认为与此同时也许情况有所改变?
史蒂文·劳

1
我还认为游标确实很慢,通常应避免使用。但是,如果OP提到我认为是的问题,那么游标就是那里的正确解决方案(由于内存限制,一次流记录一个)。
rmeador

更新的文章并未纠正相对速度测量,但确实提供了一些很好的优化和替代方法。请注意,原始文章说,游标比while循环快50倍,这很有趣
Steven A. Lowe

6
@BoltBait:我个人认为,如果您做出像这样的笼统的断言,您就不可能真正地45岁:-P
Steven A. Lowe

4
@BoltBait:你的孩子们从我的草坪上下来!
史蒂文·劳

19

上面有一个答案,其中说:“游标是访问SQL Server中数据的最慢方法……游标比基于设置的替代方法慢30倍以上。”

在许多情况下,此声明可能是正确的,但作为一个笼统的声明,这是有问题的。例如,当我想执行更新或删除操作而影响正在接受恒定生产读取的大表的许多行时,我充分利用了游标。运行一次每次更新一次的存储过程最终会比基于集合的操作快,这是因为基于集合的操作与读取操作发生冲突,并最终导致可怕的锁定问题(并且可能会完全杀死生产系统,在极端的情况下)。

在没有其他数据库活动的情况下,基于集合的操作通常会更快。在生产系统中,这取决于。


1
听起来像是证明规则的例外。
Joel Coehoorn's

6
@ [Joel Coehoorn]:我从不明白那句话。
史蒂文·劳

2
@ [Steven A. Lowe] pussy.org.uk/含义/exception-that-proves-the-rule.html将异常理解为“遗漏的内容”,并请注意此处的规则类似于“在大多数情况下,游标是坏”。
David Lay

1
@delm:感谢您的链接,现在我对这句话的理解甚至更少了!
Steven A. Lowe

5
@ [Steven A. Lowe]基本上是说,如果您用子案例“打破规则”,则必须有一条打破一般规则,因此存在一条规则。例如,来自链接:(“如果我们有类似“'星期天免费入场'之类的声明”,我们可以合理地假设一般情况下收取入场费。)
弗莱

9

开始使用SQL的开发人员倾向于在基于集合的操作会更好的地方使用游标。特别是当人们在学习了传统的编程语言之后学习SQL时,“遍历这些记录”的心态往往会导致人们不恰当地使用游标。

最严肃的SQL书籍中有一章禁止使用游标。精心编写的代码可以清楚地表明游标在其中,但不应将其用于基于集合的操作。

显然,在某些情况下,游标是正确的选择,或者至少是正确的选择。


9

当使用游标方法时,优化器通常无法使用关系代数来转换问题。通常,游标是解决问题的好方法,但是SQL是一种声明性语言,并且数据库中有很多信息,从约束到统计信息和索引,这意味着优化器有很多选项可以解决问题。问题,而游标几乎可以明确指示解决方案。


8

在Oracle PL / SQL中,游标不会导致表锁定,并且可以使用批量收集/批量获取。

在Oracle 10中,常用的隐式游标

  for x in (select ....) loop
    --do something 
  end loop;

一次隐式获取100行。明确的批量收集/批量提取也是可能的。

但是PL / SQL游标是不得已的方法,如果无法解决基于集合的SQL的问题,请使用它们。

另一个原因是并行化,数据库比基于行的命令式代码更容易并行化基于集合的大型语句。这也是函数式编程变得越来越流行的相同原因(Haskell,F#,Lisp,C#LINQ,MapReduce ...),函数式编程使并行化变得更加容易。每台计算机的CPU数量正在增加,因此并行化成为越来越多的问题。


6

通常,由于在关系数据库上,使用游标的代码性能比基于集合的操作差一个数量级。


您是否有基准或参考?我没有注意到任何这种急剧的性能下降……但是也许我的表没有足够的行来影响它(通常是一百万或更少)?
史蒂文·劳

哦,等等,我明白您的意思了-但是,我绝不提倡使用set操作中的游标,只是不要极端使用以避免游标
Steven A. Lowe

3
我记得我第一次做SQL时,我们不得不从大型机中每天导入一个50k的数据文件到SQL Server数据库中。当我更改为基于集合的操作时,该过程耗时20分钟。
Charles Bretana

6

上面的答案还没有足够强调锁定的重要性。我不是游标的忠实拥护者,因为它们通常会导致表级锁定。


1
是的,谢谢!如果没有防止它的选项(只读,仅转发等),它们肯定会一样,就像继续占用几行然后几页行的任何(SQL Server)操作一样。
史蒂文·劳

?? 这是您的锁定策略(而非游标)存在的问题。即使是SELECT语句也将添加读取锁。
亚当

3

值得一读的是,游标将在其“一个”位置执行其基于集合的对应位置,这是一个总计。在较小的表上,按列对行进行求和的总和的速度有利于基于集合的操作,但是随着表行大小的增加,游标将变得更快,因为它可以简单地将运行中的总值携带到下一个游标中循环。现在您应该进行连续总计的地方是一个不同的论点...


1
如果您是要“运行总计”某种聚合(最小,最大,总和),那么任何有能力的DBMS都会击败基于客户端,基于游标的解决方案,而仅仅是因为该功能是在引擎中执行的,并且没有客户端<->服务器开销。也许SQL Server没有能力?
理查德T

1
@ [Richard T]:我们正在讨论存储过程中的服务器端游标,而不是客户端游标;对困惑感到抱歉!
史蒂文·劳


2

除了性能问题外,我认为游标最大的失败是调试起来很痛苦。尤其是与大多数客户端应用程序中的代码相比,在这些应用程序中,调试往往相对容易,而语言功能则更加容易。实际上,我认为使用游标在SQL中执行的几乎任何操作都应该首先在客户端应用程序中发生。


2
即使没有游标,SQL也很难调试。Visual Studio中的MS SQL逐步工具似乎不喜欢我(它们挂了很多东西,或者根本没有跳断点),所以我通常只使用PRINT语句;-)
Steven A. Lowe

1

您可以张贴该光标示例或链接到该问题吗?可能有比递归CTE更好的方法。

除其他注释外,游标使用不当(经常)通常会导致不必要的页/行锁定。


1
还有一种更好的方法-一个怪胎的光标;-)
史蒂文·A·洛

1

您很可能在第二段之后就结束了您的问题,而不是仅仅因为他们的观点与您不同而称他们为“疯子”,否则试图嘲笑可能有很好的理由去感觉他们的行为的专业人士。

关于您的问题,尽管在某些情况下可能需要使用游标,但根据我的经验,开发人员决定比实际情况更频繁地使用游标“必须”使用FAR。我认为,有人过度使用游标而不是应该使用游标时,犯错的可能性要高得多。


8
汤姆,请仔细阅读-确切的短语是“疯狂的仇恨”;“讨厌”是形容词“疯狂”的对象,而不是“人”。有时候英语可能会有点难;-)
史蒂芬·A·劳

0

基本上,两个代码块执行相同的操作。也许这是一个奇怪的例子,但事实证明了这一点。SQL Server 2005:

SELECT * INTO #temp FROM master..spt_values
DECLARE @startTime DATETIME

BEGIN TRAN 

SELECT @startTime = GETDATE()
UPDATE #temp
SET number = 0
select DATEDIFF(ms, @startTime, GETDATE())

ROLLBACK 

BEGIN TRAN 
DECLARE @name VARCHAR

DECLARE tempCursor CURSOR
    FOR SELECT name FROM #temp

OPEN tempCursor

FETCH NEXT FROM tempCursor 
INTO @name

SELECT @startTime = GETDATE()
WHILE @@FETCH_STATUS = 0
BEGIN

    UPDATE #temp SET number = 0 WHERE NAME = @name
    FETCH NEXT FROM tempCursor 
    INTO @name

END 
select DATEDIFF(ms, @startTime, GETDATE())
CLOSE tempCursor
DEALLOCATE tempCursor

ROLLBACK 
DROP TABLE #temp

单个更新需要156毫秒,而光标需要2016毫秒。


3
是的,这证明了这是使用游标的一种非常愚蠢的方式!但是如果每行的更新取决于日期顺序中前一行的值怎么办?
史蒂文·劳

BEGIN TRAN SELECT TOP 1 baseval从表ORDER BY时间戳记DESC INSERT表(字段)值(值,包括从先前记录中得出的值)COMMIT TRAN
dkretz

@doofledorfer:将按日期在最后一行的基础上插入一行,而不是按日期顺序从其前一行的值更新每一行
Steven A. Lowe

要真正使用光标,您应该在更新中使用WHERE CURRENT OF
erikkallen
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.