Actualización a continuación
Tengo una tabla de cuentas con una arquitectura típica de cuenta principal / cuenta para representar una jerarquía de cuentas (SQL Server 2012). Creé una VIEW usando un CTE para descifrar la jerarquía, y en general funciona de maravilla y como se esperaba. Puedo consultar la jerarquía en cualquier nivel y ver las ramas fácilmente.
Hay un campo de lógica de negocios que debe devolverse en función de la jerarquía. Un campo en cada registro de cuenta describe el tamaño de la empresa (lo llamaremos CustomerCount). La lógica sobre la que necesito informar debe acumular CustomerCount desde toda la sucursal. En otras palabras, dada una cuenta, necesito resumir los valores de cuenta de clientes para esa cuenta junto con cada hijo en cada rama debajo de la cuenta a lo largo de la jerarquía.
Calculé con éxito el campo utilizando un campo de jerarquía integrado dentro del CTE, que se parece a acct4.acct3.acct2.acct1. El problema con el que me encuentro es simplemente hacerlo correr rápido. Sin este campo calculado, la consulta se ejecuta en ~ 3 segundos. Cuando agrego el campo calculado, se convierte en una consulta de 4 minutos.
Aquí está la mejor versión que he podido encontrar que devuelve los resultados correctos. Estoy buscando ideas sobre cómo puedo reestructurar esto COMO UNA VISTA sin tan grandes sacrificios por el rendimiento.
Entiendo la razón por la cual esto va lento (requiere calcular un predicado en la cláusula where), pero no puedo pensar en otra forma de estructurarlo y aún obtener los mismos resultados.
Aquí hay un código de muestra para construir una tabla y hacer el CTE más o menos exactamente como funciona en mi entorno.
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 ACTUALIZACIÓN
Algunas de las soluciones sugeridas hicieron fluir mis jugos, y probé un nuevo enfoque que se acerca, pero presenta un obstáculo nuevo / diferente. Honestamente, no sé si esto garantiza una publicación separada o no, pero está relacionado con la solución de este problema.
Lo que decidí fue que lo que hacía difícil la suma (cuenta de clientes) es la identificación de los niños en el contexto de una jerarquía que comienza en la parte superior y se reduce. Así que comencé creando una jerarquía que se construye de abajo hacia arriba, usando la raíz definida por "cuentas que no son padre de ninguna otra cuenta" y haciendo la unión recursiva al revés (root.parentacctid = recurse.acctid)
De esta manera, podría agregar el recuento de clientes secundarios al principal a medida que ocurre la recursividad. Debido a la forma en que necesito informes y niveles, estoy haciendo esto de arriba hacia abajo además de arriba hacia abajo, luego solo me uní a ellos a través de la identificación de la cuenta. Este enfoque resulta ser mucho más rápido que la cuenta de clientes de la consulta externa original, pero me encontré con algunos obstáculos.
Primero, estaba capturando inadvertidamente el recuento de clientes duplicados para cuentas que son padres de varios hijos. Estaba contando el doble o el triple de clientes para algunos acctidos, por la cantidad de niños que había. Mi solución fue crear otro cte que cuente cuántos nodos tiene un acct y dividir el recuento.customercount durante la recursión, por lo que cuando sumo toda la rama, el acct no se cuenta dos veces.
Entonces, en este punto, los resultados de esta nueva versión no son correctos, pero sé por qué. El cte ascendente está creando duplicados. Cuando pasa la recursividad, busca cualquier cosa en la raíz (hijos de nivel inferior) que sea hijo de una cuenta en la tabla de cuentas. En la tercera recursión, recoge las mismas cuentas que hizo en la segunda y las vuelve a colocar.
¿Ideas sobre cómo hacer un cte de abajo hacia arriba, o esto hace que fluyan otras ideas?
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
fuente
Respuestas:
Editar: este es el segundo intento
Basado en la respuesta de @Max Vernon, aquí hay una manera de evitar el uso de CTE dentro de una subconsulta en línea, que es como unirse al CTE y supongo que es la razón de la baja eficiencia. Utiliza funciones analíticas disponibles solo en la versión 2012 de SQL-Server. Probado en SQL-Fiddle
Esta parte se puede omitir de la lectura, es una copia y pega de la respuesta de Max:
Aquí ordenamos las filas del CTE usando
IdHierarchyMatch
y calculamos los números de fila y un total acumulado (desde la siguiente fila hasta el final).Luego tenemos un CTE intermedio más donde usamos los totales acumulados anteriores y los números de fila, básicamente para encontrar dónde apuntan los extremos de las ramas de la estructura de árbol:
y finalmente construimos la última parte:
Y una simplificación, usando lo mismo
cte1
que el código anterior. Prueba en SQL-Fiddle-2 . Tenga en cuenta que ambas soluciones funcionan bajo el supuesto de que tiene un máximo de cuatro niveles en su árbol:Un tercer enfoque, con un solo CTE, para la parte recursiva y luego solo funciones de agregado de ventana (
SUM() OVER (...)
), por lo que debería funcionar en cualquier versión desde 2005 en adelante. Prueba en SQL-Fiddle-3 Esta solución asume, como las anteriores, que hay 4 niveles máximos en el árbol de jerarquía:Un cuarto enfoque, que calcula como un CTE intermedio, la tabla de cierre de la jerarquía. Prueba en SQL-Fiddle-4 . El beneficio es que para los cálculos de sumas, no hay restricción en el número de niveles.
fuente
Creo que esto debería hacerlo más rápido:
Agregué una columna en el CTE llamada
IdHierarchyMatch
que es la versión hacia adelanteIdHierarchy
para habilitar laTotalCustomerCount
subconsultaWHERE
cláusula de sea modificable.Al comparar los costos estimados de subárbol para los planes de ejecución, esta forma debería ser aproximadamente 5 veces más rápida.
fuente
ROW_NUMER() OVER (ORDER BY...)
cosa. Simplemente no pude obtener los números correctos. Es una pregunta realmente genial e interesante. Buen ejercicio cerebral!IdHierarchyMatch
campo, sin embargo, no puede agregar un índice agrupado en una vista vinculada al esquema que incluye un CTE. Me pregunto si esta limitación se resuelve en SQL Server 2014.Le di una oportunidad también. No es muy bonito, pero parece funcionar mejor.
fuente