递归 CTE - row_number() 聚合

Posted

技术标签:

【中文标题】递归 CTE - row_number() 聚合【英文标题】:CTE with recursion - row_number() aggregated 【发布时间】:2014-03-01 11:59:47 【问题描述】:

我有一个具有父/子关系的表以及一个引用自身的创建日期列。 我想显示节点上最近的“活动”排序的每个父记录和所有后代。 因此,如果很久以前创建的第 1 行添加了一个新子代(例如,或者将一个新子代添加到其子代中),那么我希望它位于结果的顶部。

我目前无法使其正常工作。

我的表结构如下:

CREATE TABLE [dbo].[Orders](
    [OrderId] [int] NOT NULL,
    [Orders_OrderId] [int] NULL,
    [DateOrdered] datetime)

我写了下面的SQL来提取信息:

WITH allOrders AS 
   (SELECT po.orderid, po.Orders_OrderId, po.DateOrdered, 0 as distance,  
   row_number() over (order by DateOrdered desc) as RN1
    FROM orders po WHERE po.Orders_OrderId is null
    UNION ALL
    SELECT b2.orderid ,b2.Orders_OrderId, b2.DateOrdered, c.distance + 1, 
    c.RN1
    FROM orders b2 
    INNER JOIN allOrders c 
    ON b2.Orders_OrderId = c.orderid
    )

SELECT * from allOrders
where RN1 between 0 and 2
order by rn1 asc, distance asc

有什么方法可以“聚合”递归选择的结果,以便在整个“父”节点中选择最大日期?

SQLFiddle 演示: http://sqlfiddle.com/#!3/ca6cb/11 (记录号 1 应该是第一个,因为它有一个最近更新的孩子)

更新 感谢@twrowsell 的建议,我有以下查询,确实可以工作,但看起来很笨拙并且存在一些性能问题,我觉得我不应该有 3 个 CTE 来实现这一点。有什么方法可以在保留“行号”的同时对其进行压缩(因为这是用于带有分页的用户显示)?

WITH allOrders AS 
  (SELECT po.orderid, po.Orders_OrderId, 0 as distance, po.DateOrdered, po.orderid as [rootId]
    FROM orders po WHERE po.Orders_OrderId is null 
    UNION ALL
    SELECT b2.orderid ,b2.Orders_OrderId, c.distance + 1, b2.DateOrdered, c.[rootId]
    FROM orders b2     
    INNER JOIN allOrders c 
    ON b2.Orders_OrderId = c.orderid
    ),
    mostRecentOrders as (
    SELECT *,
    MAX(DateOrdered) OVER (PARTITION BY rootId) as [HighestOrderId]
    from allOrders
    ),
    pagedOrders as (
    select *, dense_rank() over (order by [HighestOrderId] desc) as [PagedRowNumber] from mostRecentOrders)

    SELECT  * from pagedOrders
    where PagedRowNumber between 0 and 2
    order by [HighestOrderId] desc

另外,我可以使用 MAX(orderid),因为 orderid 是标识,并且创建日期后无法在我的场景中更新。

更新的 SQLFiddle:http://sqlfiddle.com/#!3/ca6cb/41

【问题讨论】:

您使用的是什么版本的 SQL Server? @JonSenchyna SQL 2008 @EdW,只需在小提琴中显示给定样本数据的期望输出即可。 @KumarHarsh 第二个 SQLFiddle 中有正确的数据。尽管sqlfiddle.com/#!3/9df5e/1 我在这里添加了更多示例数据 【参考方案1】:

首先,您需要存储“根”订单 ID,以便区分不同的订单“树”。一旦你有了它,你就可以聚合和排序你的数据。

据我所知,您至少需要一个 CTE 来构建树,第二个来进行排名,因为您不能在 WHERE 子句中使用 DENSE_RANK()

以下查询使用临时表来存储树。该查询从树中选择两次,一次用于行,第二次用于排名。如果我使用 CTE 来存储树,它必须构建它两次,因为 CTE 基本上只是一个可重用的子查询(它会在每次使用时重新构建)。使用临时表可确保我只需要构建一次。

这里是 SQL:

DECLARE @Offset INT = 0;
DECLARE @Fetch INT = 2;

-- Create the Order Trees
WITH OrderTree AS (
  SELECT  po.orderid AS RootOrderID,
          po.orderid,
          po.Orders_OrderId,
          po.DateOrdered,
          0 AS distance
  FROM orders po WHERE po.Orders_OrderId IS NULL
  UNION/**/ALL
  SELECT  parent.RootOrderID,
          child.orderid,
          child.Orders_OrderId,
          child.DateOrdered,
          parent.distance + 1 AS distance
  FROM orders child
  INNER JOIN OrderTree parent
  ON child.Orders_OrderId = parent.orderid
)
SELECT *
INTO #OrderTree
FROM OrderTree;

-- Rank the order trees by MAX(DateOrdered)
WITH
Rankings AS (
    SELECT RootOrderID,
         MAX(DateOrdered) AS MaxDate,
         ROW_NUMBER() OVER(ORDER BY MAX(DateOrdered) DESC, RootOrderID ASC) AS Rank
  FROM #OrderTree
  GROUP BY RootOrderID
)
-- Get the next @Fetch trees, starting at rank @Offset+1
SELECT  TREE.*,
        R.MaxDate,
        R.Rank
FROM Rankings R
INNER JOIN #OrderTree TREE
    ON R.RootOrderID = TREE.RootOrderID
WHERE R.Rank BETWEEN @Offset+1 AND (@Fetch+@Offset)
ORDER BY R.Rank ASC, TREE.distance ASC;

SQLFiddle

注意:UNIONALL 之间的 /**/ 是 this issue 的解决方法。

我使用数据库中现有表中的数据构建了自己的“订单”表,并对问题中的 3-CTE 查询进行了一些基准测试。这在大量数据(117 棵树,总共 37215 个订单,最大深度为 11)上略胜一筹。我通过在打开STATISTICS IOSTATISTICS TIME 的情况下运行每个查询进行基准测试,并在每次运行前清除缓存和缓冲区。

以下是两个查询的结果,以及两者共享的递归 CTE 的结果:

╔════════════╦══════════╦════════════╦══════════════╗
║ Query      ║ CPU Time ║ Scan Count ║ Logical Reads║
╠════════════╬══════════╬════════════╬══════════════╣
║ Tree CTE   ║ 24211ms  ║ 4          ║ 1116243      ║
╟────────────╫──────────╫────────────╫──────────────╢
║ 3-CTE      ║ 24789ms  ║ 7          ║ 1192221      ║
║ Temp Table ║ 24384ms  ║ 6          ║ 1116549      ║
╚════════════╩══════════╩════════════╩══════════════╝

这两个查询的大部分似乎都是递归顺序树 CTE。去除递归 CTE 的共享成本会得到以下结果:

╔════════════╦══════════╦════════════╦══════════════╗
║ Query      ║ CPU Time ║ Scan Count ║ Logical Reads║
╠════════════╬══════════╬════════════╬══════════════╣
║ 3-CTE      ║ 578ms    ║ 3          ║ 75978        ║
║ Temp Table ║ 173ms    ║ 2          ║ 306          ║
╚════════════╩══════════╩════════════╩══════════════╝

基于这些结果,我强烈建议您将 RootOrderID 列添加到您的订单表中,以避免不得不使用可能非常昂贵的递归 CTE。

【讨论】:

【参考方案2】:

我能够获得与您在更新的小提琴中描述的相同的结果集。作为 pedro 的交叉应用的一部分,我达到了我的解决方案。根据我自己的经验,应用的性能很糟糕。最终,它演变成现在的样子,主表上的左连接和具有您请求的分页的子查询。

请找小提琴>>here (SQLFiddle)

另外,附上代码:

WITH allOrders AS (
  --anchor
  SELECT po.orderid
         , po.Orders_OrderId
         , 0 AS distance
         , po.DateOrdered
         , po.orderid AS [rootId]
  FROM orders po
  WHERE po.Orders_OrderId IS NULL

  --recursive
  UNION ALL

  SELECT b2.orderid
         , b2.Orders_OrderId
         , c.distance + 1
         , b2.DateOrdered
         , c.[rootId]
  FROM orders b2

    JOIN allOrders c
      ON b2.Orders_OrderId = c.orderid
)
SELECT a.*
       , b.max_orderdate
       , RN1
FROM allOrders a
LEFT JOIN (SELECT DISTINCT rootid, max(DateOrdered) max_orderdate 
           , row_number() over (order by max(dateordered) desc) as RN1
           FROM allOrders GROUP BY rootid) b
ON a.rootid = b.rootid
where RN1 between 0 and 2
ORDER BY b.max_orderdate DESC, a.rootid, a.orders_orderid, a.orderid

【讨论】:

为什么不使用INNER JOIN?此外,由于您在子查询中对 rootid 进行分组,因此 DISTINCT 是多余的。 @JonSenchyna 你是绝对正确的, distinct 是多余的,我从之前的代码迭代中得到了它。我习惯使用左连接。 Inner Join 也绝对有用!【参考方案3】:

我很难理解您的确切需求,包括寻呼情况。您可以为您提供的样本提供预期的结果集,这样更容易检查。

不管怎样,看来你的主要困难在于:

有什么方法可以“聚合”递归的结果 选择,以便我可以选择整个日期的最大日期 “父”节点?

...这可以通过递归 CTE 和 APPLY 轻松完成。

我不确定你到底想要什么,所以我做了这两个小提琴:

SQL Fiddle 1 - 这里所有子节点都基于根顺序,即顺序 3 与其父节点(顺序 2)的父节点(顺序 1)在一起。

SQL Fiddle 2 - 这里的孩子与他们的直接父母分组,父母也成为根,所以订单 2 不会与它的父母一起到达顶部(订单 1)。

我想你会对第一个进行一些修改。

再次重申,在此类问题中提供您的预期结果非常重要,否则您会得到大量反复试验的答案。

【讨论】:

【参考方案4】:

请看以下内容:

;WITH allOrders AS 
   (SELECT po.orderid, po.Orders_OrderId, po.DateOrdered, 0 as distance, po.orderid as [parentOrder]
    FROM orders po WHERE po.Orders_OrderId is null
        UNION ALL
    SELECT b2.orderid ,b2.Orders_OrderId, b2.DateOrdered, c.distance + 1, c.[parentOrder]
    FROM orders b2 
    INNER JOIN allOrders c ON b2.Orders_OrderId = c.orderid

    )
    SELECT a.OrderId
           ,a.Orders_OrderId
           ,a.DateOrdered
           ,top1.DateOrdered as HIghestDate
           ,a.distance
           ,a.parentOrder     
    FROM allOrders a
    INNER JOIN (SELECT TOP 2 parentOrder, MAX(DateOrdered)as highestdates FROM allOrders GROUP BY parentOrder ORDER BY MAX(DateOrdered)DESC)b on a.parentOrder=b.parentOrder 
    OUTER APPLY (SELECT TOP 1 parentOrder, DateOrdered FROM allOrders top1 WHERE a.parentOrder=top1.parentOrder ORDER BY top1.DateOrdered DESC)top1

SQLFiddle

【讨论】:

【参考方案5】:

将在外部选择工作的 OVER 子句中的 DateOrdered 上使用 MAX..

    WITH allOrders AS 
(
    SELECT po.orderid, po.Orders_OrderId, po.DateOrdered, 0 as distance,  
       row_number() over (order by DateOrdered desc) as RN1
    FROM orders po WHERE po.Orders_OrderId is null
    UNION ALL
    SELECT b2.orderid ,b2.Orders_OrderId, b2.DateOrdered, c.distance + 1, 
      c.RN1
    FROM orders b2 
    INNER JOIN allOrders c 
    ON b2.Orders_OrderId = c.orderid
    )


    SELECT *,   MAX(DateOrdered) OVER (PARTITION BY Orders_OrderId) from allOrders
    where RN1 between 0 and 2
    order by rn1 asc, distance asc

编辑: 对不起,我第一次误解了你的要求。看起来您想通过 RN1 字段而不是 Orders_OrderId 对结果进行分区,因此您的外部选择将类似于..

 SELECT MAX(DateOrdered) OVER (PARTITION BY RN1 ),*  from allOrders
where RN1 between 0 and 2
order by rn1 asc, distance asc

【讨论】:

这不起作用,因为 Orders_OrderId 没有唯一标识一个“节点”,所以它不会选择所有父子节点的最高日期。不过,它确实给了我一些想法,所以谢谢 我确实看到了,谢谢@twrowsell,即使它是在我在我的问题中发布后进行编辑的;)实际上按“rootid”进行分区似乎更快(它唯一地标识了每个“节点”) .我仍在寻找对整个查询的改进,这就是我添加赏金的原因,也许我不够清楚,我想在原始问题中保留分页。 @Maverick allOrders 是 CTE 的名称,在第一行定义。这是一个递归查询

以上是关于递归 CTE - row_number() 聚合的主要内容,如果未能解决你的问题,请参考以下文章

row_number 和 cte 使用实例:考场监考安排

CTE、ROW_NUMBER 和 ROWCOUNT

查询性能:CTE 使用 ROW_NUMBER() 选择第一行

使用 cte ROW_NUMBER() 提高性能

MariaDB表表达式:CTE

递归 CTE 通过多个级别更新父记录