Update unten
Ich habe eine Kontentabelle mit einer typischen Konto- / übergeordneten Kontenarchitektur, um eine Kontenhierarchie darzustellen (SQL Server 2012). Ich habe eine VIEW mit einem CTE erstellt, um die Hierarchie auszusortieren, und im Großen und Ganzen funktioniert es wunderbar und wie beabsichtigt. Ich kann die Hierarchie auf jeder Ebene abfragen und die Zweige leicht erkennen.
Es gibt ein Geschäftslogikfeld, das als Funktion der Hierarchie zurückgegeben werden muss. Ein Feld in jedem Kontodatensatz beschreibt die Größe des Unternehmens (wir nennen es CustomerCount). Die Logik, über die ich berichten muss, muss den CustomerCount aus der gesamten Filiale aufrollen. Mit anderen Worten, bei einem bestimmten Konto muss ich die Kundenzahlwerte für dieses Konto zusammen mit jedem untergeordneten Element in jedem Zweig unterhalb des Kontos entlang der Hierarchie zusammenfassen.
Ich habe das Feld erfolgreich mithilfe eines im CTE erstellten Hierarchiefelds berechnet, das wie acct4.acct3.acct2.acct1 aussieht. Das Problem, auf das ich stoße, ist einfach, es schnell laufen zu lassen. Ohne dieses eine berechnete Feld wird die Abfrage in ~ 3 Sekunden ausgeführt. Wenn ich das berechnete Feld hinzufüge, wird daraus eine 4-minütige Abfrage.
Hier ist die beste Version, die ich mir ausgedacht habe und die die richtigen Ergebnisse liefert. Ich bin auf der Suche nach Ideen, wie ich diese AS A VIEW ohne so große Leistungseinbußen restrukturieren kann.
Ich verstehe den Grund, warum dies langsam geht (erfordert das Berechnen eines Prädikats in der where-Klausel), aber ich kann mir keine andere Möglichkeit vorstellen, es zu strukturieren und trotzdem die gleichen Ergebnisse zu erzielen.
Hier ist ein Beispielcode zum Erstellen einer Tabelle und zum Ausführen des CTE so ziemlich genau wie in meiner Umgebung.
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 UPDATE
Bei einigen der vorgeschlagenen Lösungen flossen meine Säfte, und ich habe einen neuen Ansatz ausprobiert, der nahe kommt, aber ein neues / anderes Hindernis einführt. Ehrlich gesagt, ich weiß nicht, ob dies einen separaten Beitrag rechtfertigt oder nicht, aber es hängt mit der Lösung dieses Problems zusammen.
Ich entschied, dass die Identifizierung von Kindern im Kontext einer Hierarchie, die von oben nach unten beginnt, die Summe (Kundenzahl) erschwert. Also begann ich damit, eine Hierarchie zu erstellen, die von Grund auf aufgebaut ist, indem ich den Stamm verwendete, der durch "Konten, die keinem anderen Konto übergeordnet sind" definiert wurde, und den rekursiven Join rückwärts durchführte (root.parentacctid = recurse.acctid).
Auf diese Weise könnte ich dem Elternteil einfach die Anzahl der untergeordneten Kunden hinzufügen, während die Rekursion stattfindet. Aufgrund der Art und Weise, wie ich Berichte und Ebenen benötige, führe ich diese Bottom-Up-Cte zusätzlich zu der Top-Down-Cte durch und verbinde sie dann einfach über die Konto-ID. Dieser Ansatz erwies sich als viel schneller als die ursprüngliche Anzahl der externen Abfragen von Kunden, aber ich stieß auf einige Hindernisse.
Erstens habe ich versehentlich die doppelte Kundenzahl für Konten erfasst, die mehreren Kindern übergeordnet sind. Ich zählte die Anzahl der Kunden für einige Medikamente doppelt oder dreifach, gemessen an der Anzahl der Kinder, die es gab. Meine Lösung bestand darin, ein weiteres CTE zu erstellen, das zählt, wie viele Knoten ein Konto hat, und das acct.customercount während der Rekursion aufzuteilen. Wenn ich also den gesamten Zweig addiere, wird das Konto nicht doppelt gezählt.
Zum jetzigen Zeitpunkt sind die Ergebnisse dieser neuen Version nicht korrekt, aber ich weiß warum. Die Bottomup-CTE erstellt Duplikate. Wenn die Rekursion bestanden wird, sucht sie nach allen Elementen im Stammverzeichnis (untergeordnete Elemente der unteren Ebene), die einem Konto in der Kontentabelle untergeordnet sind. Bei der dritten Rekursion werden dieselben Konten wie bei der zweiten erneut abgerufen und eingefügt.
Ideen, wie man ein Bottom-up-CTE macht, oder fließen andere Ideen dahin?
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
quelle
Antworten:
Bearbeiten: Dies ist der zweite Versuch
Basierend auf der Antwort von @Max Vernon können Sie hier die Verwendung von CTE in einer Inline-Unterabfrage umgehen. Dies ähnelt einer Selbstverknüpfung mit dem CTE, und ich gehe davon aus, dass dies der Grund für die schlechte Effizienz ist. Es verwendet Analysefunktionen, die nur in der Version 2012 von SQL-Server verfügbar sind. Getestet bei SQL-Fiddle
Dieser Teil kann vom Lesen übersprungen werden, es ist ein Kopieren-Einfügen aus Max 'Antwort:
Hier bestellen wir die Zeilen des CTE mit
IdHierarchyMatch
und berechnen die Zeilennummern und eine laufende Summe (von der nächsten Zeile bis zum Ende).Dann haben wir noch einen Zwischen-CTE, in dem wir die vorherigen laufenden Summen und Zeilennummern verwenden - im Grunde genommen, um herauszufinden, wo die Endpunkte für die Zweige der Baumstruktur liegen:
Und zum Schluss bauen wir den letzten Teil:
Und eine Vereinfachung mit dem gleichen
cte1
Code wie oben. Test bei SQL-Fiddle-2 . Bitte beachten Sie, dass beide Lösungen unter der Annahme funktionieren, dass Sie maximal vier Ebenen in Ihrem Baum haben:Ein dritter Ansatz mit nur einem CTE für den rekursiven Teil und dann nur Fensteraggregatfunktionen (
SUM() OVER (...)
), sodass er in jeder Version ab 2005 funktionieren sollte. Test bei SQL-Fiddle-3 Bei dieser Lösung wird wie bei den vorherigen davon ausgegangen, dass der Hierarchiebaum maximal vier Ebenen enthält:Ein vierter Ansatz, der als Zwischen-CTE die Abschlusstabelle der Hierarchie berechnet. Test bei SQL-Fiddle-4 . Der Vorteil ist, dass es bei den Summenberechnungen keine Einschränkung der Anzahl der Ebenen gibt.
quelle
Ich glaube, das sollte es schneller machen:
Ich habe eine Spalte im CTE namens hinzugefügt,
IdHierarchyMatch
die die Vorwärtsversion von istIdHierarchy
, damit dieTotalCustomerCount
Unterabfrageklausel sargableWHERE
sein kann.Der Vergleich der geschätzten Teilbaumkosten für die Ausführungspläne sollte auf diese Weise ungefähr fünfmal schneller sein.
quelle
ROW_NUMER() OVER (ORDER BY...)
irgendetwas gelöst werden kann. Ich konnte einfach nicht die richtigen Zahlen herausholen. Das ist eine wirklich tolle und interessante Frage. Gute Gehirnübung!IdHierarchyMatch
Feld hinzuzufügen. Sie können jedoch keinen gruppierten Index für eine schemagebundene Ansicht hinzufügen, die einen CTE enthält. Ich frage mich, ob diese Einschränkung in SQL Server 2014 behoben ist.Ich habe es auch probiert. Es ist nicht sehr hübsch, aber es scheint eine bessere Leistung zu bringen.
quelle