Zaktualizuj poniżej
Mam tabelę kont o typowej architekturze kont acct / parent do reprezentowania hierarchii kont (SQL Server 2012). Stworzyłem WIDOK za pomocą CTE, aby wyrównać hierarchię, i ogólnie działa pięknie i zgodnie z przeznaczeniem. Mogę sprawdzać hierarchię na dowolnym poziomie i łatwo przeglądać gałęzie.
Istnieje jedno pole logiki biznesowej, które należy zwrócić jako funkcję hierarchii. Pole w każdym rekordzie konta opisuje rozmiar firmy (nazwiemy to CustomerCount). Logika, którą muszę zgłosić, wymaga zsumowania CustomerCount z całego oddziału. Innymi słowy, biorąc pod uwagę konto, muszę sumować wartości liczby klientów dla tego konta wraz z każdym dzieckiem w każdym oddziale poniżej konta wzdłuż hierarchii.
Pomyślnie obliczyłem pole, używając pola hierarchii zbudowanego w CTE, które wygląda jak acct4.acct3.acct2.acct1. Problem, na który wpadam, polega po prostu na szybkim uruchomieniu. Bez tego jednego pola obliczeniowego zapytanie jest uruchamiane w ~ 3 sekund. Kiedy dodam w polu obliczeniowym, zamienia się w 4-minutowe zapytanie.
Oto najlepsza wersja, jaką udało mi się wymyślić, która zwraca poprawne wyniki. Szukam pomysłów na to, jak mogę zrestrukturyzować to JAK WIDOK bez tak dużych poświęceń dla wydajności.
Rozumiem powód, dla którego ten zwalnia (wymaga obliczenia predykatu w klauzuli where), ale nie mogę wymyślić innego sposobu, aby go ustrukturyzować i nadal uzyskiwać takie same wyniki.
Oto przykładowy kod do zbudowania tabeli i wykonania CTE w przybliżeniu dokładnie tak, jak działa w moim środowisku.
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
AKTUALIZACJA 11/20/2013
Niektóre z sugerowanych rozwiązań sprawiły, że moje soki płynęły, i próbowałem nowego podejścia, które jest bliskie, ale wprowadza nową / inną przeszkodę. Szczerze mówiąc, nie wiem, czy to gwarantuje oddzielny post, czy nie, ale ma to związek z rozwiązaniem tego problemu.
Uznałem, że to, co utrudnia sumę (liczbę klientów), to identyfikacja dzieci w kontekście hierarchii, która zaczyna się u góry i rozwija. Zacząłem więc od utworzenia hierarchii, która buduje się od podstaw, używając katalogu głównego zdefiniowanego przez „konta, które nie są rodzicami dla żadnego innego konta” i wykonując rekurencyjne połączenie wstecz (root.parentacctid = recurse.acctid)
W ten sposób mogłem po prostu dodać liczbę nadrzędnych klientów do elementu nadrzędnego, gdy nastąpi rekurencja. Z powodu tego, jak potrzebuję raportowania i poziomów, robię to od dołu do góry oprócz odgórnego, a następnie dołączam do nich za pomocą identyfikatora konta. Takie podejście okazuje się znacznie szybsze niż pierwotne zewnętrzne zapytanie klienta, ale napotkałem kilka przeszkód.
Po pierwsze, nieumyślnie rejestrowałem zduplikowaną liczbę klientów dla kont nadrzędnych dla wielu dzieci. Byłem podwójnym lub potrójnym liczeniem klientów dla niektórych acctid, według liczby dzieci. Moim rozwiązaniem było stworzenie kolejnego cte, który liczy, ile węzłów ma acct, i podzielenie acct.customercount podczas rekurencji, więc kiedy dodam całą gałąź, acct nie jest liczony podwójnie.
W tym momencie wyniki tej nowej wersji są nieprawidłowe, ale wiem dlaczego. Bottomup cte tworzy duplikaty. Kiedy rekurencja mija, szuka wszystkiego w katalogu głównym (dzieci podrzędne na poziomie niższym), które są potomkami konta w tabeli kont. Przy trzeciej rekurencji pobiera te same konta, które zrobił w drugiej i umieszcza je ponownie.
Pomysły na to, jak zrobić oddolne Cte, czy może napływają jakieś inne pomysły?
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
źródło
Odpowiedzi:
Edycja: to druga próba
Oparty na odpowiedzi @Max Vernon, tutaj jest sposób na ominięcie użycia CTE w wewnętrznym podzapytaniu, które jest jak samodzielne dołączenie do CTE i przypuszczam, że jest to przyczyną niskiej wydajności. Wykorzystuje funkcje analityczne dostępne tylko w wersji SQL-Server 2012. Testowane w SQL-Fiddle
Tę część można pominąć w czytaniu, jest to kopia-wklej z odpowiedzi Maxa:
Tutaj porządkujemy wiersze CTE za pomocą
IdHierarchyMatch
i obliczamy numery wierszy i sumę bieżącą (od następnego wiersza do końca).Następnie mamy jeszcze jeden pośredni CTE, w którym wykorzystujemy poprzednie sumy bieżące i numery wierszy - w zasadzie, aby znaleźć miejsce, w którym kończą się gałęzie struktury drzewa:
i na koniec budujemy ostatnią część:
I uproszczenie, przy użyciu tego samego
cte1
co powyższy kod. Test w SQL-Fiddle-2 . Pamiętaj, że oba rozwiązania działają przy założeniu, że masz maksymalnie cztery poziomy w swoim drzewie:Trzecie podejście, z tylko jednym CTE, dla części rekurencyjnej, a następnie tylko funkcji agregujących okna (
SUM() OVER (...)
), więc powinno działać w dowolnej wersji od 2005 roku w górę. Test w SQL-Fiddle-3 To rozwiązanie zakłada, podobnie jak poprzednie, że w drzewie hierarchii są maksymalnie 4 poziomy:Czwarte podejście, które oblicza jako pośrednie CTE, tabelę zamknięcia hierarchii. Test w SQL-Fiddle-4 . Korzyścią jest to, że w obliczeniach sum nie ma ograniczenia liczby poziomów.
źródło
Uważam, że powinno to przyspieszyć:
Dodałem kolumnę w nazwie CTE,
IdHierarchyMatch
która jest wersjąIdHierarchy
do przodu, aby umożliwić klauzulęTotalCustomerCount
podzapytaniaWHERE
.Porównując szacunkowe koszty poddrzewa dla planów wykonania, ten sposób powinien być około 5 razy szybszy.
źródło
ROW_NUMER() OVER (ORDER BY...)
czegoś. Po prostu nie mogłem wyciągnąć z tego odpowiednich liczb. To naprawdę świetne i interesujące pytanie. Dobre ćwiczenie mózgu!IdHierarchyMatch
pola, jednak nie można dodać indeksu klastrowego do widoku związanego ze schematem, który zawiera CTE. Zastanawiam się, czy to ograniczenie zostało rozwiązane w SQL Server 2014.Dałem też szansę. Nie jest bardzo ładny, ale wydaje się, że działa lepiej.
źródło