2012-02-05 3 views
1

У меня проблема с запросом к одной из наших баз данных MS SQL Server. Следующие таблицы и представления упрощаются ради краткости, но должны служить для описания проблемы.Проблема рекурсивного запроса 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» и «locatio п 1-2" - (2 + 3)/2 = 2,5

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

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 

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

+0

Ваш вопрос не совсем ясен, потому что неясно, как выглядят ваши данные и какие результаты вы ожидаете. Можете ли вы опубликовать простой тестовый пример, который точно показывает, чего вы пытаетесь достичь? В тестовом случае нет необходимости использовать ваши реальные таблицы или данные, это просто должно проиллюстрировать ваше требование. – Pondlife

ответ

0

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

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

К сожалению, я не мог форматировать код SQL, когда я копирую его на моих SSMS

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

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

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

+0

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

Смежные вопросы