У меня проблема с запросом к одной из наших баз данных 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
Буду очень признателен за некоторые комментарии относительно того, подходит ли это подход или нет.