Проблема рекурсивного запроса SQL Server - PullRequest
1 голос
/ 06 февраля 2012

У меня проблема с запросом к одной из наших баз данных MS SQL Server. Следующие таблицы и представления для краткости упрощены, но должны служить для описания проблемы.

Оценка каждой таблицы составляется как среднее число ее непосредственных детей. Представления достаточны для фиксированных структур, но они становятся более сложными с иерархией расположения, которая в настоящее время находится во вложенной форме набора. Не существует фиксированного количества уровней в иерархии местоположений, поскольку они определяются пользователем.

Я пытался решить эту проблему с помощью рекурсивного CTE, но они не допускают агрегацию в рекурсивной части.

CREATE TABLE [dbo].[locations_main](
    [id] [smallint] NOT NULL,
    [name] [nchar](50) NOT NULL,
    [lft] [smallint] NOT NULL,
    [rgt] [smallint] NOT NULL,
    [parent_id] [smallint] NULL,
    CONSTRAINT [PK_locations_main] PRIMARY KEY CLUSTERED ([id] ASC)
)
GO

INSERT INTO [dbo].[locations_main] VALUES
    (1, 'location 1', 1, 16, NULL),
    (2, 'location 1-1', 2, 9, 1),
    (3, 'location 1-1-1', 3, 4, 2),
    (4, 'location 1-1-2', 5, 6, 2),
    (5, 'location 1-1-3', 7, 8, 2),
    (7, 'location 1-2', 10, 15, 1),
    (8, 'location 1-2-1', 11, 12, 7),
    (9, 'location 1-2-2', 13, 14, 7)
GO

CREATE TABLE [dbo].[outcomes](
    [id] [smallint] NOT NULL,
    [location_id] [smallint] NOT NULL,
    [name] [nvarchar](50) NOT NULL,
 CONSTRAINT [PK_outcomes] PRIMARY KEY CLUSTERED ([id] ASC)
)
GO

INSERT INTO [dbo].[outcomes] VALUES
    (1, 3, 'outcome 1'),
    (2, 4, 'outcome 2'),
    (3, 5, 'outcome 3'),
    (4, 8, 'outcome 4'),
    (5, 9, 'outcome 5')
GO

CREATE TABLE [dbo].[prompts](
    [id] [smallint] NOT NULL,
    [outcome_id] [smallint] NOT NULL,
    [name] [nvarchar](50) NOT NULL,
 CONSTRAINT [PK_prompts] PRIMARY KEY CLUSTERED ([id] ASC)
)
GO

INSERT INTO [dbo].[prompts] VALUES
    (1, 1, 'prompt 1'),
    (2, 2, 'prompt 2'),
    (3, 3, 'prompt 3'),
    (4, 4, 'prompt 4'),
    (5, 5, 'prompt 5')
GO

CREATE TABLE [dbo].[subprompts](
    [id] [smallint] NOT NULL,
    [prompt_id] [smallint] NOT NULL,
    [name] [nvarchar](50) NOT NULL,
    [score] [smallint] NOT NULL,
 CONSTRAINT [PK_subprompts] PRIMARY KEY CLUSTERED ([id] ASC)
)
GO

INSERT INTO [dbo].[subprompts] VALUES
    (1, 1, 'subprompt 1', 1),
    (2, 1, 'subprompt 2', 1),
    (3, 2, 'subprompt 3', 1),
    (4, 2, 'subprompt 4', 3),
    (5, 3, 'subprompt 5', 2),
    (6, 3, 'subprompt 6', 4),
    (7, 4, 'subprompt 7', 1),
    (8, 4, 'subprompt 8', 5),
    (9, 5, 'subprompt 9', 3),
    (10, 5, 'subprompt 10', 3)
GO

CREATE VIEW [dbo].[vw_prompts]
AS
SELECT
    dbo.prompts.id,
    dbo.prompts.outcome_id,
    dbo.prompts.name,
    AVG(dbo.subprompts.score) AS score
FROM dbo.prompts
LEFT OUTER JOIN dbo.subprompts
    ON dbo.prompts.id = dbo.subprompts.prompt_id
GROUP BY
    dbo.prompts.id,
    dbo.prompts.outcome_id,
    dbo.prompts.name
GO

CREATE VIEW [dbo].[vw_outcomes]
AS
SELECT
    dbo.outcomes.id,
    dbo.outcomes.location_id,
    dbo.outcomes.name,
    AVG(dbo.vw_prompts.score) AS score
FROM dbo.outcomes
LEFT OUTER JOIN dbo.vw_prompts
    ON dbo.outcomes.id = dbo.vw_prompts.id
GROUP BY
    dbo.outcomes.id,
    dbo.outcomes.location_id,
    dbo.outcomes.name
GO

Приведенный ниже запрос извлекает все местоположения, но вычисляет средние значения из конечных узлов, а не непосредственных потомков рассматриваемого местоположения -

SELECT loc_main_ag.name, AVG(CAST(vw_outcomes.score AS FLOAT))
FROM locations_main loc_main_ag
LEFT JOIN locations_main loc_main
    ON loc_main_ag.lft <= loc_main.lft
    AND loc_main_ag.rgt >= loc_main.rgt
INNER JOIN vw_outcomes
    ON loc_main.id = vw_outcomes.location_id
GROUP BY loc_main_ag.name

возвращает

location 1       2.4
location 1-1     2
location 1-1-1   1
location 1-1-2   2
location 1-1-3   3
location 1-2     3
location 1-2-1   3
location 1-2-2   3

«местоположение 1» имеет среднее из «местоположения 1-1-1», «местоположения 1-1-2», «местоположения 1-1-3», «местоположения 1-2-1» и «местоположения 1» -2-2 "- (1 + 2 + 3 + 3 + 3) / 5 = 2,4 вместо среднего значения для" местоположения 1-1 "и" местоположения 1-2 "- (2 + 3) / 2 = 2,5

Я пытался решить эту проблему с помощью CTE, но столкнулся с проблемой при использовании GROUP BY и агрегатных функций в рекурсивной части CTE -

WITH location_scores
AS
(
-- Anchor member definition
-- Get score for all leaf node locations
SELECT locations_main.id, locations_main.name, locations_main.parent_id, AVG(CAST(vw_outcomes.score AS FLOAT)) AS score
FROM locations_main
INNER JOIN vw_outcomes
    ON locations_main.id = vw_outcomes.location_id
WHERE locations_main.rgt - locations_main.lft = 1
GROUP BY locations_main.id, locations_main.name, locations_main.parent_id

UNION ALL

-- Recursive member definition
-- Rollup through locations parents to build averages
SELECT locations_main.id, locations_main.name, locations_main.parent_id, AVG(CAST(location_scores.score AS FLOAT)) AS score
FROM locations_main
INNER JOIN vw_outcomes
    ON locations_main.id = vw_outcomes.location_id
INNER JOIN location_scores
    ON locations_main.id = location_scores.parent_id
GROUP BY locations_main.id, locations_main.name, locations_main.parent_id

)
-- Statement that executes the CTE
SELECT *
FROM location_scores

ОБНОВЛЕНИЕ : Вот моя попытка использовать табличную функцию. Он возвращает правильные результаты на основе упрощенного примера, включенного здесь, но я обеспокоен тем, как это будет масштабироваться. Иерархия, против которой он будет выполняться в дикой природе, может иметь где-то около 15 ^ 5 записей.

CREATE FUNCTION scores () RETURNS
    @result TABLE
    (
        id              SMALLINT,
        name            NVARCHAR(50),
        lft             SMALLINT,
        rgt             SMALLINT,
        parent_id       SMALLINT,
        score           FLOAT,
        [level]         SMALLINT
    ) AS
BEGIN
    DECLARE @level INT
    SET @level = 1

    INSERT INTO @result
        SELECT
            locations_main.id,
            locations_main.name,
            locations_main.lft,
            locations_main.rgt,
            locations_main.parent_id,
            AVG(CAST(vw_outcomes.score AS FLOAT)) AS score,
            @level AS [level]
        FROM locations_main
        INNER JOIN vw_outcomes 
            ON locations_main.id = vw_outcomes.location_id 
        WHERE locations_main.rgt - locations_main.lft = 1 
        GROUP BY
            locations_main.id,
            locations_main.name,
            locations_main.lft,
            locations_main.rgt,
            locations_main.parent_id

    WHILE ( SELECT COUNT(*) FROM @result WHERE level = @level AND parent_id IS NOT NULL ) > 0 BEGIN

        INSERT INTO @result
        SELECT
            locations_main.id,
            locations_main.name,
            locations_main.lft,
            locations_main.rgt,
            locations_main.parent_id,
            AVG(CAST(res.score AS FLOAT)) AS score,
            (@level + 1) AS [level]
        FROM locations_main
        INNER JOIN @result res
            ON locations_main.id = res.parent_id
            AND res.level = @level
        GROUP BY
            locations_main.id,
            locations_main.name,
            locations_main.lft,
            locations_main.rgt,
            locations_main.parent_id

        SET @level = @level + 1

    END

RETURN
END

Буду очень признателен за некоторые комментарии относительно того, подходит ли это подход или нет.

Ответы [ 2 ]

0 голосов
/ 09 февраля 2012

Это функция с табличным значением, которую я придумал.Я не думаю, что это будет кому-нибудь полезно из-за характера проблемы, но я включаю ее сюда для полноты картины.В общей сложности 920 тыс. Записей по всей иерархии это возвращает за 3 секунды на нашем сервере разработки.

CREATE FUNCTION cqc_location_template_scores (@location_id INT = NULL) RETURNS
    @result TABLE
    (
        id              SMALLINT,
        name            NVARCHAR(50),
        lft             SMALLINT,
        rgt             SMALLINT,
        parent_id       SMALLINT,
        score           FLOAT,
        [level]         SMALLINT
    ) AS
BEGIN
    DECLARE @level INT
    SET @level = 1

    DECLARE @lft INT, @rgt INT
    IF (@location_id IS NOT NULL)
        SELECT @lft = lft, @rgt = rgt FROM locations_main WHERE id = @location_id
    ELSE
        SELECT @lft = NULL, @rgt = NULL

    DECLARE @ROLLUP_TYPE VARCHAR(50)
    SELECT @ROLLUP_TYPE = parmvalue FROM globals WHERE parameter = 'CQC_ROLLUP_TYPE'

    -- TEST TO GUARD AGAINST INFINITE LOOP CAUSED BY LOCATIONS_MAIN RECORD BEING ITS OWN PARENT
    IF ((SELECT COUNT(*) FROM locations_main WHERE id = parent_id) > 0)
        RETURN


    INSERT INTO @result
        SELECT
            locations_main.id,
            locations_main.name,
            locations_main.lft,
            locations_main.rgt,
            locations_main.parent_id,

            CASE @ROLLUP_TYPE
                WHEN 'AVE' THEN CAST(ROUND(AVG(CAST(CODE_CQC_STATUS.cod_score AS FLOAT)), 0) AS INT) 
                WHEN 'WORST' THEN MIN(CODE_CQC_STATUS.cod_score)
                ELSE NULL
            END AS score,

            @level AS [level]
        FROM locations_main
        INNER JOIN cqc_outcomes 
            ON locations_main.id = cqc_outcomes.cdo_location
        INNER JOIN CODE_CQC_STATUS
            ON cqc_outcomes.cdo_status = CODE_CQC_STATUS.CODE
        WHERE locations_main.rgt - locations_main.lft = 1
        AND (locations_main.lft >= @lft OR @lft IS NULL)
        AND (locations_main.rgt <= @rgt OR @rgt IS NULL)
        GROUP BY
            locations_main.id,
            locations_main.name,
            locations_main.lft,
            locations_main.rgt,
            locations_main.parent_id

    WHILE ( SELECT COUNT(*) FROM @result WHERE level = @level AND parent_id IS NOT NULL ) > 0 BEGIN

        INSERT INTO @result
        SELECT
            locations_main.id,
            locations_main.name,
            locations_main.lft,
            locations_main.rgt,
            locations_main.parent_id,

            CASE @ROLLUP_TYPE
                WHEN 'AVE' THEN CAST(ROUND(AVG(CAST(res.score AS FLOAT)), 0) AS INT) 
                WHEN 'WORST' THEN MIN(res.score)
                ELSE NULL
            END AS score,

            (@level + 1) AS [level]
        FROM locations_main
        INNER JOIN @result res
            ON locations_main.id = res.parent_id
            AND res.level = @level
        WHERE (locations_main.lft >= @lft OR @lft IS NULL)
        AND (locations_main.rgt <= @rgt OR @rgt IS NULL)
        GROUP BY
            locations_main.id,
            locations_main.name,
            locations_main.lft,
            locations_main.rgt,
            locations_main.parent_id

        SET @level = @level + 1

        -- TEST TO GUARD AGAINST INFINITE LOOP
        IF (@level > 10)
        BEGIN
            DELETE FROM @result
            RETURN
        END

    END

RETURN
END
GO
0 голосов
/ 07 февраля 2012

Извините, я не смог отформатировать sql-код, когда копировал его на мою SSMS

Но что я могу сказать о рекурсивных запросах в SQL Server, так это то, что они состоят из двух частей.Один якорь и одна рекурсивная часть.

Вы можете проверить статью http://www.kodyaz.com/t-sql/sql-server-recursive-query-with-recursive-cte.aspx для рекурсивных выборок

И если Group By вызывает проблему, вы можете придумать способ группировкирезультат запроса вне рекурсивной части.

...