Perbarui di bawah ini
Saya memiliki daftar akun dengan arsitektur akun khusus akun induk / orang tua untuk mewakili hierarki akun (SQL Server 2012). Saya membuat VIEW menggunakan CTE untuk memilah-milah hierarki, dan secara keseluruhan itu bekerja dengan indah, dan sebagaimana dimaksud. Saya dapat meminta hierarki di tingkat mana pun, dan melihat cabang dengan mudah.
Ada satu bidang logika bisnis yang perlu dikembalikan sebagai fungsi hierarki. Bidang di setiap catatan akun menjelaskan ukuran bisnis (kami akan menyebutnya CustomerCount). Logika yang saya perlu laporkan harus menggulung CustomerCount dari seluruh cabang. Dengan kata lain, diberikan akun, saya perlu meringkas nilai-nilai customercount untuk akun itu bersama dengan setiap anak di setiap cabang di bawah akun di sepanjang hierarki.
Saya berhasil menghitung bidang menggunakan bidang hierarki yang dibangun dalam CTE, yang terlihat seperti acct4.acct3.acct2.acct1. Masalah yang saya hadapi adalah membuatnya berjalan cepat. Tanpa bidang terhitung ini, kueri berjalan dalam ~ 3 detik. Ketika saya menambahkan di bidang terhitung, itu berubah menjadi permintaan 4 menit.
Ini adalah versi terbaik yang bisa saya dapatkan dengan mengembalikan hasil yang benar. Saya mencari ide tentang bagaimana saya bisa merestrukturisasi ini SEBAGAI PANDANGAN tanpa pengorbanan besar untuk kinerja.
Saya mengerti alasan mengapa ini berjalan lambat (harus menghitung predikat dalam klausa where), tetapi saya tidak bisa memikirkan cara lain untuk menyusunnya dan masih mendapatkan hasil yang sama.
Berikut ini beberapa contoh kode untuk membuat tabel dan melakukan CTE persis seperti yang berfungsi di lingkungan saya.
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
11/20/2013 UPDATE
Beberapa solusi yang disarankan membuat jus saya mengalir, dan saya mencoba pendekatan baru yang mendekati, tetapi memperkenalkan hambatan baru / berbeda. Sejujurnya, saya tidak tahu apakah ini membutuhkan pos terpisah atau tidak, tetapi ini terkait dengan solusi masalah ini.
Apa yang saya putuskan adalah bahwa yang membuat jumlah (customercount) menjadi sulit adalah identifikasi anak-anak dalam konteks hierarki yang dimulai dari atas dan meningkat. Jadi saya mulai dengan membuat hierarki yang dibangun dari bawah ke atas, menggunakan root yang didefinisikan oleh "akun yang bukan induk dari akun lain" dan melakukan rekursif bergabung mundur (root.parentacctid = recurse.acctid)
Dengan cara ini saya bisa menambahkan jumlah pelanggan anak ke orang tua saat rekursi terjadi. Karena saya perlu melaporkan, dan level, saya melakukan ini dari bawah ke atas selain dari atas ke bawah, kemudian bergabung dengan mereka melalui id akun. Pendekatan ini ternyata jauh lebih cepat daripada customercount kueri luar yang asli, tetapi saya mengalami beberapa kendala.
Pertama, saya secara tidak sengaja menangkap jumlah pelanggan duplikat untuk akun yang merupakan induk dari banyak anak. Saya menghitung pelanggan dua atau tiga kali lipat untuk beberapa acctid, dengan jumlah anak di sana. Solusi saya adalah membuat lagi cte yang menghitung berapa banyak node yang dimiliki acct, dan membagi acct.customercount selama rekursi, jadi ketika saya menjumlahkan seluruh cabang acct tidak dihitung ganda.
Jadi pada titik ini, hasil versi baru ini tidak benar, tetapi saya tahu mengapa. Bottomup cte membuat duplikat. Ketika rekursi berlalu, ia mencari apa saja di root (anak-anak tingkat bawah) yang merupakan anak dari sebuah akun di tabel akun. Pada rekursi ketiga, ia mengambil akun yang sama dengan yang kedua dan menempatkannya kembali.
Gagasan tentang cara melakukan bottom up cte, atau apakah ini membuat ide lain mengalir?
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
sumber
Jawaban:
Sunting: ini adalah upaya kedua
Berdasarkan jawaban @Max Vernon, berikut adalah cara untuk mem-bypass penggunaan CTE di dalam subquery inline, yang seperti menggabungkan diri sendiri dengan CTE dan saya kira adalah alasan untuk efisiensi yang buruk. Ini menggunakan fungsi analitik yang hanya tersedia di SQL-Server versi 2012. Diuji di SQL-Fiddle
Bagian ini dapat dilewati dari membaca, ini adalah copy-paste dari jawaban Max:
Di sini kami memesan baris CTE menggunakan
IdHierarchyMatch
dan kami menghitung nomor baris dan total berjalan (dari baris berikutnya hingga akhir.)Kemudian kita memiliki satu lagi CTE perantara di mana kita menggunakan total berjalan sebelumnya dan nomor baris - pada dasarnya untuk menemukan di mana titik akhir untuk cabang-cabang struktur pohon:
dan akhirnya kami membangun bagian terakhir:
Dan penyederhanaan, menggunakan kode yang sama
cte1
seperti di atas. Tes di SQL-Fiddle-2 . Harap perhatikan bahwa kedua solusi bekerja dengan asumsi bahwa Anda memiliki maksimum empat level di pohon Anda:Pendekatan ketiga, dengan hanya satu CTE, untuk bagian rekursif dan kemudian hanya fungsi agregat jendela (
SUM() OVER (...)
), sehingga harus bekerja dalam versi apa pun mulai 2005 ke atas. Tes di SQL-Fiddle-3 Solusi ini mengasumsikan, seperti yang sebelumnya, bahwa ada 4 level maksimum dalam hierarki pohon:Pendekatan ke-4, yang dihitung sebagai CTE perantara, tabel penutupan hierarki. Tes di SQL-Fiddle-4 . Manfaatnya adalah bahwa untuk perhitungan jumlah, tidak ada penolakan pada jumlah level.
sumber
Saya percaya ini harus membuatnya lebih cepat:
Saya menambahkan kolom di CTE bernama
IdHierarchyMatch
yang merupakan versi majuIdHierarchy
untuk memungkinkan klausaTotalCustomerCount
subqueryWHERE
menjadi lebih besar.Membandingkan perkiraan biaya subtree untuk rencana eksekusi, cara ini seharusnya sekitar 5 kali lebih cepat.
sumber
ROW_NUMER() OVER (ORDER BY...)
atau sesuatu. Saya hanya tidak bisa mendapatkan angka yang benar dari itu. Itu pertanyaan yang sangat bagus dan menarik. Latihan otak yang bagus!IdHierarchyMatch
lapangan, namun Anda tidak bisa menambahkan indeks berkerumun pada tampilan skema-terikat yang mencakup CTE. Saya ingin tahu apakah batasan ini diselesaikan dalam SQL Server 2014.Saya mencobanya juga. Ini tidak terlalu cantik, tetapi tampaknya berkinerja lebih baik.
sumber