Агрегатные функции HierarchyID в T-SQL - PullRequest
1 голос
/ 18 января 2012

Меня попросили запросить базу данных регистрации времени, чтобы отобразить всю работу, проделанную для заданных проектов.Каждый проект разбит на задачи, каждый из которых может быть разбит на задачи.Иерархия задач может быть произвольным числом уровней.Часть требования заключается в предоставлении общего времени, отработанного для каждой задачи или узла в иерархии (не только узлов конечного уровня, но и всех узлов, включая узел проекта верхнего уровня, узлы конечного уровня и все промежуточные узлы).

Работая с такой иерархией, я предполагаю, что может быть полезно использовать тип данных HIERARCHYID.Есть ли способ сделать что-то вроде SUM с ROLLUP в иерархии, чтобы дать промежуточные итоги для каждого узла в иерархии?

Я предполагал, что подобный сводный свод в иерархии будет общим требованиемно мне совсем не повезло найти способ сделать это, или даже если это возможно.

Ответы [ 2 ]

2 голосов
/ 12 июля 2012

Вот кое-что, что я попробовал, и это работало хорошо. В этом случае у меня была таблица Taxonomies, которая имела id и ParentTaxonomyID, который указывает на ID. В этой хранимой процедуре я хотел подсчитать количество связанных проблем, связанных с таксономией, но я хотел бы суммировать их по иерархии. Вот хранимая процедура, которую я использовал

ALTER FUNCTION [dbo].[func_NumberOfQuestions](  
@TaxonomyID INT )
RETURNS INT
AS
BEGIN

DECLARE @NChildren INT
SELECT  @NChildren = dbo.func_NumberOfTaxonomyChildren(@TaxonomyID)

DECLARE @NumberOfQuestions INT, @NumberOfDirectQuestions INT, 
    @NumberOfChildQuestions INT 

SELECT  @NumberOfDirectQuestions = COUNT(*) 
FROM    ProblemTaxonomies
WHERE   TaxonomyID = @TaxonomyID

SELECT @NumberOfChildQuestions = 0
IF @NChildren > 0
BEGIN
SELECT  @NumberOfChildQuestions = 
        ISNULL(SUM(dbo.func_NumberOfQuestions(id)), 0)
FROM    Taxonomies
WHERE   ParentTaxonomyID = @TaxonomyID
END

RETURN @NumberOfDirectQuestions + @NumberOfChildQuestions
END

Я использовал функцию в T-SQL, это должно быть довольно очевидным рекурсивным вызовом - но с помощью SQL я смог использовать функцию SUM для Children

2 голосов
/ 18 января 2012

Разобрался, как это сделать.Метод немного запутанный, возможно, кто-то другой может придумать более аккуратную версию.

Метод состоит из четырех шагов:

  1. Запустите функцию ROW_NUMBER для всех задач.для данных проектов.Разделение по ParentId, чтобы все дочерние задачи данного родителя были пронумерованы 1, 2, 3, 4 и т. Д. Это работает на всех уровнях иерархии задач;

  2. Использование рекурсивногоCTE (общее табличное выражение), чтобы пройти вверх по иерархии задач от конечного уровня до вершины.Это создаст структуру иерархии задач из отношения родитель-потомок в таблице TimeCode.Первоначально я пытался включить здесь функцию ROW_NUMBER, но это не сработало из-за способа, которым Microsoft внедрила CTE;

  3. Добавить столбец HIERARCHYID в структуру, созданную на шаге 2;

  4. Выполните самостоятельное объединение набора записей, чтобы получить все дочерние элементы каждого узла в структуре.Группируйте по родительскому узлу и суммируйте время, записанное для каждого дочернего узла.Обратите внимание, что метод HIERARCHYID IsDescendantOf возвращает не только дочерние элементы узла, но и сам узел.Поэтому, если какое-либо время было записано для родительской задачи, а также для дочерних элементов, оно будет включено в общее время для этого родительского узла.

Вот сценарий:

-- Cannot include a ROW_NUMBER function within the recursive member of the 
--    common table expression as SQL Server recurses depth first. ie SQL 
--    Server recurses each row separately, completing the recursion for a 
--    given row before starting the next.
-- To get around this, use ROW_NUMBER outside the common table expression.

DECLARE @tblTask TABLE (TimeCodeId INT, ParentId INT, ProjectID INT, 
    Level INT, TaskIndex VARCHAR(12), Duration FLOAT);

INSERT INTO @tblTask (TimeCodeId, ParentId, ProjectID, 
    Level, TaskIndex, Duration)
SELECT tc.TimeCodeId, 
    tc.ParentId, 
    CASE
        WHEN tc.ParentId IS NULL THEN tc.ReferenceId1
        ELSE tc.ReferenceId2
    END AS ProjectID, 
    1 AS Level, 
    CAST(ROW_NUMBER() OVER (PARTITION BY tc.ParentId 
                            ORDER BY tc.[Description]) AS VARCHAR(12)) 
                                                            AS TaskIndex, 
    ts.Duration            
FROM Time.TimeCode tc 
    LEFT JOIN 
    (    -- Get time sub-totals for each task.
        SELECT TimeCodeId, 
            SUM(Duration) AS Duration
        FROM Time.Timesheet
        WHERE ReferenceId2 IN (12196, 12198)
        GROUP BY TimeCodeId
    ) ts
    ON tc.TimeCodeId = ts.TimeCodeId
WHERE ReferenceId2 IN (12196, 12198)
ORDER BY [Description];

DECLARE @tblHierarchy TABLE (HierarchyNode HIERARCHYID, 
    Level INT, Duration FLOAT);

-- Common table expression that builds up the task hierarchy recursively.
WITH cte_task_hierarchy AS 
(
    -- Anchor member.
    SELECT t.TimeCodeId,
        t.ParentID,  
        t.ProjectID, 
        t.Level, 
        CAST('/' + t.TaskIndex + '/' AS VARCHAR(200)) AS HierarchyNodeText, 
        t.Duration            
    FROM @tblTask t

    UNION ALL

    -- Dummy root node for HIERARCHYID.
    --    (easier to add it after another query so don't have to cast the 
    --    NULLs to data types)
    SELECT NULL AS TimeCodeId, 
        NULL AS ParentID, 
        NULL AS ProjectID, 
        0 AS Level, 
        CAST('/' AS VARCHAR(200)) AS HierarchyNodeText, 
        NULL AS Duration

    UNION ALL 

    -- Recursive member that walks up the task hierarchy.
    SELECT tp.TimeCodeId, 
        tp.ParentID,  
        th.ProjectID, 
        th.Level + 1 AS Level, 
        CAST('/' + tp.TaskIndex + th.HierarchyNodeText AS VARCHAR(200)) 
            AS HierarchyNodeText,
        th.Duration
    FROM cte_task_hierarchy th 
        JOIN @tblTask tp ON th.ParentID = tp.TimeCodeId 
)
INSERT INTO @tblHierarchy (HierarchyNode, 
    Level, Duration)
SELECT hierarchyid::Parse(cth.HierarchyNodeText), 
    cth.Level, cth.Duration
FROM cte_task_hierarchy cth 
-- This filters recordset to exclude intermediate steps in the recursion 
--    - only want the final result.
WHERE cth.ParentId IS NULL
ORDER BY cth.HierarchyNodeText;

-- Show the task hierarchy.
SELECT *, HierarchyNode.ToString() AS NodeText
FROM @tblHierarchy;

-- Calculate the sub-totals for each task in the hierarchy.
SELECT t1.HierarchyNode.ToString() AS NodeText, 
    COALESCE(SUM(t2.Duration), 0) AS DurationTotal
FROM @tblHierarchy t1 
    JOIN @tblHierarchy t2 
        ON t2.HierarchyNode.IsDescendantOf(t1.HierarchyNode) = 1
GROUP BY t1.HierarchyNode;

Результаты:

Первый набор записей (структура задач со столбцом HIERARCHYID):

HierarchyNode    Level    Duration    NodeText
-------------    -----   --------    --------
0x               0        NULL       /
0x58             1        NULL       /1/
0x5AC0           2        12.15      /1/1/
0x5AD6           3        8.92       /1/1/1/
0x5ADA           3        11.08      /1/1/2/
0x5ADE           3        7          /1/1/3/
0x5B40           2        182.18     /1/2/
0x5B56           3        233.71     /1/2/1/
0x5B5A           3        227.27     /1/2/2/
0x5BC0           2        45.4       /1/3/
0x68             1        NULL       /2/
0x6AC0           2        8.5        /2/1/
0x6B40           2        2.17       /2/2/
0x6BC0           2        8.91       /2/3/
0x6C20           2        1.75       /2/4/
0x6C60           2        60.25      /2/5/

Второй набор записей (задачи с промежуточными итогами для каждой задачи):

NodeText    DurationTotal
--------    -------------
/            809.29
/1/          727.71
/1/1/        39.15
/1/1/1/      8.92
/1/1/2/      11.08
/1/1/3/      7
/1/2/        643.16
/1/2/1/      233.71
/1/2/2/      227.27
/1/3/        45.4
/2/          81.58
/2/1/        8.5
/2/2/        2.17
/2/3/        8.91
/2/4/        1.75
/2/5/        60.25
...