Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Pendekatan terbaik untuk total lari berkelompok

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 #2

    Gabung 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 sendiri

    Aplikasi 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 luar

    SUM 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 RANGE

    SUM 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 ROWS

    Iterasi 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 set

    Satu-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 pendekatan

Selain 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() menggunakan ROWS 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 sebenarnya DATETIME – 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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Mempertahankan MAX (atau MIN) berjalan yang dikelompokkan

  2. Cara Menemukan Nilai Minimum di Kolom

  3. Perbaikan Bug R2 2008 yang Merusak RCSI

  4. Menghubungkan ke Lotus Notes dari Java

  5. Cara Menggunakan Fungsi SQL SUM