SQL计算列,递归地总结层次结构

时间:2013-02-24 07:57:44

标签: sql-server-2008-r2 common-table-expression recursive-query calculated-columns

到目前为止的[简化]故事:

在Visual Studio 2010下的.mdf数据库中,我有一个下表:

CREATE TABLE [dbo].[SandTable](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [isDone] [bit] NOT NULL,
    [percentComplete]  AS ([dbo].[CompletePercent]([id],[isDone])),
    [parentId] [int] NULL,
 CONSTRAINT [PK_SandTable] PRIMARY KEY CLUSTERED 
(
    [id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON)
)
ALTER TABLE [dbo].[SandTable]  WITH CHECK ADD  CONSTRAINT [FK_SandTable_SandTable] FOREIGN KEY([parentId])

这个想法是行形成为树/林,其中parentId用作父节点的“指针”。

'percentComplete'计算列使用函数CompletePercent来计算以行为根的子树的完整程度,如下所示:

  • 如果一行的'isDone'位为1,那么我们认为整个子树完全100%(这是用户覆盖),因此返回1.0。
  • 但是,如果'isDone'为0,我需要计算整个子树的'完整性'。我这样做是通过递归地平均直接孩子的“完整性”,这对孩子来说是这样,等等直到叶子。

起初,我尝试将'CompletePercent'平均值设为直接子项'percentComplete'列。但是,正如我发现的(后来在线确认),计算列不能用作计算列计算的一部分。

目前,我很沮丧,因为'isDone'= 1行总是得1,而'isDone'= 0行0,使用以下CompletePercent实现:

CREATE FUNCTION [dbo].[CompletePercent]
    (
    @id int, 
    @isDone bit = 0
    )
RETURNS float
AS
    BEGIN
        DECLARE @result float
        IF @isDone = 1
            SET @result = 1.0
        ELSE
            SET @result = 
                (SELECT 
                    CASE 
                        WHEN (COUNT(*) = 0) THEN 0.0
                        ELSE AVG(dbo.CompletePercent(id, isDone))
                    END
                FROM dbo.SandTable
                WHERE parentId = @id
                )
        RETURN @result
    END

我希望这里有一些简单的东西,我只是缺少了,长时间盯着它。

我的下一步是尝试使用我正在研究的递归CTE。但是,我不确定如何编码所需的“特殊”条件平均值。

如果有人能够在我的行动中发现错误,或者指导我参加CTE,我将非常感激。

[编辑:] 即使在CTE赛道上,我也走到了死胡同,以下是疯狂的(如果可能的话,可能会浪费)查询:

WITH Weights AS (SELECT SandTable.id, COUNT(NULLIF (SandTable.isDone, 0)) AS isDone, 100.0 AS weight, COUNT(ST.id) AS kids
    FROM SandTable INNER JOIN
        SandTable AS ST ON SandTable.id = ST.parentId
    WHERE (SandTable.parentId IS NULL)
    GROUP BY SandTable.id
    UNION ALL
    SELECT SandTable_1.id, COUNT(NULLIF (SandTable_1.isDone, 0)) AS isDone, MyCTE_2.weight / MyCTE_2.kids AS weight, COUNT(ST_1.id) AS kids
    FROM SandTable AS SandTable_1 INNER JOIN
        MyCTE AS MyCTE_2 ON SandTable_1.parentId = MyCTE_2.id AND MyCTE_2.isDone = 0 INNER JOIN
        SandTable AS ST_1 ON SandTable.id = ST_1.parentId
    WHERE (SandTable_1.parentId IS NOT NULL)
    GROUP BY SandTable_1.id)
 SELECT SUM(weight)
    FROM Weights AS Weights_1
    WHERE (isDone > 0)

这个想法是沿着层次结构向下移动(目前从根目录开始,但我计划将其修改为以特定的id开头),并为每个节点计算子节点数并测试'isDone'(在此完成用于计算用于执行计数的JOIN的聚合,现在isDone在CTE的结果中被认为是“真”,如果它不是0)。每个节点的“权重”(实际上是它对总数的贡献百分比)是它的父母的权重除以其兄弟的数量(包括其自身),根设置为100%。

对于'isDone'节点或在叶子处停止跳闸。两者都有下一个递归步骤返回0行。

最后,'idDone'节点的总权重相加(其他节点仅用于递归)。

然而,这无法运行,因为结果错误表明: “在递归公共表表达式的递归部分中不允许使用GROUP BY,HAVING或聚合函数。”

同样,任何方向取得任何进展的任何提示都将受到高度赞赏。

此致 ShaiB

1 个答案:

答案 0 :(得分:0)

无论您选择什么路线,这都可能是相当昂贵的操作。但是,这里有一些可能会有所帮助的想法:

首先,您是否考虑过使用视图?您可以将计算列放在表上并将其添加到视图中,这可能会让您绕过计算列约束。您还可以使视图可更新(通过代替触发器),这样对于您的应用程序,它的行为就像一个表。

其次,您可以通过存储过程来完成此操作。使用游标一次迭代基表一行,计算percentComplete列的值,并将结果存储在表变量中。 (您可能会以这样的方式编写它,您只需要访问基表中的每一行。)然后只需返回(即,SELECT)表变量的结果。

第三,类似于第二种,在插入/更新/删除之后写入触发器以重新计算每行的percentComplete,而不是使用计算列。虽然这会给你非常快速的读取,但写入时可能会非常慢。

第四,您可以通过CLR功能执行此操作(即,将其写入C#并将其导入服务器)。对于具有CLR功能的函数,您可以摒弃许多SQL Server(傻瓜)规则。 (虽然,这并不意味着它总是一个好主意。)

第五,也许最复杂的是,您可以编写一个CLR表函数来读取表中的行(没有percentComplete)并计算并将percentComplete列附加到结果集。然后,使用它作为视图的基础(即SELECT * FROM dbo.GetTheTree()),然后使用替代触发器使视图可更新(类似于第二个选项)。

希望能给你一些想法!