Cập nhật dưới đây
Tôi có một bảng tài khoản với kiến trúc tài khoản gốc / acct điển hình để thể hiện một hệ thống phân cấp tài khoản (SQL Server 2012). Tôi đã tạo ra một XEM bằng cách sử dụng CTE để băm phân cấp, và trên toàn bộ nó hoạt động rất đẹp và như dự định. Tôi có thể truy vấn hệ thống phân cấp ở mọi cấp độ và xem các nhánh dễ dàng.
Có một trường logic nghiệp vụ cần được trả về như là một hàm của hệ thống phân cấp. Một trường trong mỗi bản ghi tài khoản mô tả quy mô của doanh nghiệp (chúng tôi sẽ gọi nó là CustomerCount). Logic tôi cần báo cáo về các nhu cầu để triển khai CustomerCount từ toàn bộ chi nhánh. Nói cách khác, được cung cấp một tài khoản, tôi cần tổng hợp các giá trị tùy chỉnh cho tài khoản đó cùng với mọi đứa trẻ ở mọi chi nhánh bên dưới tài khoản dọc theo phân cấp.
Tôi đã tính toán thành công trường bằng cách sử dụng trường phân cấp được xây dựng trong CTE, trông giống như acct4.acct3.acct2.acct1. Vấn đề tôi gặp phải chỉ đơn giản là làm cho nó chạy nhanh. Nếu không có trường được tính toán này, truy vấn sẽ chạy trong ~ 3 giây. Khi tôi thêm vào trường được tính toán, nó sẽ chuyển thành truy vấn 4 phút.
Đây là phiên bản tốt nhất mà tôi có thể đưa ra để trả về kết quả chính xác. Tôi đang tìm kiếm ý tưởng về cách tôi có thể cơ cấu lại điều này NHƯ THẾ NÀO mà không cần hy sinh lớn như vậy để thực hiện.
Tôi hiểu lý do cái này đi chậm (yêu cầu tính toán một vị ngữ trong mệnh đề where), nhưng tôi không thể nghĩ ra cách nào khác để cấu trúc nó và vẫn nhận được kết quả tương tự.
Đây là một số mã mẫu để xây dựng bảng và thực hiện CTE khá chính xác khi nó hoạt động trong môi trường của tôi.
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 CẬP NHẬT
Một số giải pháp được đề xuất khiến nước trái cây của tôi chảy, và tôi đã thử một cách tiếp cận mới, nhưng đưa ra một trở ngại mới / khác. Thành thật mà nói, tôi không biết liệu điều này có đảm bảo một bài riêng biệt hay không, nhưng nó liên quan đến giải pháp của vấn đề này.
Điều tôi quyết định là điều khiến cho việc tính tổng (customercount) trở nên khó khăn là việc xác định trẻ em trong bối cảnh của một hệ thống phân cấp bắt đầu từ đầu và xây dựng xuống. Vì vậy, tôi đã bắt đầu bằng cách tạo một hệ thống phân cấp xây dựng từ dưới lên, sử dụng gốc được xác định bởi "tài khoản không phải là tài khoản phụ" và thực hiện phép nối đệ quy ngược (root.parentacctid = recurse.acctid)
Bằng cách này, tôi chỉ có thể thêm số lượng khách hàng con cho phụ huynh khi đệ quy xảy ra. Vì cách tôi cần báo cáo và cấp độ, tôi đang thực hiện cte từ dưới lên này từ trên xuống, sau đó chỉ cần tham gia chúng qua id tài khoản. Cách tiếp cận này hóa ra nhanh hơn nhiều so với tùy chỉnh truy vấn bên ngoài ban đầu, nhưng tôi gặp phải một vài trở ngại.
Đầu tiên, tôi đã vô tình chiếm được số lượng khách hàng trùng lặp cho các tài khoản là cha mẹ của nhiều trẻ em. Tôi đã tăng gấp đôi hoặc gấp ba số lượng khách hàng cho một số acctid, theo số lượng trẻ em có. Giải pháp của tôi là tạo ra một cte khác, đếm số acct có bao nhiêu nút và chia acct.customercount trong quá trình đệ quy, vì vậy khi tôi thêm toàn bộ nhánh, acct sẽ không được tính hai lần.
Vì vậy, tại thời điểm này, kết quả của phiên bản mới này không chính xác, nhưng tôi biết tại sao. Cte dưới cùng đang tạo ra các bản sao. Khi đệ quy đi qua, nó sẽ tìm bất cứ thứ gì trong thư mục gốc (con cấp dưới) là con của một tài khoản trong bảng tài khoản. Trong lần đệ quy thứ ba, nó chọn các tài khoản giống như lần thứ hai và đặt lại chúng.
Ý tưởng về cách làm cte từ dưới lên, hoặc điều này có làm cho bất kỳ ý tưởng nào khác trôi chảy không?
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