아래 업데이트
계정 계층 구조를 나타내는 일반적인 계정 / 부모 계정 아키텍처를 가진 계정 테이블이 있습니다 (SQL Server 2012). 계층 구조를 해시하기 위해 CTE를 사용하여 VIEW를 만들었으며 전체적으로 아름답고 의도 한대로 작동합니다. 모든 수준에서 계층 구조를 쿼리하고 분기를 쉽게 볼 수 있습니다.
계층의 함수로 리턴해야하는 하나의 비즈니스 로직 필드가 있습니다. 각 계정 레코드의 필드는 비즈니스 규모를 설명합니다 (고객 카운트라고 함). 내가보고 해야하는 논리는 전체 지점에서 CustomerCount를 롤업해야합니다. 즉, 계정이 주어지면 해당 계정의 고객 수 값을 계층 구조를 따라 계정 아래의 모든 지점에있는 모든 자식과 합산해야합니다.
accte4.acct3.acct2.acct1과 같은 CTE 내에 빌드 된 계층 구조 필드를 사용하여 필드를 성공적으로 계산했습니다. 내가 겪고있는 문제는 단순히 빨리 실행시키는 것입니다. 이 하나의 계산 된 필드가 없으면 쿼리가 ~ 3 초 안에 실행됩니다. 계산 된 필드를 추가하면 4 분 쿼리로 바뀝니다.
다음은 올바른 결과를 반환하는 최고의 버전입니다. 성능을 크게 희생시키지 않으면 서이 AS A VIEW를 재구성하는 방법에 대한 아이디어를 찾고 있습니다.
나는 이것이 느린 이유를 이해하지만 (where 절에서 술어를 계산해야 함), 그것을 구조화하고 여전히 동일한 결과를 얻는 다른 방법을 생각할 수 없습니다.
다음은 내 환경에서 작동하는 것처럼 테이블을 작성하고 CTE를 수행하는 샘플 코드입니다.
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
2013 년 11 월 20 일 업데이트
제안 된 솔루션 중 일부는 주스가 흐르고 근접한 새로운 접근법을 시도했지만 새로운 / 다른 장애물을 소개합니다. 솔직히 이것이 별도의 게시물이 필요한지 여부는 알 수 없지만이 문제의 해결과 관련이 있습니다.
내가 결정한 것은 합계 (고객 수)를 어렵게 만드는 것은 최상위에서 시작하여 쌓이는 계층 구조의 맥락에서 어린이를 식별하는 것입니다. 그래서 "다른 계정의 부모가 아닌 계정"에 의해 정의 된 루트를 사용하고 재귀 조인을 거꾸로하는 루트 (root.parentacctid = recurse.acctid)를 사용하여 아래에서 위로 계층 구조를 만드는 것으로 시작했습니다.
이렇게하면 재귀가 발생할 때 자식 고객 수를 부모에게 추가 할 수 있습니다. 보고 및 수준이 필요한 방법 때문에 하향식 외에도이 상향식 cte를 수행 한 다음 계정 ID를 통해 연결합니다. 이 방법은 원래 외부 쿼리 고객 수보다 훨씬 빠르지 만 몇 가지 장애물이 있습니다.
첫째, 여러 자녀의 부모 계정에 대한 중복 고객 수를 실수로 캡처하고있었습니다. 나는 아이들의 수에 따라 일부 acctid의 고객 수를 두 배 또는 세 배로 세었다. 내 솔루션은 acct의 노드 수를 계산하는 또 다른 cte를 만들고 재귀 중에 acct.customercount를 나누는 것이므로 전체 분기를 합산하면 acct가 두 번 계산되지 않습니다.
따라서이 시점 에서이 새로운 버전의 결과는 정확하지 않지만 그 이유를 알고 있습니다. 상향식 cte가 중복을 생성 중입니다. 재귀가 통과하면 루트 인 하위 (하위 레벨 하위)에서 계정 테이블의 계정에 해당하는 항목을 찾습니다. 세 번째 재귀에서는 두 번째 재귀에서와 동일한 계정을 가져 와서 다시 넣습니다.
상향식 cte를 수행하는 방법에 대한 아이디어 또는 다른 아이디어가 나오나요?
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