递归 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
注意:UNION
和 ALL
之间的 /**/
是 this issue 的解决方法。
我使用数据库中现有表中的数据构建了自己的“订单”表,并对问题中的 3-CTE 查询进行了一些基准测试。这在大量数据(117 棵树,总共 37215 个订单,最大深度为 11)上略胜一筹。我通过在打开STATISTICS IO
和STATISTICS 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() 聚合的主要内容,如果未能解决你的问题,请参考以下文章