Mettre à jour ci-dessous
J'ai un tableau de comptes avec une architecture de compte acct / parent typique pour représenter une hiérarchie de comptes (SQL Server 2012). J'ai créé une VUE en utilisant un CTE pour hacher la hiérarchie, et dans l'ensemble cela fonctionne à merveille et comme prévu. Je peux interroger la hiérarchie à n'importe quel niveau et voir facilement les branches.
Il existe un champ de logique métier qui doit être renvoyé en fonction de la hiérarchie. Un champ dans chaque enregistrement de compte décrit la taille de l'entreprise (nous l'appellerons CustomerCount). La logique dont j'ai besoin pour rendre compte doit remonter le compte client de toute la succursale. En d'autres termes, compte tenu d'un compte, j'ai besoin de résumer les valeurs de compte personnalisé pour ce compte avec chaque enfant dans chaque branche sous le compte le long de la hiérarchie.
J'ai réussi à calculer le champ à l'aide d'un champ de hiérarchie construit dans le CTE, qui ressemble à acct4.acct3.acct2.acct1. Le problème que je rencontre est simplement de le faire fonctionner rapidement. Sans ce seul champ calculé, la requête s'exécute en ~ 3 secondes. Lorsque j'ajoute dans le champ calculé, cela se transforme en une requête de 4 minutes.
Voici la meilleure version que j'ai pu trouver qui renvoie les bons résultats. Je cherche des idées sur la façon de restructurer cela COMME UNE VUE sans sacrifices énormes à la performance.
Je comprends la raison pour laquelle celui-ci va lentement (nécessite le calcul d'un prédicat dans la clause where), mais je ne peux pas penser à une autre façon de le structurer et d'obtenir toujours les mêmes résultats.
Voici un exemple de code pour construire une table et faire le CTE à peu près exactement comme cela fonctionne 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
20/11/2013 MISE À JOUR
Certaines des solutions suggérées ont fait couler mon jus et j'ai essayé une nouvelle approche qui se rapproche, mais introduit un obstacle nouveau / différent. Honnêtement, je ne sais pas si cela justifie 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 (compte personnalisé) difficile, c'est l'identification des enfants dans le contexte d'une hiérarchie qui commence en haut et se construit progressivement. J'ai donc commencé par créer une hiérarchie qui se construit de bas en haut, en utilisant la racine définie par "les comptes qui ne sont parents d'aucun autre compte" et en faisant la jointure récursive à l'envers (root.parentacctid = recurse.acctid)
De cette façon, je pourrais simplement ajouter le nombre de clients enfants au parent lorsque la récursivité se produit. En raison de la façon dont j'ai besoin de rapports et de niveaux, je fais ce cte de bas en haut en plus de haut en bas, puis je les rejoins simplement via l'ID de compte. Cette approche s'avère beaucoup plus rapide que le compte personnalisé de la requête externe d'origine, mais j'ai rencontré quelques obstacles.
Tout d'abord, je capturais par inadvertance le nombre de clients en double pour les comptes parents de plusieurs enfants. J'ai compté deux ou trois fois le nombre de clients pour certains acctid, par le nombre d'enfants. Ma solution était de créer un autre cte qui compte le nombre de nœuds d'un acct et de diviser le acct.customercount pendant la récursivité, donc quand j'additionne la branche entière, l'acct n'est pas compté deux fois.
Donc, à 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é réussit, elle recherche tout élément de la racine (enfants de niveau inférieur) qui est enfant d'un compte dans la table des comptes. Lors de la troisième récursivité, il récupère les mêmes comptes que dans le second et les réinsère.
Des idées sur la façon de faire un cte de bas en haut, ou cela fait-il couler d'autres idées?
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
la source
Réponses:
Edit: c'est la deuxième tentative
Sur la base de la réponse de @Max Vernon, voici un moyen de contourner l'utilisation de CTE dans une sous-requête en ligne, ce qui revient à rejoindre le CTE et je suppose que c'est la raison de la faible efficacité. Il utilise des fonctions analytiques disponibles uniquement dans la version 2012 de SQL-Server. Testé à SQL-Fiddle
Cette partie peut être sautée de la lecture, c'est un copier-coller de la réponse de Max:
Ici, nous ordonnons les lignes du CTE en utilisant le
IdHierarchyMatch
et nous calculons les numéros de ligne et un total cumulé (de la ligne suivante jusqu'à la fin).Ensuite, nous avons un CTE intermédiaire supplémentaire où nous utilisons les totaux et les numéros de ligne précédents - essentiellement pour trouver où les points finaux pour les branches de la structure arborescente:
et enfin nous construisons la dernière partie:
Et une simplification, en utilisant le même
cte1
que le code ci-dessus. Testez à SQL-Fiddle-2 . Veuillez noter que les deux solutions fonctionnent en supposant que vous avez un maximum de quatre niveaux dans votre arborescence:Une troisième approche, avec un seul CTE, pour la partie récursive et ensuite uniquement des fonctions d'agrégation de fenêtres (
SUM() OVER (...)
), donc cela devrait fonctionner dans n'importe quelle version à partir de 2005. Test à SQL-Fiddle-3 Cette solution suppose, comme les précédentes, qu'il existe 4 niveaux maximum dans l'arborescence:Une 4ème approche, qui calcule comme CTE intermédiaire, la table de clôture de la hiérarchie. Testez à SQL-Fiddle-4 . L'avantage est que pour le calcul des sommes, il n'y a pas de restriction sur le nombre de niveaux.
la source
Je pense que cela devrait accélérer les choses:
J'ai ajouté une colonne dans le CTE nommé
IdHierarchyMatch
qui est la version directe deIdHierarchy
pour permettre à la clause deTotalCustomerCount
sous - requêteWHERE
d'être sargable.En comparant les coûts estimatifs des sous-arbres pour les plans d'exécution, cette méthode devrait être environ 5 fois plus rapide.
la source
ROW_NUMER() OVER (ORDER BY...)
ou quelque chose. Je ne pouvais tout simplement pas en tirer les bons chiffres. C'est une question vraiment géniale et intéressante. Bon exercice cérébral!IdHierarchyMatch
terrain, mais vous ne pouvez pas ajouter un index cluster sur une vue liée au schéma qui inclut un CTE. Je me demande si cette limitation est résolue dans SQL Server 2014.Je lui ai aussi donné un coup de feu. Ce n'est pas très joli, mais il semble mieux fonctionner.
la source