Posting blog pertama di situs ini, pada Juli 2012, berbicara tentang pendekatan terbaik untuk menjalankan total. Sejak itu, saya telah beberapa kali ditanyai bagaimana saya akan menangani masalah jika total yang berjalan lebih kompleks – khususnya, jika saya perlu menghitung total yang berjalan untuk beberapa entitas – katakanlah, setiap pesanan pelanggan.
Contoh asli menggunakan kasus fiktif dari sebuah kota yang mengeluarkan tilang; total berjalan hanyalah menggabungkan dan menghitung jumlah tiket ngebut setiap hari (terlepas dari siapa tiket itu dikeluarkan atau untuk berapa harganya). Contoh yang lebih kompleks (tetapi praktis) mungkin menggabungkan nilai total tilang, yang dikelompokkan berdasarkan SIM, per hari. Mari kita bayangkan tabel berikut:
BUAT TABEL dbo.SpeedingTickets( IncidentID INT IDENTITY(1,1) PRIMARY KEY, LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL); BUAT INDEKS UNIK x PADA dbo.SpeedingTicket(LicenseNumber, IncidentDate) INCLUDE(TicketAmount);
Anda mungkin bertanya, DECIMAL(7,2)
, Betulkah? Seberapa cepat orang-orang ini pergi? Nah, di Kanada, misalnya, tidak terlalu sulit untuk mendapatkan denda $10.000 karena ngebut.
Sekarang, mari kita isi tabel dengan beberapa data sampel. Saya tidak akan membahas semua secara spesifik di sini, tetapi ini akan menghasilkan sekitar 6.000 baris yang mewakili banyak pengemudi dan beberapa jumlah tiket selama periode satu bulan:
;DENGAN Jumlah Tiket(ID,Nilai) AS ( -- 10 jumlah tiket arbitrer PILIH i,p DARI ( NILAI(1,32,75),(2,75), (3,109),(4,175),(5,295), (6,68,50), (7,125), (8,145), (9,199), (10,250) ) AS v(i,p)),LicenseNumbers(LicenseNumber,[newid]) AS ( -- 1000 nomor lisensi acak SELECT TOP ( 1000) 7000000 + nomor, n =NEWID() FROM [master].dbo.spt_values MANA nomor ANTARA 1 DAN 999999 ORDER OLEH n),JanuariTanggal([hari]) AS ( -- setiap hari di bulan Januari 2014 PILIH ATAS (31) DATEADD(DAY, number, '20140101') FROM [master].dbo.spt_values WHERE [type] =N'P' ORDER BY number),Tickets(LicenseNumber,[day],s) AS( -- match *some* lisensi ke hari mereka mendapat tiket SELECT DISTINCT l.LicenseNumber, d.[day], s =RTRIM(l.LicenseNumber) FROM LicenseNumber AS l CROSS JOIN JanuariTanggal AS d WHERE CHECKSUM(NEWID()) % 100 =l.LicenseNumber % 100 DAN (RTRIM(l.LicenseNumber) LIKE '%' + KANAN(CONVERT(CHAR(8), d.[day], 112),1) + '%') ATAU (RTRIM(l.LicenseNumber+1) LIKE ' %' + KANAN( CONVERT(CHAR(8), d.[day], 112),1) + '%'))INSERT dbo.SpeedingTickets(LicenseNumber,IncidentDate,TicketAmount)PILIH t.LicenseNumber, t.[hari], ta.Nilai DARI Tiket AS t INNER JOIN Jumlah Tiket AS ta ON ta.ID =CONVERT(INT,RIGHT(t.s,1))-CONVERT(INT,LEFT(RIGHT(t.s,2),1)) ORDER BY t.[hari], t .LicenseNumber;
Ini mungkin tampak sedikit terlalu rumit, tetapi salah satu tantangan terbesar yang sering saya hadapi ketika menulis posting blog ini adalah membangun sejumlah data "acak"/arbitrer yang realistis. Jika Anda memiliki metode yang lebih baik untuk populasi data arbitrer, tentu saja, jangan gunakan gumaman saya sebagai contoh – mereka adalah periferal dari pos ini.
Pendekatan
Ada berbagai cara untuk memecahkan masalah ini di T-SQL. Berikut adalah tujuh pendekatan, bersama dengan rencana terkait. Saya telah mengabaikan teknik seperti kursor (karena pasti akan lebih lambat) dan CTE rekursif berbasis tanggal (karena bergantung pada hari yang berdekatan).
Subkueri #1
SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =TicketAmount + COALESCE( ( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets AS WHERE s.LicenseNumber =o.LicenseNumber DAN s.IncidentDate <0)FROMDate .SpeedingTicket SEBAGAI ORDER BY LicenseNumber, IncidentDate;
Rencanakan subkueri #1
Subkueri #2
SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =( SELECT SUM(TicketAmount) FROM dbo.SpeedingTickets WHERE LicenseNumber =t.LicenseNumber AND IncidentDate <=t.IncidentDate )FROM dbo.SpeedingTickets AS LicenseDNtORDER;>
Rencanakan subkueri #2Gabung mandiri
PILIH t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FROM dbo.SpeedingTickets SEBAGAI t1INNER GABUNG dbo.SpeedingTickets SEBAGAI t2 PADA t1.LicenseNumberict1.LicenseNumberict =. t2.IncidentDateGROUP OLEH t1.LicenseNumber, t1.IncidentDate, t1.TicketAmountORDER OLEH t1.LicenseNumber, t1.IncidentDate;
Rencanakan untuk bergabung sendiriAplikasi luar
SELECT t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FROM dbo.SpeedingTickets SEBAGAI t1OUTER APPLY( SELECT TicketAmount FROM dbo.SpeedingTickets WHERE LicenseNumber =Number =Number =IncidentDate) SEBAGAI t2GROUP OLEH t1.LicenseNumber, t1.IncidentDate, t1.TicketAmountORDER OLEH t1.LicenseNumber, t1.IncidentDate;
Rencanakan untuk penerapan luarSUM OVER() menggunakan RANGE (khusus 2012+)
SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate RANGE UNBOUNDED SEBELUMNYA ) DARI dbo.SpeedingTicket ORDER BY LicenseNumber, IncidentDate;
Rencanakan SUM OVER() menggunakan RANGESUM OVER() menggunakan ROWS (khusus 2012+)
SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate BARIS UNBOUNDED SEBELUMNYA ) DARI dbo.SpeedingTicket ORDER BY LicenseNumber, IncidentDate;
Rencanakan SUM OVER() menggunakan ROWSIterasi berbasis set
Dengan penghargaan kepada Hugo Kornelis (@Hugo_Kornelis) untuk Bab #4 di SQL Server MVP Deep Dives Volume #1, pendekatan ini menggabungkan pendekatan berbasis kumpulan dan pendekatan kursor.
DECLARE @x TABLE( LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL, RunningTotal DECIMAL(7,2) NOT NULL, rn INT NOT NULL, PRIMARY KEY(LicenseNumber, IncidentDate) ); INSERT @x(LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn)SELECT LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() OVER (PARTISI BY LicenseNumber ORDER BY IncidentDate) DARI dbo.SpeedingTicket; MENYATAKAN @rn INT =1, @rc INT =1; SAAT @rc> 0MULAI SET @rn +=1; UPDATE [current] SET RunningTotal =[last].RunningTotal + [current].TicketAmount FROM @x AS [current] INNER JOIN @x AS [last] ON [current].LicenseNumber =[last].LicenseNumber DAN [terakhir]. rn =@rn - 1 WHERE [saat ini].rn =@rn; SET @rc =@@ROWCOUNT;END SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal FROM @x ORDER BY LicenseNumber, IncidentDate;Karena sifatnya, pendekatan ini menghasilkan banyak rencana identik dalam proses memperbarui variabel tabel, yang semuanya mirip dengan rencana self-join dan outer apply, tetapi dapat menggunakan seek:
Salah satu dari banyak rencana UPDATE yang dihasilkan melalui iterasi berbasis setSatu-satunya perbedaan antara setiap rencana di setiap iterasi adalah jumlah baris. Melalui setiap iterasi berturut-turut, jumlah baris yang terpengaruh harus tetap sama atau turun, karena jumlah baris yang terpengaruh pada setiap iterasi mewakili jumlah pengemudi dengan tiket pada jumlah hari itu (atau, lebih tepatnya, jumlah hari di "peringkat" itu).
Hasil Kinerja
Berikut adalah bagaimana pendekatan ditumpuk, seperti yang ditunjukkan oleh SQL Sentry Plan Explorer, dengan pengecualian pendekatan iterasi berbasis set yang, karena terdiri dari banyak pernyataan individu, tidak mewakili dengan baik jika dibandingkan dengan yang lain.
Rencanakan metrik runtime Explorer untuk enam dari tujuh pendekatanSelain meninjau paket dan membandingkan metrik waktu proses di Plan Explorer, saya juga mengukur waktu proses mentah di Management Studio. Berikut adalah hasil menjalankan setiap kueri 10 kali, dengan mengingat bahwa ini juga termasuk waktu render di SSMS:
Durasi runtime, dalam milidetik, untuk ketujuh pendekatan (10 iterasi )Jadi, jika Anda menggunakan SQL Server 2012 atau lebih baik, pendekatan terbaik tampaknya adalah
SUM OVER()
menggunakanROWS UNBOUNDED PRECEDING
. Jika Anda tidak menggunakan SQL Server 2012, pendekatan subquery kedua tampaknya optimal dalam hal runtime, meskipun jumlah pembacaan yang tinggi dibandingkan dengan, katakanlah,OUTER APPLY
pertanyaan. Dalam semua kasus, tentu saja, Anda harus menguji pendekatan ini, yang disesuaikan dengan skema Anda, terhadap sistem Anda sendiri. Data, indeks, dan faktor lain Anda dapat menghasilkan solusi berbeda yang paling optimal di lingkungan Anda.Kompleksitas Lainnya
Sekarang, indeks unik menandakan bahwa setiap kombinasi LicenseNumber + IncidentDate akan berisi total kumulatif tunggal, jika pengemudi tertentu mendapatkan beberapa tiket pada hari tertentu. Aturan bisnis ini membantu menyederhanakan logika kita sedikit, menghindari kebutuhan akan pemutus untuk menghasilkan total berjalan deterministik.
Jika Anda memiliki kasus di mana Anda mungkin memiliki beberapa baris untuk kombinasi LicenseNumber + IncidentDate yang diberikan, Anda dapat memutuskan ikatan menggunakan kolom lain yang membantu membuat kombinasi menjadi unik (jelas tabel sumber tidak lagi memiliki batasan unik pada dua kolom tersebut) . Perhatikan bahwa ini dimungkinkan bahkan dalam kasus di mana
DATE
kolom sebenarnyaDATETIME
– banyak orang berasumsi bahwa nilai tanggal/waktu itu unik, tetapi ini tentu saja tidak selalu dijamin, terlepas dari perinciannya.Dalam kasus saya, saya bisa menggunakan
IDENTITY
kolom,IncidentID
; inilah cara saya menyesuaikan setiap solusi (mengakui bahwa mungkin ada cara yang lebih baik; hanya membuang ide):/* --------- subquery #1 --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =TicketAmount + COALESCE( ( SELECT SUM(TicketAmount) FROM dbo. SpeedingTickets AS s WHERE s.LicenseNumber =o.LicenseNumber AND (s.IncidentDate=t2.IncidentDate -- menambahkan baris ini:DAN t1.IncidentID>=t2.IncidentIDGROUP BY .TicketAmountORDER OLEH t1.LicenseNumber, t1.IncidentDate; /* --------- penerapan luar --------- */ SELECT t1.LicenseNumber, t1.IncidentDate, t1.TicketAmount, RunningTotal =SUM(t2.TicketAmount)FROM dbo.SpeedingTicket AS t1OUTER APPLY( SELECT TicketAmount FROM dbo.SpeedingTickets WHERE LicenseNumber =t1.LicenseNumber AND IncidentDate <=t1.IncidentDate -- menambahkan baris ini:AND IncidentID <=t1.IncidentID) SEBAGAI t2GROUP OLEH t1.LicenseNumt1. OLEH t1.LicenseNumber, t1.IncidentDate; /* --------- SUM() LEBIH menggunakan RANGE --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate, IncidentID RANGE UNBOUNDED PRECEDING -- menambahkan kolom ini ^^^^^^^^^^^^ ) DARI dbo.SpeedingTicket ORDER BY LicenseNumber, IncidentDate; /* --------- SUM() LEBIH menggunakan ROWS --------- */ SELECT LicenseNumber, IncidentDate, TicketAmount, RunningTotal =SUM(TicketAmount) OVER ( PARTITION BY LicenseNumber ORDER BY IncidentDate, IncidentID ROWS UNBOUNDED PRECEDING -- menambahkan kolom ini ^^^^^^^^^^^^ ) FROM dbo.SpeedingTicket ORDER BY LicenseNumber, IncidentDate; /* --------- iterasi berbasis set --------- */ DECLARE @x TABLE( -- menambahkan kolom ini, dan menjadikannya PK:IncidentID INT PRIMARY KEY, LicenseNumber INT NOT NULL, IncidentDate DATE NOT NULL, TicketAmount DECIMAL(7,2) NOT NULL, RunningTotal DECIMAL(7,2) NOT NULL, rn INT NOT NULL); -- menambahkan kolom tambahan ke INSERT/SELECT:INSERT @x(IncidentID, LicenseNumber, IncidentDate, TicketAmount, RunningTotal, rn)SELECT IncidentID, LicenseNumber, IncidentDate, TicketAmount, TicketAmount, ROW_NUMBER() OVER (PARTITION BY LicenseNumber ORDER BY IncidentDate , IncidentID) -- dan menambahkan kolom tie-breaker ini ------------------------------^^^^^^^^ ^^^^ DARI dbo.SpeedingTicket; -- sisa solusi iterasi berbasis set tetap tidak berubah Komplikasi lain yang mungkin Anda temui adalah ketika Anda tidak mengejar seluruh tabel, melainkan subset (misalnya, dalam hal ini, minggu pertama Januari). Anda harus melakukan penyesuaian dengan menambahkan
WHERE
klausa, dan ingatlah predikat tersebut saat Anda juga memiliki subkueri yang berkorelasi.