Обновление ниже
У меня есть таблица учетных записей с типичной архитектурой учетных записей acct / parent для представления иерархии учетных записей (SQL Server 2012). Я создал VIEW, используя CTE для хэширования иерархии, и в целом он работает прекрасно и по назначению. Я могу запросить иерархию на любом уровне и легко увидеть ветви.
Существует одно поле бизнес-логики, которое должно быть возвращено как функция иерархии. Поле в каждой учетной записи описывает размер бизнеса (назовем его CustomerCount). Логика, о которой мне нужно сообщить, должна свернуть CustomerCount из всей ветки. Другими словами, учитывая учетную запись, мне нужно суммировать значения количества клиентов для этой учетной записи вместе с каждым дочерним элементом в каждом филиале ниже учетной записи в иерархии.
Я успешно рассчитал поле, используя иерархическое поле, построенное в CTE, которое выглядит как acct4.acct3.acct2.acct1. Проблема, с которой я сталкиваюсь, состоит в том, чтобы заставить ее работать быстро. Без этого одного вычисляемого поля запрос выполняется за ~ 3 секунды. Когда я добавляю в вычисляемое поле, оно превращается в 4-минутный запрос.
Вот лучшая версия, которую я смог придумать, которая возвращает правильные результаты. Я ищу идеи о том, как я могу реструктурировать этот AS A VIEW без таких огромных жертв производительности.
Я понимаю причину, по которой это происходит медленно (требует вычисления предиката в предложении where), но я не могу придумать другой способ структурировать его и при этом получить те же результаты.
Вот пример кода для построения таблицы и выполнения CTE в точности так, как он работает в моей среде.
Use Tempdb
go
CREATE TABLE dbo.Account
(
Acctid varchar(1) NOT NULL
, Name varchar(30) NULL
, ParentId varchar(1) NULL
, CustomerCount int NULL
);
INSERT Account
SELECT 'A','Best Bet',NULL,21 UNION ALL
SELECT 'B','eStore','A',30 UNION ALL
SELECT 'C','Big Bens','B',75 UNION ALL
SELECT 'D','Mr. Jimbo','B',50 UNION ALL
SELECT 'E','Dr. John','C',100 UNION ALL
SELECT 'F','Brick','A',222 UNION ALL
SELECT 'G','Mortar','C',153 ;
With AccountHierarchy AS
( --Root values have no parent
SELECT
Root.AcctId AccountId
, Root.Name AccountName
, Root.ParentId ParentId
, 1 HierarchyLevel
, cast(Root.Acctid as varchar(4000)) IdHierarchy --highest parent reads right to left as in id3.Acctid2.Acctid1
, cast(replace(Root.Name,'.','') as varchar(4000)) NameHierarchy --highest parent reads right to left as in name3.name2.name1 (replace '.' so name parse is easy in last step)
, cast(Root.Acctid as varchar(4000)) HierarchySort --reverse of above, read left to right name1.name2.name3 for sorting on reporting only
, cast(Root.Name as varchar(4000)) HierarchyLabel --use for labels on reporting only, indents names under sorted hierarchy
, Root.CustomerCount CustomerCount
FROM
tempdb.dbo.account Root
WHERE
Root.ParentID is null
UNION ALL
SELECT
Recurse.Acctid AccountId
, Recurse.Name AccountName
, Recurse.ParentId ParentId
, Root.HierarchyLevel + 1 HierarchyLevel --next level in hierarchy
, cast(cast(recurse.Acctid as varchar(40)) + '.' + Root.IdHierarchy as varchar(4000)) IdHierarchy --cast because in real system this is a uniqueidentifier type needs converting
, cast(replace(recurse.Name,'.','') + '.' + Root.NameHierarchy as varchar(4000)) NameHierarchy --replace '.' for parsing in last step, cast to make room for lots of sub levels down the hierarchy
, cast(Root.AccountName + '.' + Recurse.Name as varchar(4000)) HierarchySort
, cast(space(root.HierarchyLevel * 4) + Recurse.Name as varchar(4000)) HierarchyLabel
, Recurse.CustomerCount CustomerCount
FROM
tempdb.dbo.account Recurse INNER JOIN
AccountHierarchy Root on Root.AccountId = Recurse.ParentId
)
SELECT
hier.AccountId
, Hier.AccountName
, hier.ParentId
, hier.HierarchyLevel
, hier.IdHierarchy
, hier.NameHierarchy
, hier.HierarchyLabel
, parsename(hier.IdHierarchy,1) Acct1Id
, parsename(hier.NameHierarchy,1) Acct1Name --This is why we stripped out '.' during recursion
, parsename(hier.IdHierarchy,2) Acct2Id
, parsename(hier.NameHierarchy,2) Acct2Name
, parsename(hier.IdHierarchy,3) Acct3Id
, parsename(hier.NameHierarchy,3) Acct3Name
, parsename(hier.IdHierarchy,4) Acct4Id
, parsename(hier.NameHierarchy,4) Acct4Name
, hier.CustomerCount
/* fantastic up to this point. Next block of code is what causes problem.
Logic of code is "sum of CustomerCount for this location and all branches below in this branch of hierarchy"
In live environment, goes from taking 3 seconds to 4 minutes by adding this one calc */
, (
SELECT
sum(children.CustomerCount)
FROM
AccountHierarchy Children
WHERE
hier.IdHierarchy = right(children.IdHierarchy, (1 /*length of id field*/ * hier.HierarchyLevel) + hier.HierarchyLevel - 1 /*for periods inbetween ids*/)
--"where this location's idhierarchy is within child idhierarchy"
--previously tried a charindex(hier.IdHierarchy,children.IdHierarchy)>0, but that performed even worse
) TotalCustomerCount
FROM
AccountHierarchy hier
ORDER BY
hier.HierarchySort
drop table tempdb.dbo.Account
20.11.2013 ОБНОВЛЕНИЕ
Некоторые из предложенных решений заставили мои соки течь, и я попробовал новый подход, который приближается, но вводит новое / другое препятствие. Честно говоря, я не знаю, заслуживает ли это отдельного поста или нет, но это связано с решением этой проблемы.
Я решил, что то, что усложняло сумму (количество клиентов), - это идентификация детей в контексте иерархии, которая начинается сверху и строится вниз. Поэтому я начал с создания иерархии, которая строится снизу вверх, используя корень, определенный как «учетные записи, которые не являются родительскими для любой другой учетной записи», и выполняю рекурсивное объединение в обратном направлении (root.parentacctid = recurse.acctid)
Таким образом, я мог бы просто добавить количество дочерних клиентов к родителю, когда произойдет рекурсия. Из-за того, что мне нужны отчеты и уровни, я делаю это снизу вверх в дополнение к сверху вниз, а затем просто присоединяюсь к ним через идентификатор учетной записи. Этот подход оказывается намного быстрее, чем исходный клиентский счет внешнего запроса, но я столкнулся с несколькими препятствиями.
Во-первых, я случайно собирал дубликаты клиентов для учетных записей, которые являются родительскими для нескольких детей. Я был в два или три раза больше подсчитывал количество клиентов по количеству детей, которых там было. Мое решение состояло в том, чтобы создать еще один cte, который подсчитывает, сколько узлов имеет acct, и делит acct.customercount во время рекурсии, поэтому, когда я складываю всю ветвь, acct не учитывается дважды.
Так что на данный момент результаты этой новой версии не верны, но я знаю, почему. Внизу cte создает дубликаты. Когда рекурсия проходит, она ищет в корне все, что является дочерним для учетной записи в таблице учетных записей. В третьей рекурсии он берет те же учетные записи, что и во второй, и вводит их снова.
Идеи о том, как сделать снизу вверх, или это дает какие-то другие идеи течет?
Use Tempdb
go
CREATE TABLE dbo.Account
(
Acctid varchar(1) NOT NULL
, Name varchar(30) NULL
, ParentId varchar(1) NULL
, CustomerCount int NULL
);
INSERT Account
SELECT 'A','Best Bet',NULL,1 UNION ALL
SELECT 'B','eStore','A',2 UNION ALL
SELECT 'C','Big Bens','B',3 UNION ALL
SELECT 'D','Mr. Jimbo','B',4 UNION ALL
SELECT 'E','Dr. John','C',5 UNION ALL
SELECT 'F','Brick','A',6 UNION ALL
SELECT 'G','Mortar','C',7 ;
With AccountHierarchy AS
( --Root values have no parent
SELECT
Root.AcctId AccountId
, Root.Name AccountName
, Root.ParentId ParentId
, 1 HierarchyLevel
, cast(Root.Acctid as varchar(4000)) IdHierarchy --highest parent reads right to left as in id3.Acctid2.Acctid1
, cast(replace(Root.Name,'.','') as varchar(4000)) NameHierarchy --highest parent reads right to left as in name3.name2.name1 (replace '.' so name parse is easy in last step)
, cast(Root.Acctid as varchar(4000)) HierarchySort --reverse of above, read left to right name1.name2.name3 for sorting on reporting only
, cast(Root.Acctid as varchar(4000)) HierarchyMatch
, cast(Root.Name as varchar(4000)) HierarchyLabel --use for labels on reporting only, indents names under sorted hierarchy
, Root.CustomerCount CustomerCount
FROM
tempdb.dbo.account Root
WHERE
Root.ParentID is null
UNION ALL
SELECT
Recurse.Acctid AccountId
, Recurse.Name AccountName
, Recurse.ParentId ParentId
, Root.HierarchyLevel + 1 HierarchyLevel --next level in hierarchy
, cast(cast(recurse.Acctid as varchar(40)) + '.' + Root.IdHierarchy as varchar(4000)) IdHierarchy --cast because in real system this is a uniqueidentifier type needs converting
, cast(replace(recurse.Name,'.','') + '.' + Root.NameHierarchy as varchar(4000)) NameHierarchy --replace '.' for parsing in last step, cast to make room for lots of sub levels down the hierarchy
, cast(Root.AccountName + '.' + Recurse.Name as varchar(4000)) HierarchySort
, CAST(CAST(Root.HierarchyMatch as varchar(40)) + '.'
+ cast(recurse.Acctid as varchar(40)) as varchar(4000)) HierarchyMatch
, cast(space(root.HierarchyLevel * 4) + Recurse.Name as varchar(4000)) HierarchyLabel
, Recurse.CustomerCount CustomerCount
FROM
tempdb.dbo.account Recurse INNER JOIN
AccountHierarchy Root on Root.AccountId = Recurse.ParentId
)
, Nodes as
( --counts how many branches are below for any account that is parent to another
select
node.ParentId Acctid
, cast(count(1) as float) Nodes
from AccountHierarchy node
group by ParentId
)
, BottomUp as
( --creates the hierarchy starting at accounts that are not parent to any other
select
Root.Acctid
, root.ParentId
, cast(isnull(root.customercount,0) as float) CustomerCount
from
tempdb.dbo.Account Root
where
not exists ( select 1 from tempdb.dbo.Account OtherAccts where root.Acctid = OtherAccts.ParentId)
union all
select
Recurse.Acctid
, Recurse.ParentId
, root.CustomerCount + cast ((isnull(recurse.customercount,0) / nodes.nodes) as float) CustomerCount
-- divide the recurse customercount by number of nodes to prevent duplicate customer count on accts that are parent to multiple children, see customercount cte next
from
tempdb.dbo.Account Recurse inner join
BottomUp Root on root.ParentId = recurse.acctid inner join
Nodes on nodes.Acctid = recurse.Acctid
)
, CustomerCount as
(
select
sum(CustomerCount) TotalCustomerCount
, hier.acctid
from
BottomUp hier
group by
hier.Acctid
)
SELECT
hier.AccountId
, Hier.AccountName
, hier.ParentId
, hier.HierarchyLevel
, hier.IdHierarchy
, hier.NameHierarchy
, hier.HierarchyLabel
, hier.hierarchymatch
, parsename(hier.IdHierarchy,1) Acct1Id
, parsename(hier.NameHierarchy,1) Acct1Name --This is why we stripped out '.' during recursion
, parsename(hier.IdHierarchy,2) Acct2Id
, parsename(hier.NameHierarchy,2) Acct2Name
, parsename(hier.IdHierarchy,3) Acct3Id
, parsename(hier.NameHierarchy,3) Acct3Name
, parsename(hier.IdHierarchy,4) Acct4Id
, parsename(hier.NameHierarchy,4) Acct4Name
, hier.CustomerCount
, customercount.TotalCustomerCount
FROM
AccountHierarchy hier inner join
CustomerCount on customercount.acctid = hier.accountid
ORDER BY
hier.HierarchySort
drop table tempdb.dbo.Account
источник
Ответы:
Изменить: это вторая попытка
Основываясь на ответе @Max Vernon, вот способ обойти использование CTE внутри встроенного подзапроса, что похоже на самостоятельное присоединение к CTE, и я полагаю, это является причиной низкой эффективности. Он использует аналитические функции, доступные только в версии 2012 SQL-Server. Проверено на SQL-Fiddle
Эту часть можно пропустить из чтения, это копия из ответа Макса:
Здесь мы упорядочиваем строки CTE с помощью
IdHierarchyMatch
и вычисляем номера строк и промежуточный итог (от следующей строки до конца).Затем у нас есть еще один промежуточный CTE, где мы используем предыдущие промежуточные итоги и номера строк - в основном, чтобы найти конечные точки для ветвей древовидной структуры:
и наконец мы строим последнюю часть:
И упрощение, используя тот же
cte1
код, что и выше. Тест на SQL-Fiddle-2 . Обратите внимание, что оба решения работают при условии, что у вас есть максимум четыре уровня в вашем дереве:Третий подход, только с одним CTE, для рекурсивной части, а затем только для оконных агрегатных функций (
SUM() OVER (...)
), поэтому он должен работать в любой версии, начиная с 2005 года. Тестирование в SQL-Fiddle-3 В этом решении, как и в предыдущих, предполагается, что в иерархическом дереве максимум 4 уровня:Четвертый подход, который вычисляет в качестве промежуточного CTE таблицу закрытия иерархии. Тест на SQL-Fiddle-4 . Преимущество состоит в том, что для расчета сумм не существует ограничений на количество уровней.
источник
Я считаю, что это должно сделать это быстрее:
Я добавил столбец в CTE с именем,
IdHierarchyMatch
который является прямой версией,IdHierarchy
чтобы можно было использовать условиеTotalCustomerCount
подзапросаWHERE
.Сравнивая предполагаемые затраты на поддерево для планов выполнения, этот путь должен быть примерно в 5 раз быстрее.
источник
ROW_NUMER() OVER (ORDER BY...)
или чего-то еще. Я просто не мог получить правильные цифры из этого. Это действительно замечательный и интересный вопрос. Хорошее упражнение для мозга!IdHierarchyMatch
поле, однако вы не можете добавить кластеризованный индекс в привязанном к схеме представлении, которое включает CTE. Интересно, разрешено ли это ограничение в SQL Server 2014.Я тоже дал ему шанс. Это не очень красиво, но, кажется, работает лучше.
источник