mise à jour ci-dessous
J'ai une table de comptes avec une architecture typique du compte ACCT/parent pour représenter une hiérarchie de comptes (SQL Server 2012). J'ai créé une vue à l'aide d'un CTE pour sortir de la hiérarchie, et dans l'ensemble, cela fonctionne magnifiquement et comme prévu. Je peux interroger la hiérarchie à n'importe quel niveau et voir facilement les branches.
Il existe un champ logique commercial qui doit être retourné en fonction de la hiérarchie. Un champ dans chaque enregistrement de compte décrit la taille de l'entreprise (nous l'appellerons sur CustomerCount). La logique que je dois faire rapport sur les besoins pour traverser le centre-ville de toute la branche. En d'autres termes, compte tenu d'un compte, je dois résumer les valeurs CustomerCount pour ce compte ainsi que chaque enfant de chaque branche sous le compte le long de la hiérarchie.
J'ai calculé avec succès le champ à l'aide d'un champ de hiérarchie construit dans la CTE, qui ressemble à Acct4.ACCT3.Acct2.acct1. Le problème que je rencontre est simplement de le faire courir vite. Sans celui-ci calculé, la requête fonctionne dans environ 3 secondes. Lorsque j'ajoute dans le champ calculé, il se transforme en une requête de 4 minutes.
Voici la meilleure version que j'ai pu proposer cela renvoie les résultats corrects. Je cherche des idées sur la façon dont je puisse la restructurer comme une vue sans que des sacrifices aussi importants à la performance.
Je comprends la raison pour laquelle celui-ci va lentement (nécessite de calculer un prédicat dans la clause où), mais je ne peux pas penser à une autre façon de le structurer et de trouver les mêmes résultats.
Voici un exemple de code pour construire une table et faire le CTE à peu près exactement comme ça marche dans mon environnement.
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
Certaines des solutions suggérées ont eu mon jus qui coule, et j'ai essayé une nouvelle approche qui s'approche, mais introduit un obstacle nouveau/différent. Honnêtement, je ne sais pas si cela garantit un poste séparé ou non, mais c'est lié à la solution de ce problème.
Ce que j'ai décidé, c'est que ce qui rendait la somme (Customercount) difficile est l'identification des enfants dans le contexte d'une hiérarchie qui commence au sommet et se couche. J'ai donc commencé par créer une hiérarchie qui s'appuie à partir de bas en haut, à l'aide de la racine définie par "Comptes qui ne sont pas parent à un autre compte" et faisant la joindre récursive à l'envers (root.parentacCTD = recueil.acCTID)
De cette façon, je pouvais simplement ajouter le nombre de clients enfants au parent car la récursivité se produit. En raison de la manière dont j'ai besoin de rapports et de niveaux, je fais que ce type de bas du haut en haut en plus du haut en bas, puis je viens de les rejoindre via un identifiant de compte. Cette approche se révèle être beaucoup plus rapide que le centre-poste d'origine externe de la requête extérieure, mais j'ai rencontré quelques obstacles.
Premièrement, j'avais capturé par inadvertance le nombre de clients duplicatifs pour des comptes qui sont parents à plusieurs enfants. J'étais double ou triple comptant le nombre de clients pour certains Acctifs, par le nombre d'enfants qu'il y en avait. Ma solution consistait à créer encore un autre CTE qui compte le nombre de nœuds d'un ACCT et divisez l'ACCT.CUSTOMERCOUNT au cours de la récursivité, donc lorsque j'additionne la branche entière, l'ACCT n'est pas à double compter.
Ainsi, à ce stade, les résultats de cette nouvelle version ne sont pas corrects, mais je sais pourquoi. Le CTE BottomUp crée des doublons. Lorsque la récursivité passe, il cherche tout ce qui est dans la racine (enfants de niveau inférieur) qui est enfant à un compte dans la table du compte. Lors de la troisième récursion, il récupère les mêmes comptes que cela a fait dans la seconde et la met à nouveau.
Des idées sur la façon de faire une CTE de bas en haut ou -nent-cela d'autres idées qui circulent?
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
EDIT: Ceci est une deuxième tentative
Basé sur la réponse de @max Vernon, voici un moyen de contourner l'utilisation de CTE à l'intérieur d'une sous-requête en ligne, qui est comme auto-rejoindre le CTE et que je présume la raison de la mauvaise efficacité. Il utilise des fonctions analytiques disponibles uniquement dans la version 2012 de SQL-Server. Testé à SQL-FIDDLE
Cette partie peut être ignorée de la lecture, c'est une pâte de copie de la réponse de Max:
;With AccountHierarchy AS
(
SELECT
Root.AcctId AccountId
, Root.Name AccountName
, Root.ParentId ParentId
, 1 HierarchyLevel
, cast(Root.Acctid as varchar(4000)) IdHierarchyMatch
, cast(Root.Acctid as varchar(4000)) IdHierarchy
, cast(replace(Root.Name,'.','') as varchar(4000)) NameHierarchy
, cast(Root.Acctid as varchar(4000)) HierarchySort
, cast(Root.Name as varchar(4000)) HierarchyLabel ,
Root.CustomerCount CustomerCount
FROM
account Root
WHERE
Root.ParentID is null
UNION ALL
SELECT
Recurse.Acctid AccountId
, Recurse.Name AccountName
, Recurse.ParentId ParentId
, Root.HierarchyLevel + 1 HierarchyLevel
, CAST(CAST(Root.IdHierarchyMatch as varchar(40)) + '.'
+ cast(recurse.Acctid as varchar(40)) as varchar(4000)) IdHierarchyMatch
, cast(cast(recurse.Acctid as varchar(40)) + '.'
+ Root.IdHierarchy as varchar(4000)) IdHierarchy
, cast(replace(recurse.Name,'.','') + '.'
+ Root.NameHierarchy as varchar(4000)) NameHierarchy
, cast(Root.AccountName + '.'
+ Recurse.Name as varchar(4000)) HierarchySort
, cast(space(root.HierarchyLevel * 4)
+ Recurse.Name as varchar(4000)) HierarchyLabel
, Recurse.CustomerCount CustomerCount
FROM
account Recurse INNER JOIN
AccountHierarchy Root on Root.AccountId = Recurse.ParentId
)
Ici, nous commandons les rangées du CTE à l'aide de IdHierarchyMatch
et nous calculons les numéros de ligne et un total de fonctionnement (de la ligne suivante jusqu'à la fin.)
, cte1 AS
(
SELECT
h.AccountId
, h.AccountName
, h.ParentId
, h.HierarchyLevel
, h.IdHierarchy
, h.NameHierarchy
, h.HierarchyLabel
, parsename(h.IdHierarchy,1) Acct1Id
, parsename(h.NameHierarchy,1) Acct1Name
, parsename(h.IdHierarchy,2) Acct2Id
, parsename(h.NameHierarchy,2) Acct2Name
, parsename(h.IdHierarchy,3) Acct3Id
, parsename(h.NameHierarchy,3) Acct3Name
, parsename(h.IdHierarchy,4) Acct4Id
, parsename(h.NameHierarchy,4) Acct4Name
, h.CustomerCount
, h.HierarchySort
, h.IdHierarchyMatch
, Rn = ROW_NUMBER() OVER
(ORDER BY h.IdHierarchyMatch)
, RunningCustomerCount = COALESCE(
SUM(h.CustomerCount)
OVER
(ORDER BY h.IdHierarchyMatch
ROWS BETWEEN 1 FOLLOWING
AND UNBOUNDED FOLLOWING)
, 0)
FROM
AccountHierarchy AS h
)
Ensuite, nous avons un autre CTE intermédiaire où nous utilisons les totaux et les numéros de rangée précédents - essentiellement pour trouver l'endroit où les points d'extrémité des branches de la structure des arbres:
, cte2 AS
(
SELECT
cte1.*
, rn3 = LAST_VALUE(Rn) OVER
(PARTITION BY Acct1Id, Acct2Id, Acct3Id
ORDER BY Acct4Id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
, rn2 = LAST_VALUE(Rn) OVER
(PARTITION BY Acct1Id, Acct2Id
ORDER BY Acct3Id, Acct4Id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
, rn1 = LAST_VALUE(Rn) OVER
(PARTITION BY Acct1Id
ORDER BY Acct2Id, Acct3Id, Acct4Id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
, rcc3 = LAST_VALUE(RunningCustomerCount) OVER
(PARTITION BY Acct1Id, Acct2Id, Acct3Id
ORDER BY Acct4Id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
, rcc2 = LAST_VALUE(RunningCustomerCount) OVER
(PARTITION BY Acct1Id, Acct2Id
ORDER BY Acct3Id, Acct4Id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
, rcc1 = LAST_VALUE(RunningCustomerCount) OVER
(PARTITION BY Acct1Id
ORDER BY Acct2Id, Acct3Id, Acct4Id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
FROM
cte1
)
et enfin, nous construisons la dernière partie:
SELECT
hier.AccountId
, hier.AccountName
--- -- columns skipped
, hier.CustomerCount
, TotalCustomerCount = hier.CustomerCount
+ hier.RunningCustomerCount
- ca.LastRunningCustomerCount
, hier.HierarchySort
, hier.IdHierarchyMatch
FROM
cte2 hier
OUTER APPLY
( SELECT LastRunningCustomerCount, Rn
FROM
( SELECT LastRunningCustomerCount
= RunningCustomerCount, Rn
FROM (SELECT NULL a) x WHERE 4 <= HierarchyLevel
UNION ALL
SELECT rcc3, Rn3
FROM (SELECT NULL a) x WHERE 3 <= HierarchyLevel
UNION ALL
SELECT rcc2, Rn2
FROM (SELECT NULL a) x WHERE 2 <= HierarchyLevel
UNION ALL
SELECT rcc1, Rn1
FROM (SELECT NULL a) x WHERE 1 <= HierarchyLevel
) x
ORDER BY Rn
OFFSET 0 ROWS
FETCH NEXT 1 ROWS ONLY
) ca
ORDER BY
hier.HierarchySort ;
Et une simplification, en utilisant le même cte1
Comme code ci-dessus. Testez sur SQL-FIDDLE-2. Veuillez noter que les deux solutions fonctionnent sous l'hypothèse que vous avez un maximum de quatre niveaux dans votre arbre:
SELECT
hier.AccountId
--- -- skipping rows
, hier.CustomerCount
, TotalCustomerCount = CustomerCount
+ RunningCustomerCount
- CASE HierarchyLevel
WHEN 4 THEN RunningCustomerCount
WHEN 3 THEN LAST_VALUE(RunningCustomerCount) OVER
(PARTITION BY Acct1Id, Acct2Id, Acct3Id
ORDER BY Acct4Id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
WHEN 2 THEN LAST_VALUE(RunningCustomerCount) OVER
(PARTITION BY Acct1Id, Acct2Id
ORDER BY Acct3Id, Acct4Id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
WHEN 1 THEN LAST_VALUE(RunningCustomerCount) OVER
(PARTITION BY Acct1Id
ORDER BY Acct2Id, Acct3Id, Acct4Id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
END
, hier.HierarchySort
, hier.IdHierarchyMatch
FROM cte1 AS hier
ORDER BY
hier.HierarchySort ;
Une troisième approche, avec un seul CTE, pour la partie récursive, puis uniquement les fonctions d'agrégat de fenêtres (SUM() OVER (...)
), il devrait donc fonctionner dans n'importe quelle version de 2005 à la hausse. Testez sur SQL-FIDDLE- Cette solution suppose, comme les précédentes, qu'il y a 4 niveaux maximum dans l'arborescence de la hiérarchie:
;WITH AccountHierarchy AS
(
SELECT
AccountId = Root.AcctId
, AccountName = Root.Name
, ParentId = Root.ParentId
, HierarchyLevel = 1
, HierarchySort = CAST(Root.Acctid AS VARCHAR(4000))
, HierarchyLabel = CAST(Root.Name AS VARCHAR(4000))
, Acct1Id = CAST(Root.Acctid AS VARCHAR(4000))
, Acct2Id = CAST(NULL AS VARCHAR(4000))
, Acct3Id = CAST(NULL AS VARCHAR(4000))
, Acct4Id = CAST(NULL AS VARCHAR(4000))
, Acct1Name = CAST(Root.Name AS VARCHAR(4000))
, Acct2Name = CAST(NULL AS VARCHAR(4000))
, Acct3Name = CAST(NULL AS VARCHAR(4000))
, Acct4Name = CAST(NULL AS VARCHAR(4000))
, CustomerCount = Root.CustomerCount
FROM
account AS Root
WHERE
Root.ParentID IS NULL
UNION ALL
SELECT
Recurse.Acctid
, Recurse.Name
, Recurse.ParentId
, Root.HierarchyLevel + 1
, CAST(Root.AccountName + '.'
+ Recurse.Name AS VARCHAR(4000))
, CAST(SPACE(Root.HierarchyLevel * 4)
+ Recurse.Name AS VARCHAR(4000))
, Root.Acct1Id
, CASE WHEN Root.HierarchyLevel = 1
THEN cast(Recurse.Acctid AS VARCHAR(4000))
ELSE Root.Acct2Id
END
, CASE WHEN Root.HierarchyLevel = 2
THEN CAST(Recurse.Acctid AS VARCHAR(4000))
ELSE Root.Acct3Id
END
, CASE WHEN Root.HierarchyLevel = 3
THEN CAST(Recurse.Acctid AS VARCHAR(4000))
ELSE Root.Acct4Id
END
, cast(Root.AccountName as varchar(4000))
, CASE WHEN Root.HierarchyLevel = 1
THEN CAST(Recurse.Name AS VARCHAR(4000))
ELSE Root.Acct2Name
END
, CASE WHEN Root.HierarchyLevel = 2
THEN CAST(Recurse.Name AS VARCHAR(4000))
ELSE Root.Acct3Name
END
, CASE WHEN Root.HierarchyLevel = 3
THEN CAST(Recurse.Name AS VARCHAR(4000))
ELSE Root.Acct4Name
END
, Recurse.CustomerCount
FROM
account AS Recurse INNER JOIN
AccountHierarchy AS Root ON Root.AccountId = Recurse.ParentId
)
SELECT
h.AccountId
, h.AccountName
, h.ParentId
, h.HierarchyLevel
, IdHierarchy =
CAST(COALESCE(h.Acct4Id+'.','')
+ COALESCE(h.Acct3Id+'.','')
+ COALESCE(h.Acct2Id+'.','')
+ h.Acct1Id AS VARCHAR(4000))
, NameHierarchy =
CAST(COALESCE(h.Acct4Name+'.','')
+ COALESCE(h.Acct3Name+'.','')
+ COALESCE(h.Acct2Name+'.','')
+ h.Acct1Name AS VARCHAR(4000))
, h.HierarchyLabel
, h.Acct1Id
, h.Acct1Name
, h.Acct2Id
, h.Acct2Name
, h.Acct3Id
, h.Acct3Name
, h.Acct4Id
, h.Acct4Name
, h.CustomerCount
, TotalCustomerCount =
CASE h.HierarchyLevel
WHEN 4 THEN h.CustomerCount
WHEN 3 THEN SUM(h.CustomerCount) OVER
(PARTITION BY h.Acct1Id, h.Acct2Id, h.Acct3Id)
WHEN 2 THEN SUM(h.CustomerCount) OVER
(PARTITION BY Acct1Id, h.Acct2Id)
WHEN 1 THEN SUM(h.CustomerCount) OVER
(PARTITION BY h.Acct1Id)
END
, h.HierarchySort
, IdHierarchyMatch =
CAST(h.Acct1Id
+ COALESCE('.'+h.Acct2Id,'')
+ COALESCE('.'+h.Acct3Id,'')
+ COALESCE('.'+h.Acct4Id,'') AS VARCHAR(4000))
FROM
AccountHierarchy AS h
ORDER BY
h.HierarchySort ;
Une quatrième approche, qui calcule comme une CTE intermédiaire, la table de fermeture de la hiérarchie. Testez à SQL-FIDDLE-4. La prestation est que pour les calculs des sommes, il n'y a pas de résilication sur le nombre de niveaux.
;WITH AccountHierarchy AS
(
-- skipping several line, identical to the 3rd approach above
)
, ClosureTable AS
(
SELECT
AccountId = Root.AcctId
, AncestorId = Root.AcctId
, CustomerCount = Root.CustomerCount
FROM
account AS Root
UNION ALL
SELECT
Recurse.Acctid
, Root.AncestorId
, Recurse.CustomerCount
FROM
account AS Recurse INNER JOIN
ClosureTable AS Root ON Root.AccountId = Recurse.ParentId
)
, ClosureGroup AS
(
SELECT
AccountId = AncestorId
, TotalCustomerCount = SUM(CustomerCount)
FROM
ClosureTable AS a
GROUP BY
AncestorId
)
SELECT
h.AccountId
, h.AccountName
, h.ParentId
, h.HierarchyLevel
, h.HierarchyLabel
, h.CustomerCount
, cg.TotalCustomerCount
, h.HierarchySort
FROM
AccountHierarchy AS h
JOIN
ClosureGroup AS cg
ON cg.AccountId = h.AccountId
ORDER BY
h.HierarchySort ;
Je crois que cela devrait le rendre plus rapide:
;With AccountHierarchy AS
(
SELECT
Root.AcctId AccountId
, Root.Name AccountName
, Root.ParentId ParentId
, 1 HierarchyLevel
, cast(Root.Acctid as varchar(4000)) IdHierarchyMatch
, cast(Root.Acctid as varchar(4000)) IdHierarchy
, cast(replace(Root.Name,'.','') as varchar(4000)) NameHierarchy
, cast(Root.Acctid as varchar(4000)) HierarchySort
, cast(Root.Name as varchar(4000)) HierarchyLabel ,
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
, CAST(CAST(Root.IdHierarchyMatch as varchar(40)) + '.'
+ cast(recurse.Acctid as varchar(40)) as varchar(4000)) IdHierarchyMatch
, cast(cast(recurse.Acctid as varchar(40)) + '.'
+ Root.IdHierarchy as varchar(4000)) IdHierarchy
, cast(replace(recurse.Name,'.','') + '.'
+ Root.NameHierarchy as varchar(4000)) NameHierarchy
, 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
, 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
, (
SELECT
sum(children.CustomerCount)
FROM
AccountHierarchy Children
WHERE
Children.IdHierarchyMatch LIKE hier.IdHierarchyMatch + '%'
) TotalCustomerCount
, HierarchySort
, IdHierarchyMatch
FROM
AccountHierarchy hier
ORDER BY
hier.HierarchySort
J'ai ajouté une colonne dans la CTE nommée IdHierarchyMatch
qui est la version avant de IdHierarchy
pour activer la clause TotalCustomerCount
WHERE
d'être sargable.
Comparaison des coûts de sous-arbitres estimés pour les plans d'exécution, de cette façon devrait être environ 5 fois plus rapide.
J'ai aussi donné un coup de feu. Ce n'est pas très joli, mais il semble mieux performer.
USE Tempdb
go
SET STATISTICS IO ON;
SET STATISTICS TIME OFF;
SET NOCOUNT ON;
--------
-- assuming the original table looks something like this
-- and you cannot control it's indexes
-- (only widened the data types a bit for the extra sample rows)
--------
CREATE TABLE dbo.Account
(
Acctid VARCHAR(10) NOT NULL ,
Name VARCHAR(100) NULL ,
ParentId VARCHAR(10) NULL ,
CustomerCount INT NULL
);
--------
-- inserting the same records as in your sample
--------
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;
--------
-- now lets up the ante a bit and add some extra rows with random parents
-- to these 7 items, it is hard to measure differences with so few rows
--------
DECLARE @numberOfRows INT = 25000
DECLARE @from INT = 1
DECLARE @to INT = 7
DECLARE @T1 TABLE ( n INT );
WITH cte ( n )
AS ( SELECT ROW_NUMBER() OVER ( ORDER BY CURRENT_TIMESTAMP )
FROM sys.messages
)
INSERT INTO @T1
SELECT n
FROM cte
WHERE n <= @numberOfRows;
INSERT INTO dbo.Account
( acctId ,
name ,
parentId ,
Customercount
)
SELECT CHAR(64 + RandomNumber) + CAST(n AS VARCHAR(10)) AS Id ,
CAST('item ' + CHAR(64 + RandomNumber) + CAST(n AS VARCHAR(10)) AS VARCHAR(100)) ,
CHAR(64 + RandomNumber) AS parentId ,
ABS(CHECKSUM(NEWID()) % 100) + 1 AS RandomCustCount
FROM ( SELECT n ,
ABS(CHECKSUM(NEWID()) % @to) + @from AS RandomNumber
FROM @T1
) A;
--------
-- Assuming you cannot control it's indexes, in my tests we're better off taking the IO hit of copying the data
-- to some structure that is better optimized for this query. Not quite what I initially expected, but we seem
-- to be better off that way.
--------
CREATE TABLE tempdb.dbo.T1
(
AccountId VARCHAR(10) NOT NULL
PRIMARY KEY NONCLUSTERED ,
AccountName VARCHAR(100) NOT NULL ,
ParentId VARCHAR(10) NULL ,
HierarchyLevel INT NULL ,
HPath VARCHAR(1000) NULL ,
IdHierarchy VARCHAR(1000) NULL ,
NameHierarchy VARCHAR(1000) NULL ,
HierarchyLabel VARCHAR(1000) NULL ,
HierarchySort VARCHAR(1000) NULL ,
CustomerCount INT NOT NULL
);
CREATE CLUSTERED INDEX IX_Q1
ON tempdb.dbo.T1 ([ParentId]);
-- for summing customer counts over parents
CREATE NONCLUSTERED INDEX IX_Q2
ON tempdb.dbo.T1 (HPath) INCLUDE(CustomerCount);
INSERT INTO tempdb.dbo.T1
( AccountId ,
AccountName ,
ParentId ,
HierarchyLevel ,
HPath ,
IdHierarchy ,
NameHierarchy ,
HierarchyLabel ,
HierarchySort ,
CustomerCount
)
SELECT Acctid AS AccountId ,
Name AS AccountName ,
ParentId AS ParentId ,
NULL AS HierarchyLevel ,
NULL AS HPath ,
NULL AS IdHierarchy ,
NULL AS NameHierarchy ,
NULL AS HierarchyLabel ,
NULL AS HierarchySort ,
CustomerCount AS CustomerCount
FROM tempdb.dbo.account;
--------
-- I cannot seem to force an efficient way to do the sum while selecting over the recursive cte,
-- so I took it aside. I am sure there is a more elegant way but I can't seem to make it happen.
-- At least it performs better this way. But it remains a very expensive query.
--------
;
WITH AccountHierarchy
AS ( SELECT Root.AccountId AS AcId ,
Root.ParentId ,
1 AS HLvl ,
CAST(Root.AccountId AS VARCHAR(1000)) AS [HPa] ,
CAST(Root.accountId AS VARCHAR(1000)) AS hid ,
CAST(REPLACE(Root.AccountName, '.', '') AS VARCHAR(1000)) AS hn ,
CAST(Root.accountid AS VARCHAR(1000)) AS hs ,
CAST(Root.accountname AS VARCHAR(1000)) AS hl
FROM tempdb.dbo.T1 Root
WHERE Root.ParentID IS NULL
UNION ALL
SELECT Recurse.AccountId AS acid ,
Recurse.ParentId ParentId ,
Root.Hlvl + 1 AS hlvl ,
CAST(Root.HPa + '.' + Recurse.AccountId AS VARCHAR(1000)) AS hpa ,
CAST(recurse.AccountId + '.' + Root.hid AS VARCHAR(1000)) AS hid ,
CAST(REPLACE(recurse.AccountName, '.', '') + '.' + Root.hn AS VARCHAR(1000)) AS hn ,
CAST(Root.hs + '.' + Recurse.AccountName AS VARCHAR(1000)) AS hs ,
CAST(SPACE(root.hlvl * 4) + Recurse.AccountName AS VARCHAR(1000)) AS hl
FROM tempdb.dbo.T1 Recurse
INNER JOIN AccountHierarchy Root ON Root.AcId = Recurse.ParentId
)
UPDATE tempdb.dbo.T1
SET HierarchyLevel = HLvl ,
HPath = Hpa ,
IdHierarchy = hid ,
NameHierarchy = hn ,
HierarchyLabel = hl ,
HierarchySort = hs
FROM AccountHierarchy
WHERE AccountId = AcId;
SELECT --HPath ,
AccountId ,
AccountName ,
ParentId ,
HierarchyLevel ,
IdHierarchy ,
NameHierarchy ,
HierarchyLabel ,
PARSENAME(IdHierarchy, 1) Acct1Id ,
PARSENAME(NameHierarchy, 1) Acct1Name ,
PARSENAME(IdHierarchy, 2) Acct2Id ,
PARSENAME(NameHierarchy, 2) Acct2Name ,
PARSENAME(IdHierarchy, 3) Acct3Id ,
PARSENAME(NameHierarchy, 3) Acct3Name ,
PARSENAME(IdHierarchy, 4) Acct4Id ,
PARSENAME(NameHierarchy, 4) Acct4Name ,
CustomerCount ,
Cnt.TotalCustomerCount
FROM tempdb.dbo.t1 Hier
CROSS APPLY ( SELECT SUM(CustomerCount) AS TotalCustomerCount
FROM tempdb.dbo.t1
WHERE HPath LIKE hier.HPath + '%'
) Cnt
ORDER BY HierarchySort;
DROP TABLE tempdb.dbo.t1;
DROP TABLE tempdb.dbo.Account;