#sql-server #tsql #common-table-expression
#sql-сервер #tsql #common-table-expression
Вопрос:
У меня есть структура дерева в одной таблице. Таблица представляет собой дерево категорий, которые могут быть вложены бесконечно. Каждая категория имеет столбец productCount, в котором указывается, сколько продуктов находится непосредственно в категории (не суммируя дочерние категории).
Id | ParentId | Name | ProductCount
------------------------------------
1 | -1 | Cars | 0
2 | -1 | Bikes | 1
3 | 1 | Ford | 10
4 | 3 | Mustang | 7
5 | 3 | Focus | 4
Я хотел бы сделать sql-запрос, который для каждой строки / категории дает мне количество продуктов, включая продукты в дочерних категориях.
Вывод для приведенной выше таблицы должен быть
Id | ParentId | Name | ProductCount | ProductCountIncludingChildren
--------------------------------------------------------------------------
1 | -1 | Cars | 0 | 21
2 | -1 | Bikes | 1 | 1
3 | 1 | Ford | 10 | 21
4 | 3 | Mustang | 7 | 7
5 | 3 | Focus | 4 | 4
Я знаю, что, вероятно, мне следует использовать CTE, но не могу заставить его работать так, как нужно.
Любая помощь приветствуется!
Комментарии:
1. Что вы пробовали до сих пор? Отправьте свой запрос…
2. Пробовал CTE, но не смог заставить его правильно суммировать
Ответ №1:
Вы можете использовать рекурсивный CTE, где вы в привязочной части получаете все строки, а в рекурсивной части объединяетесь, чтобы получить дочерние строки. Запомните исходный Id
псевдоним RootID
из привязочной части и выполните агрегирование суммы в основном запросе, сгруппированном по RootID
.
Настройка схемы MS SQL Server 2012:
create table T
(
Id int primary key,
ParentId int,
Name varchar(10),
ProductCount int
);
insert into T values
(1, -1, 'Cars', 0),
(2, -1, 'Bikes', 1),
(3, 1, 'Ford', 10),
(4, 3, 'Mustang', 7),
(5, 3, 'Focus', 4);
create index IX_T_ParentID on T(ParentID) include(ProductCount, Id);
Запрос 1:
with C as
(
select T.Id,
T.ProductCount,
T.Id as RootID
from T
union all
select T.Id,
T.ProductCount,
C.RootID
from T
inner join C
on T.ParentId = C.Id
)
select T.Id,
T.ParentId,
T.Name,
T.ProductCount,
S.ProductCountIncludingChildren
from T
inner join (
select RootID,
sum(ProductCount) as ProductCountIncludingChildren
from C
group by RootID
) as S
on T.Id = S.RootID
order by T.Id
option (maxrecursion 0)
| ID | PARENTID | NAME | PRODUCTCOUNT | PRODUCTCOUNTINCLUDINGCHILDREN |
|----|----------|---------|--------------|-------------------------------|
| 1 | -1 | Cars | 0 | 21 |
| 2 | -1 | Bikes | 1 | 1 |
| 3 | 1 | Ford | 10 | 21 |
| 4 | 3 | Mustang | 7 | 7 |
| 5 | 3 | Focus | 4 | 4 |
Комментарии:
1. Этот рекурсивный CTE имеет очень плохое масштабирование, поскольку он, по сути, копирует конечное значение для всех родителей, непосредственно и далее по дереву (например, копирует productCount из Mustang в каждый из Ford и Cars). Я попробовал это на наборе данных около 200, и результирующий набор CTE увеличился примерно до 100 тыс. строк, и это заняло около полминуты.
2. @Elaskanator спасибо за попытку, я хочу сделать что-то подобное примерно для 3 миллионов наборов. Просто мурашки по коже, думая о моем наборе результатов CTE.
Ответ №2:
Это та же концепция, что и ответ Тома, но меньше кода (и намного быстрее).
with cte as
(
select v.Id, v.ParentId, v.Name, v.ProductCount,
cast('/' cast(v.Id as varchar) '/' as varchar) Node
from Vehicle v
where ParentId = -1
union all
select v.Id, v.ParentId, v.Name, v.ProductCount,
cast(c.Node CAST(v.Id as varchar) '/' as varchar)
from Vehicle v
join cte c on v.ParentId = c.Id
)
select c1.Id, c1.ParentId, c1.Name, c1.ProductCount,
c1.ProductCount SUM(isnull(c2.ProductCount, 0)) ProductCountIncludingChildren
from cte c1
left outer join cte c2 on c1.Node <> c2.Node and left(c2.Node, LEN(c1.Node)) = c1.Node
group by c1.Id, c1.ParentId, c1.Name, c1.ProductCount
order by c1.Id
SQL Fiddle (я добавил несколько дополнительных строк данных для тестирования)
Комментарии:
1. При приведении к
varchar
без указания длины строки вы получите значение по умолчанию в 30 символов. Этого может быть достаточно, но я думаю, что лучше четко указать, какую длину строки вы на самом деле хотите использовать.2. Это правда. Я не знаю, как выглядят его фактические данные, поэтому я не интересовался подобными деталями.
3. Ну, он сказал, что «таблица — это дерево категорий, которые могут быть вложены бесконечно». Что, конечно, не буквально верно, но это может сделать дерево довольно глубоким .
4. Я признаю, что это не идеальное решение. Ваш ответ пока лучший.
Ответ №3:
На самом деле это может быть хорошим использованием HIERARCHYID в SQL Server..
CREATE TABLE [dbo].[CategoryTree]
(
[Id] INT,
[ParentId] INT,
[Name] VARCHAR(100),
[ProductCount] INT
)
GO
INSERT [dbo].[CategoryTree]
VALUES
(1, -1, 'Cars', 0),
(2, -1, 'Bikes', 1),
(3, 1, 'Ford', 10),
(4, 3, 'Mustang', 7),
(5, 3, 'Focus', 4)
--,(6, 1, 'BMW', 100)
GO
Запрос
WITH [cteRN] AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY [ParentId] ORDER BY [ParentId]) AS [ROW_NUMBER]
FROM [dbo].[CategoryTree]
),
[cteHierarchy] AS (
SELECT CAST(
CAST(hierarchyid::GetRoot() AS VARCHAR(100))
CAST([ROW_NUMBER] AS VARCHAR(100))
'/' AS HIERARCHYID
) AS [Node],
*
FROM [cteRN]
WHERE [ParentId] = -1
UNION ALL
SELECT CAST(
hierarchy.Node.ToString()
CAST(RN.[ROW_NUMBER] AS VARCHAR(100)
) '/' AS HIERARCHYID),
rn.*
FROM [cteRN] rn
INNER JOIN [cteHierarchy] hierarchy
ON rn.[ParentId] = hierarchy.[Id]
)
SELECT x.[Node].ToString() AS [Node],
x.[Id], x.[ParentId], x.[Name], x.[ProductCount],
x.[ProductCount] SUM(ISNULL(child.[ProductCount],0))
AS [ProductCountIncludingChildren]
FROM [cteHierarchy] x
LEFT JOIN [cteHierarchy] child
ON child.[Node].IsDescendantOf(x.[Node]) = 1
AND child.[Node] <> x.[Node]
GROUP BY x.[Node], x.[Id], x.[ParentId], x.[Name], x.[ProductCount]
ORDER BY x.[Id]
Результат
Комментарии:
1. Обратите внимание, что большая часть запроса касается только настройки столбца HierarchyId «Node». Если бы вы могли хранить данные в столбце HierarchyId, то окончательный запрос должен быть довольно быстрым..
2. Для актуальной проблемы в этом сообщении приведенное выше решение работает так же хорошо и намного проще, но использование HierarchyId позволяет суммировать по уровню, что намного лучше imo.
Ответ №4:
Это не будет оптимальным, но оно работает, однако оно включает в себя 2 CTE. 1 основной CTE и CTE в табличной функции для суммирования значений для каждого поддерева.
Первый CTE
;WITH cte
AS
(
SELECT
anchor.Id,
anchor.ParentId,
anchor.Name,
anchor.ProductCount,
s.Total AS ProductCountIncludingChildren
FROM
testTable anchor
CROSS APPLY SumChild(anchor.id) s
WHERE anchor.parentid = -1
UNION ALL
SELECT
child.Id,
child.ParentId,
child.Name,
child.ProductCount,
s.Total AS ProductCountIncludingChildren
FROM
cte
INNER JOIN testTable child on child.parentid = cte.id
CROSS APPLY SumChild(child.id) s
)
SELECT * from cte
И функция
CREATE FUNCTION SumChild
(
@id int
)
RETURNS TABLE
AS
RETURN
(
WITH cte
AS
(
SELECT
anchor.Id,
anchor.ParentId,
anchor.ProductCount
FROM
testTable anchor
WHERE anchor.id = @id
UNION ALL
SELECT
child.Id,
child.ParentId,
child.ProductCount
FROM
cte
INNER JOIN testTable child on child.parentid = cte.id
)
SELECT SUM(ProductCount) AS Total from CTE
)
GO
Что приводит к:
из исходной таблицы
Извиняюсь за форматирование.
Ответ №5:
Я не смог придумать хороший ответ на основе набора на основе T-SQL, но я придумал ответ: временная таблица имитирует структуру вашей таблицы. Табличная переменная является рабочей таблицей.
--Initial table
CREATE TABLE #products (Id INT, ParentId INT, NAME VARCHAR(255), ProductCount INT)
INSERT INTO #products
( ID,ParentId, NAME, ProductCount )
VALUES ( 1,-1,'Cars',0),(2,-1,'Bikes',1),(3,1,'Ford',10),(4,3,'Mustang',7),(5,3,'Focus',4)
--Work table
DECLARE @products TABLE (ID INT, ParentId INT, NAME VARCHAR(255), ProductCount INT, ProductCountIncludingChildren INT)
INSERT INTO @products
( ID ,
ParentId ,
NAME ,
ProductCount ,
ProductCountIncludingChildren
)
SELECT Id ,
ParentId ,
NAME ,
ProductCount,
0
FROM #products
DECLARE @i INT
SELECT @i = MAX(id) FROM @products
--Stupid loop - loops suck
WHILE @i > 0
BEGIN
WITH cte AS (SELECT ParentId, SUM(ProductCountIncludingChildren) AS ProductCountIncludingChildren FROM @products GROUP BY ParentId)
UPDATE p1
SET p1.ProductCountIncludingChildren = p1.ProductCount isnull(p2.ProductCountIncludingChildren,0)
FROM @products p1
LEFT OUTER JOIN cte p2 ON p1.ID = p2.ParentId
WHERE p1.ID = @i
SELECT @i = @i - 1
END
SELECT *
FROM @products
DROP TABLE #products
Мне было бы очень интересно увидеть лучший подход, основанный на множестве. Проблема, с которой я столкнулся, заключается в том, что при использовании рекурсивных cte вы начинаете с родительского элемента и переходите к дочерним элементам — на самом деле это не работает для получения суммы на родительских уровнях. Вам нужно будет выполнить какой-то обратный рекурсивный cte.
Комментарии:
1. Вы можете начать с нижней части дерева и работать в рекурсивном CTE, используя что-то вроде
SELECT leafNodes.* FROM [dbo].[CategoryTree] leafNodes LEFT JOIN [dbo].[CategoryTree] children ON children.[ParentId] = leafNodes.[Id] WHERE children.[Id] IS NULL
в качестве привязки2. Проблема в том, что вы не можете использовать GROUP BY и aggregation в рекурсивном элементе CTE. Единственное, что я мог придумать, это рекурсивный CTE в скалярной функции, которая по сути такая же, как при использовании цикла.
3. Я думаю, что у меня была та же идея, что и у вас, но я использовал функцию табличного значения (что не нужно, см. Выше — я также отметил, что это не оптимально). Я также думал о том, чтобы идти снизу вверх, суммируя по ходу дела, но не мог понять, как это сделать быстро.