Atualize abaixo
Eu tenho uma tabela de contas com uma arquitetura típica de conta acct / pai para representar uma hierarquia de contas (SQL Server 2012). Criei uma VIEW usando um CTE para dividir a hierarquia e, no geral, ela funciona lindamente e como pretendido. Posso consultar a hierarquia em qualquer nível e ver os ramos facilmente.
Há um campo de lógica de negócios que precisa ser retornado em função da hierarquia. Um campo em cada registro de conta descreve o tamanho da empresa (vamos chamá-la CustomerCount). A lógica que preciso relatar precisa acumular o CustomerCount de toda a filial. Em outras palavras, dada uma conta, preciso resumir os valores da conta personalizada para essa conta, juntamente com todos os filhos em todos os ramos abaixo da conta ao longo da hierarquia.
Calculei com sucesso o campo usando um campo de hierarquia criado no CTE, que se parece com acct4.acct3.acct2.acct1. O problema que estou enfrentando é simplesmente fazê-lo correr rápido. Sem esse campo calculado, a consulta é executada em ~ 3 segundos. Quando adiciono o campo calculado, ele se transforma em uma consulta de 4 minutos.
Aqui está a melhor versão que pude apresentar que retorna os resultados corretos. Estou procurando idéias sobre como posso reestruturar isso COMO UMA VISTA sem sacrifícios enormes para o desempenho.
Entendo o motivo pelo qual este fica lento (requer o cálculo de um predicado na cláusula where), mas não consigo pensar em outra maneira de estruturá-lo e ainda assim obter os mesmos resultados.
Aqui está um código de exemplo para criar uma tabela e executar o CTE exatamente da mesma maneira que funciona no meu ambiente.
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 ATUALIZAÇÃO
Algumas das soluções sugeridas fizeram meus sucos fluírem, e tentei uma nova abordagem que se aproxima, mas introduz um novo / diferente obstáculo. Sinceramente, não sei se isso merece um post separado ou não, mas está relacionado à solução desse problema.
O que eu decidi foi que o que estava dificultando a soma (contagem personalizada) é a identificação de filhos no contexto de uma hierarquia que começa no topo e se acumula. Comecei criando uma hierarquia criada de baixo para cima, usando a raiz definida por "contas que não são pai de nenhuma outra conta" e fazendo a junção recursiva ao contrário (root.parentacctid = recurse.acctid)
Dessa forma, eu poderia adicionar a contagem de clientes filhos aos pais à medida que a recursão acontece. Por causa de como preciso de relatórios e níveis, eu estou fazendo esse procedimento de baixo para cima, além de cima para baixo, e apenas juntando-os via ID da conta. Essa abordagem acaba sendo muito mais rápida do que a contagem personalizada da consulta externa original, mas me deparei com alguns obstáculos.
Primeiro, eu estava capturando inadvertidamente a contagem duplicada de clientes para contas que são pai de vários filhos. Eu estava contando o dobro ou o triplo da contagem de clientes, de acordo com o número de filhos que havia. Minha solução foi criar mais um cte que conta quantos nós uma conta possui e dividir a conta acct.custom durante a recursão; portanto, quando adiciono todo o ramo, a conta não está sendo contada duas vezes.
Portanto, neste ponto, os resultados desta nova versão não estão corretos, mas eu sei o porquê. O cte bottomup está criando duplicatas. Quando a recursão passa, ela procura qualquer coisa na raiz (filhos de nível inferior) que é filho de uma conta na tabela de contas. Na terceira recursão, ele pega as mesmas contas que fez na segunda e as coloca novamente.
Idéias sobre como fazer um código ascendente ou isso faz com que outras idéias fluam?
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
fonte
Respostas:
Edit: esta é a segunda tentativa
Com base na resposta de @Max Vernon, aqui está uma maneira de ignorar o uso de CTE dentro de uma subconsulta em linha, que é como se associar à CTE e presumo que seja a razão da baixa eficiência. Ele usa funções analíticas disponíveis apenas na versão 2012 do SQL-Server. Testado no SQL-Fiddle
Esta parte pode ser pulada da leitura, é uma cópia e colagem da resposta de Max:
Aqui, ordenamos as linhas do CTE usando o
IdHierarchyMatch
e calculamos os números de linhas e um total em execução (da próxima linha até o final).Depois, temos mais um CTE intermediário, no qual usamos os totais e números de linhas anteriores - basicamente para descobrir onde os pontos finais dos ramos da estrutura da árvore:
e finalmente construímos a última parte:
E uma simplificação, usando o mesmo
cte1
código acima. Teste no SQL-Fiddle-2 . Observe que ambas as soluções funcionam sob a suposição de que você tem um máximo de quatro níveis em sua árvore:Uma terceira abordagem, com apenas um CTE, para a parte recursiva e, em seguida, apenas as funções agregadas da janela (
SUM() OVER (...)
), deve funcionar em qualquer versão a partir de 2005. Teste no SQL-Fiddle-3 Esta solução assume, como os anteriores, que existem 4 níveis no máximo na árvore da hierarquia:Uma quarta abordagem, que calcula como CTE intermediária, a tabela de fechamento da hierarquia. Teste no SQL-Fiddle-4 . O benefício é que, para os cálculos das somas, não há rescisão no número de níveis.
fonte
Eu acredito que isso deve torná-lo mais rápido:
Eu adicionei uma coluna no CTE chamada
IdHierarchyMatch
qual é a versão direta doIdHierarchy
para permitir que a cláusula deTotalCustomerCount
subconsultaWHERE
seja sargável.Comparando os custos estimados da subárvore para os planos de execução, esse caminho deve ser aproximadamente 5 vezes mais rápido.
fonte
ROW_NUMER() OVER (ORDER BY...)
coisa ou algo assim. Eu simplesmente não conseguia obter os números certos. É uma pergunta realmente ótima e interessante. Bom exercício cerebral!IdHierarchyMatch
campo, no entanto, você não pode adicionar um índice clusterizado em uma exibição vinculada ao esquema que inclua um CTE. Gostaria de saber se esta limitação é resolvido no SQL Server 2014.Eu tentei também. Não é muito bonito, mas parece ter um desempenho melhor.
fonte