2

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

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

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”列。但是,正如我发现的(后来在网上确认),计算列不能用作计算列计算的一部分。

目前,使用 CompletePercent 的以下实现,我总是为 'isDone'=1 行获得 1 和 0 为 'isDone'=0 行而感到沮丧:

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 不为 0,则在 CTE 的结果中将其视为“真”)。每个节点的“权重”(实际上是它占总数的百分比)是它的父节点的权重除以其兄弟节点(包括它自己)的数量,根集为 100%。

对于“isDone”节点或叶子节点,向下行程停止。两者都将有下一个递归步骤返回 0 行)。

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

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

同样,我们将不胜感激任何有关在任何方向取得进展的提示。

问候,ShaiB

4

1 回答 1

0

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

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

其次,您可以通过存储过程来做到这一点。使用游标一次遍历基表一行,计算percentComplete列的值,并将结果存储在表变量中。(您可能会这样写,您只需访问基表中的每一行一次。)然后简单地返回(即,选择)表变量的结果。

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

第四,您可以通过 CLR 函数(即,用 C# 编写并将其导入服务器)来执行此操作。对于带有 CLR 函数的函数,您可以摆脱许多 SQL Server 的(愚蠢的)规则。(虽然,这并不意味着它总是一个好主意。)

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

希望能给你一些想法!

于 2013-03-16T02:35:20.933 回答