FAST_FORWARD游标何时会有工作表(这是否应该避免)?

33

背景

在尝试运行总计查询时,我注意到有时预估的计划只显示一个“获取查询”。

Fetch

实际计划显示从聚集索引扫描中重复获取

Fetch Scan

在其他情况下(例如向查询添加TOP时),估计的计划显示一个“Population Query”阶段,该阶段填充工作表。

Fetch and Populate

实际计划显示使用聚集索引扫描来填充工作表,然后对该工作表进行重复查找。

Seeks

问题

  1. SQL Server在选择方法时使用什么标准?
  2. 我认为第一种方法(不需要额外的工作表填充步骤)更有效,我的想法正确吗?

(奖励问题:如果有人能解释为什么第一个查询中的每个扫描都计为2个逻辑读取,那可能会很有启发性)

附加信息

我找到了这篇文章,其中解释了FAST_FORWARD游标可以使用动态计划或静态计划。在本例中,第一个查询似乎使用动态计划,而第二个查询使用静态计划。

我还发现,如果我尝试

SET @C2 = CURSOR DYNAMIC TYPE_WARNING FOR SELECT TOP ...

光标会被隐式转换为一个keyset光标,因此清楚地表明动态光标不支持TOP构造,可能是出于Ruben回答中的原因 - 仍在寻找这方面的明确解释。

然而,我也读到过动态光标往往比其静态对应物来源1, 来源2),这对我来说似乎令人惊讶,因为静态光标必须读取源数据,复制它,然后读取副本,而不仅仅是读取源数据。我之前引用的那篇文章提到动态光标使用markers。有人能解释一下这些是什么吗?它只是RID或CI密钥,还是其他东西?

脚本

SET STATISTICS IO OFF

CREATE TABLE #T ( ord INT IDENTITY PRIMARY KEY, total INT, Filler char(8000))

INSERT INTO #T (total) VALUES (37),(80),(55),(31),(53)

DECLARE @running_total INT, 
    @ord INT, 
    @total INT
    
SET @running_total = 0
SET STATISTICS IO ON
DECLARE @C1 AS CURSOR;
SET @C1 = CURSOR FAST_FORWARD FOR SELECT ord, total FROM #T ORDER BY ord;
OPEN @C1;
PRINT 'Initial FETCH C1'
FETCH NEXT FROM @C1 INTO @ord, @total ;
WHILE @@FETCH_STATUS = 0
BEGIN
  SET @running_total = @running_total + @total
  PRINT 'FETCH C1'
  FETCH NEXT FROM @C1 INTO @ord, @total ;
END

SET @running_total = 0
SET STATISTICS IO ON
DECLARE @C2 AS CURSOR;
SET @C2 = CURSOR FAST_FORWARD FOR SELECT TOP 5 ord, total FROM #T ORDER BY ord;
OPEN @C2;
PRINT 'Initial FETCH C2'
FETCH NEXT FROM @C2 INTO @ord, @total ;
WHILE @@FETCH_STATUS = 0
BEGIN
  SET @running_total = @running_total + @total
  PRINT 'FETCH C2'
  FETCH NEXT FROM @C2 INTO @ord, @total ;
END

PRINT 'End C2'
DROP TABLE #T 

一个解释可能是工作表提供了一些一致性。top 5在一个事务中检索,就像一个快照。如果没有工作表,你可能会得到一个包含从未同时存在于表中的行的top 5 - Andomar
@Andomar - 可能是这样的情况。在这种特定情况下,我正在使用本地的 #temp 表,因此 SQL Server 可能会认识到它将始终保持一致,因为其他事务无法修改它。我也尝试了 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED,仍然看到相同的结果。(而且 SET ROWCOUNT 5 也不会改变两个计划) - Martin Smith
2个回答

9
只是一种猜测,但通常情况下,TOP-ORDER BY需要SQL Server以某种方式缓存结果(无论是索引扫描的结果还是整个结果在临时结构中,或者介于两者之间)。可以认为对于游标,即使按主键排序(如您的示例),当在索引扫描的范围内进行删除或插入操作,并且尚未完成获取时,必须执行此操作,因为不能允许TOP 5游标意外返回少于5行,而相应的SELECT确实返回了5行(或更糟:游标返回了超过5行)。
理论上,在游标已确定索引扫描的范围之后,如果表中有删除或插入,而这些删除或插入恰好在索引扫描的范围内,但您还没有完成获取,就可能发生这种奇怪的情况。为防止发生这种情况,他们可能在这里出于安全考虑。 (他们只是没有针对#temp表进行优化。)
不过还有一个问题:SQL Server是否允许使用FETCH FROM SELECT TOP n而没有ORDER BY子句? (我这里没有运行SQL Server实例。)知道会很有趣,能知道那种计划吗?

1
对于可以通过遵循索引满足的 TOP ... ORDER BY,结果通常不会被缓冲到类似于 Spool 的东西中。添加了一个 TOP 迭代器到计划中,用于计算行数并在达到目标时停止从子迭代器请求行。对于一般(非游标)查询,除非处于可串行化或快照隔离级别,否则不会尝试减轻在查询期间插入或删除行的影响,而且如果在扫描期间移动,则有可能读取相同的行两次。不确定带有 TOP 的游标为什么需要更严格的语义? - Martin Smith
此外,如果没有ORDER BY,主要的更新操作(可能会被锁定,直到你通过物理行或页)是最重要的。这意味着只要你通过游标流式传输,表格就可以逐步解锁。但是,如果有ORDER BY,则不是这种情况,你可能需要一个广泛的表格或索引锁,直到关闭游标,否则可能会得到不一致的结果。当你在1000行表上执行TOP 5时,这正是你想要的,因此查询优化器可能会决定提前制作快照,以便仅锁定所选行。 - Ruben
2
@Parmenion - 绝对没有可以依赖的隐式order by。如果使用DISTINCT,您可能会看到您描述的行为作为排序操作的结果,但不能保证它会使用哈希聚合。对于其他查询,由于使用的访问方法,您可能会观察到顺序是按索引键顺序排列的,但同样不能依赖此行为,因为并行性、IAM有序扫描或旋转扫描都可能改变这种观察到的行为。如果需要ORDER BY,应明确指定。 - Martin Smith
@Martin - 我刚刚测试了一下,似乎你最后一个查询中缺少了 TOP 关键字。添加 TOP 会再次进行填充。此外,请注意使用 TOP 1 不会导致填充。因此,我认为 SQL Server 正试图防止在 TOP 查询上出现您所描述的无限循环的可能性(这是有道理的)。 - Ruben
@Ruben - 我还不太确定是否将其标记为答案,但是考虑到你可能是正确的,我会给予疑点的好处以获得悬赏! - Martin Smith
显示剩余7条评论

5
SQL Server在选择哪种方法的时候主要是以成本为基础的决策。引用您链接的文章中的话,"在动态计划看起来很有前途的情况下,启发式跳过成本比较可能会被跳过。这主要发生在非常便宜的查询中,尽管细节有点晦涩。"
如果所有行最终都将被处理,则静态计划可能会表现得更好。动态和静态游标计划具有不同的优缺点。关于这个问题稍后会有更多讨论。
动态游标计划中的所有迭代器必须能够保存和恢复状态、向前和向后扫描、每个输出行处理一个输入行,并且是非阻塞的。总的来说,Top不能满足所有这些要求;类CQScanTopNew没有实现必要的Set/Get/Goto/Marker()和ReverseDirection()方法(等等)。
对于Transact-SQL游标,通常情况下,动态游标比静态游标慢,因为需要保存和恢复动态查询计划的状态。静态游标则需要复制集合的开销(对于大型集合可能是主要因素),但检索每行的成本相当小。键集的每行检索开销比静态游标高,因为它们必须外连接回源表以检索非键列。
当访问集合的相对小部分和/或检索不是逐行进行时,动态游标是最优的。这是许多常见游标场景的典型访问模式,只是博客文章倾向于测试其他类型的场景:)
这归结于扫描的状态保存方式和读取计数方式。
标记存在于动态游标计划中的每个迭代器中,而不仅仅是访问方法。'标记'是重新启动计划迭代器所需的所有状态信息,在其离开的点上。对于访问方法,RID或索引键(如果必要,带有唯一标识符)是其中很大一部分,但绝不是全部。

1
谢谢。关于游标内部的信息似乎很难找到! - Martin Smith

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