【问题标题】:Why do Recursive CTEs run analytic functions (ROW_NUMBER) procedurally?为什么递归 CTE 以程序方式运行分析函数 (ROW_NUMBER)?
【发布时间】:2012-04-16 08:05:28
【问题描述】:

我昨天回答了一个递归 CTE,它暴露了这些在 SQL Server 中的实现方式问题(可能也在其他 RDBMS 中?)。基本上,当我尝试针对当前递归级别使用ROW_NUMBER 时,它会针对每一行当前递归级别的子集 运行。我希望这将在真正的 SET 逻辑中工作,并针对整个当前递归级别运行。

看来,from this MSDN article,我发现的问题是预期功能:

CTE 递归部分中的分析和聚合函数是 应用于当前递归级别的集合而不是集合 对于 CTE。 像 ROW_NUMBER 这样的函数只对 通过当前递归级别而不是全部传递给它们的数据 传递给 CTE 递归部分的一组数据。更多 信息,请参阅 J. 在递归 CTE 中使用分析函数。

在我的挖掘中,我找不到任何地方可以解释为什么选择它以这种方式工作?这更像是基于集合的语言中的一种程序方法,因此这与我的 SQL 思维过程相悖,并且在我看来非常令人困惑。 是否有人知道和/或谁能解释为什么递归 CTE 以程序方式在递归级别处理分析函数?


以下是帮助可视化的代码:

注意,每个代码输出中的RowNumber 列。

Here is the SQLFiddle for the CTE (only showing the 2nd level of the recursion)

WITH myCTE
AS
(
  SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, 1 AS RecurseLevel
  FROM tblGroups
  WHERE ParentId IS NULL

  UNION ALL

  SELECT tblGroups.*, 
      ROW_NUMBER() OVER (ORDER BY myCTE.RowNumber , tblGroups.Score desc) AS RowNumber, 
      RecurseLevel + 1 AS RecurseLevel
  FROM tblGroups
      JOIN myCTE
          ON myCTE.GroupID = tblGroups.ParentID
 )
SELECT *
FROM myCTE
WHERE RecurseLevel = 2;

Here is the second SQLFiddle for what I would expect the CTE to do (again only need the 2nd level to display the issue)

WITH myCTE
AS
(
  SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, 1 AS RecurseLevel
  FROM tblGroups
  WHERE ParentId IS NULL
 )
  SELECT tblGroups.*, 
      ROW_NUMBER() OVER (ORDER BY myCTE.RowNumber , tblGroups.Score desc) AS RowNumber, 
      RecurseLevel + 1 AS RecurseLevel
  FROM tblGroups
      JOIN myCTE
          ON myCTE.GroupID = tblGroups.ParentID;

我一直设想 SQL 递归 CTE 运行起来更像 this while loop

DECLARE @RecursionLevel INT
SET @RecursionLevel = 0
SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, @RecursionLevel AS recurseLevel
INTO #RecursiveTable
FROM tblGroups
WHERE ParentId IS NULL

WHILE EXISTS( SELECT tblGroups.* FROM tblGroups JOIN #RecursiveTable ON #RecursiveTable.GroupID = tblGroups.ParentID WHERE recurseLevel = @RecursionLevel)
BEGIN

    INSERT INTO #RecursiveTable
    SELECT tblGroups.*, 
        ROW_NUMBER() OVER (ORDER BY #RecursiveTable.RowNumber , tblGroups.Score desc) AS RowNumber, 
        recurseLevel + 1 AS recurseLevel
    FROM tblGroups
        JOIN #RecursiveTable
            ON #RecursiveTable.GroupID = tblGroups.ParentID
    WHERE recurseLevel = @RecursionLevel
    SET @RecursionLevel = @RecursionLevel + 1
END

SELECT * FROM #RecursiveTable ORDER BY RecurseLevel;

【问题讨论】:

  • 所有递归 CTE 当前都获得相同的基本计划,将行添加到充当堆栈的假脱机,然后使用嵌套循环逐行处理该行。 EXCEPT as per this question 的类似问题
  • @MartinSmith 是的,我明白这一点,但我的问题是为什么它会这样做,因为它可以很容易地处理这是一个基于集合的递归。这是 SQL 的优势,而不是这种过程方法。
  • 不知道。猜猜更简单或更有效的实现?大多数暴露逻辑和物理实现之间差异的功能都被禁止。 EXCEPT will join 那个列表。 Connect Item for ROW_NUMBER 表示他们在 2008 年也曾在这里这样做过,但在某些与 hierarchyids 相关的用例中将其颠倒过来
  • @MartinSmith Hrmmm,我与 Paul White 不在同一页面上(因为我仍然不同意),但我确实理解他们的推理来自哪里。如果您发表您的评论作为答案,我会接受,因为这就是我正在寻找的...MS 推理

标签: sql sql-server common-table-expression row-number recursive-cte


【解决方案1】:

分析函数的特殊之处在于它们需要已知的结果集来解析。 它们依赖于后面的、前面的或完整的结果集来计算当前值。 也就是说,在包含分析函数的视图上永远不允许合并视图。为什么? 这会改变结果。

例如:

    Select * from (
      select row_number() over (partition by c1 order by c2) rw, c3 from t) z
    where c3=123

不一样
    select row_number() over (partition by c1 order by c2) rw, c3 from t 
    where c3=123

这 2 个将返回不同的 rw 值。 这就是为什么包含分析函数的子查询将始终在之前完全解析,并且永远不会与其余部分合并。

更新

查看第二个查询:

WITH myCTE
AS
(
  SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, 1 AS RecurseLevel
  FROM tblGroups
  WHERE ParentId IS NULL
 )
  SELECT tblGroups.*, 
      ROW_NUMBER() OVER (ORDER BY myCTE.RowNumber , tblGroups.Score desc) AS RowNumber, 
      RecurseLevel + 1 AS RecurseLevel
  FROM tblGroups
      JOIN myCTE
          ON myCTE.GroupID = tblGroups.ParentID;

它的工作原理就像它被写成一样(相同的执行计划和结果):

SELECT tblGroups.*, 
      ROW_NUMBER() OVER (ORDER BY myCTE.RowNumber , tblGroups.Score desc) AS RowNumber, 
      RecurseLevel + 1 AS RecurseLevel
FROM tblGroups
JOIN (
    SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, 1 AS RecurseLevel
    FROM tblGroups
    WHERE ParentId IS NULL
    )myCTE ON myCTE.GroupID = tblGroups.ParentID;

这个需要分区重置行号。

递归查询在 while 循环中不起作用,它们不是程序性的。在基础上,它们像递归函数一样工作,但根据表、查询、索引,它们可以被优化为以一种或另一种方式运行。

如果我们确实遵循在使用分析函数时视图不能合并的概念,并且查看查询1。它只能运行一次,并且处于嵌套循环中。

WITH myCTE
AS
( /*Cannot be merged*/
  SELECT *, ROW_NUMBER() OVER (ORDER BY Score desc) AS RowNumber, 1 AS RecurseLevel,
  cast(0 as bigint) n
  FROM tblGroups
  WHERE ParentId IS NULL

  UNION ALL

/*Cannot be merged*/
  SELECT tblGroups.*, 
      ROW_NUMBER() OVER (ORDER BY myCTE.RowNumber, tblGroups.Score desc) AS RowNumber,       RecurseLevel + 1 AS RecurseLevel,
  myCTE.RowNumber
  FROM tblGroups
      JOIN myCTE
          ON myCTE.GroupID = tblGroups.ParentID
 )
SELECT *
FROM myCTE;

所以第一个选择,第二个也不能合并。运行此查询的唯一方法是在每个级别中返回的每个项目的嵌套循环中,因此重置。同样,这不是程序与否的问题,只是可能的执行计划的问题。

希望这能回答你的问题,如果没有,请告诉我:)

是的

【讨论】:

  • 谢谢,但我已经了解分析函数。问题不在于分析函数如何工作,而在于它们如何在 CTE 中发挥作用。它们以更程序化的方式运行,而不是典型的 SQL 集逻辑。请注意,我的第一个查询与第二个查询几乎相同(我只是从第二次迭代中挑选出结果)。请重新阅读我的整个问题,希望它对我的期望更有意义。
  • 对不起,我是 stackoverflow 的新手。这是我所看到的,第二个查询不是 CTE。这只是一个常规的加入。第一个是CTE。行号(分析函数)在 CTE 中的工作方式与任何其他查询一样。而且它们不能以“while 循环”的方式工作。 CTE 是一个自联接的递归 sql(至少在 Oracle 中:-))。不知道这是否回答了你的问题,如果没有,请告诉我,这让我很好奇!
  • 第二个是 CTE,不是递归的。这是我在评论中的错误,因为没有更清楚地说明(它应该是……但是它们在 recursive CTE 中是如何起作用的)。我要说的是 recursive CTE is 本质上是一个 while 循环。第一个查询是一个完全递归的 CTE,我只输出第一个迭代(在基集之后)......但请注意 RowNumber 重置。第二个只是一个常规的 CTE,我使用输出来运行我第一个中的相同查询(基本上模仿第一次迭代中应该发生的事情)..notice RowNumber 不会重置
  • 你是对的 CTE 但不是递归的。第二个没有理由重置,您只要求按 CTE 行号排序的行号。查询返回 4 行,按 CTE (1,2)&score 排序的 abcd。如果您想要重置,它应该是(按 myCTE.RowNumber 顺序按 tblGroups.Score desc 分区)。每次更改级别/父级时,第一个查询都会重置。
  • 不,我不想重置,每次更改父项/子项时,第一个查询都会重置。据我所知,第一次迭代的查询 1 和查询 2 应该是相同的。我不知道还能怎么解释。请务必仔细查看每个示例及其输出。希望你能明白我在说什么。
猜你喜欢
  • 1970-01-01
  • 2014-03-01
  • 1970-01-01
  • 2011-11-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多