Sumber yang bagus untuk menghitung total berjalan di SQL Server adalah dokumen ini
oleh Itzik Ben Gan yang dikirimkan ke Tim SQL Server sebagai bagian dari kampanyenya untuk memiliki OVER
klausa diperpanjang lebih jauh dari implementasi awal SQL Server 2005. Di dalamnya ia menunjukkan bagaimana begitu Anda masuk ke puluhan ribu baris, kursor keluar melakukan solusi berbasis set. SQL Server 2012 memang memperpanjang OVER
klausa membuat kueri semacam ini menjadi lebih mudah.
SELECT col1,
SUM(col1) OVER (ORDER BY ind ROWS UNBOUNDED PRECEDING)
FROM @tmp
Karena Anda menggunakan SQL Server 2005 namun ini tidak tersedia untuk Anda.
Adam Machanic tampilkan di sini bagaimana CLR dapat digunakan untuk meningkatkan kinerja kursor TSQL standar.
Untuk definisi tabel ini
CREATE TABLE RunningTotals
(
ind int identity(1,1) primary key,
col1 int
)
Saya membuat tabel dengan 2.000 dan 10.000 baris dalam database dengan ALLOW_SNAPSHOT_ISOLATION ON
dan satu dengan pengaturan ini (Alasan untuk ini adalah karena hasil awal saya berada di DB dengan pengaturan pada yang menyebabkan aspek membingungkan dari hasil).
Indeks berkerumun untuk semua tabel hanya memiliki 1 halaman root. Jumlah halaman daun untuk masing-masing halaman ditunjukkan di bawah ini.
+-------------------------------+-----------+------------+
| | 2,000 row | 10,000 row |
+-------------------------------+-----------+------------+
| ALLOW_SNAPSHOT_ISOLATION OFF | 5 | 22 |
| ALLOW_SNAPSHOT_ISOLATION ON | 8 | 39 |
+-------------------------------+-----------+------------+
Saya menguji kasus berikut (Tautan menunjukkan rencana eksekusi)
- Kiri Bergabung dan Grup Oleh
- Subkueri terkait Paket baris 2000 ,Paket 10.000 baris
- CTE dari jawaban Mikael (diperbarui)
- CTE di bawah
Alasan dimasukkannya opsi CTE tambahan adalah untuk memberikan solusi CTE yang akan tetap berfungsi jika ind
kolom tidak dijamin berurutan.
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
DECLARE @col1 int, @sumcol1 bigint;
WITH RecursiveCTE
AS (
SELECT TOP 1 ind, col1, CAST(col1 AS BIGINT) AS Total
FROM RunningTotals
ORDER BY ind
UNION ALL
SELECT R.ind, R.col1, R.Total
FROM (
SELECT T.*,
T.col1 + Total AS Total,
rn = ROW_NUMBER() OVER (ORDER BY T.ind)
FROM RunningTotals T
JOIN RecursiveCTE R
ON R.ind < T.ind
) R
WHERE R.rn = 1
)
SELECT @col1 =col1, @sumcol1=Total
FROM RecursiveCTE
OPTION (MAXRECURSION 0);
Semua kueri memiliki CAST(col1 AS BIGINT)
ditambahkan untuk menghindari kesalahan overflow saat runtime. Selain itu untuk semuanya, saya menetapkan hasil ke variabel seperti di atas untuk menghilangkan waktu yang dihabiskan untuk mengirim kembali hasil dari pertimbangan.
Hasil
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | | | Base Table | Work Table | Time |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| | Snapshot | Rows | Scan count | logical reads | Scan count | logical reads | cpu | elapsed |
| Group By | On | 2,000 | 2001 | 12709 | | | 1469 | 1250 |
| | On | 10,000 | 10001 | 216678 | | | 30906 | 30963 |
| | Off | 2,000 | 2001 | 9251 | | | 1140 | 1160 |
| | Off | 10,000 | 10001 | 130089 | | | 29906 | 28306 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| Sub Query | On | 2,000 | 2001 | 12709 | | | 844 | 823 |
| | On | 10,000 | 2 | 82 | 10000 | 165025 | 24672 | 24535 |
| | Off | 2,000 | 2001 | 9251 | | | 766 | 999 |
| | Off | 10,000 | 2 | 48 | 10000 | 165025 | 25188 | 23880 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE No Gaps | On | 2,000 | 0 | 4002 | 2 | 12001 | 78 | 101 |
| | On | 10,000 | 0 | 20002 | 2 | 60001 | 344 | 342 |
| | Off | 2,000 | 0 | 4002 | 2 | 12001 | 62 | 253 |
| | Off | 10,000 | 0 | 20002 | 2 | 60001 | 281 | 326 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
| CTE Alllows Gaps | On | 2,000 | 2001 | 4009 | 2 | 12001 | 47 | 75 |
| | On | 10,000 | 10001 | 20040 | 2 | 60001 | 312 | 413 |
| | Off | 2,000 | 2001 | 4006 | 2 | 12001 | 94 | 90 |
| | Off | 10,000 | 10001 | 20023 | 2 | 60001 | 313 | 349 |
+------------------+----------+--------+------------+---------------+------------+---------------+-------+---------+
Baik subquery yang berkorelasi maupun GROUP BY
versi menggunakan gabungan loop bersarang "segitiga" yang didorong oleh pemindaian indeks berkerumun pada RunningTotals
tabel (T1
) dan, untuk setiap baris yang dikembalikan oleh pemindaian itu, mencari kembali ke dalam tabel (T2
) bergabung sendiri di T2.ind<=T1.ind
.
Ini berarti bahwa baris yang sama diproses berulang kali. Ketika T1.ind=1000
baris diproses, self join mengambil dan menjumlahkan semua baris dengan ind <= 1000
, lalu untuk baris berikutnya di mana T1.ind=1001
1000 baris yang sama diambil lagi dan dijumlahkan dengan satu baris tambahan dan seterusnya.
Jumlah total operasi tersebut untuk tabel 2.000 baris adalah 2.001.000, untuk 10k baris 50.000.000 atau lebih umum (n² + n) / 2
yang jelas tumbuh secara eksponensial.
Dalam kasus 2.000 baris perbedaan utama antara GROUP BY
dan versi subquery adalah bahwa yang pertama memiliki agregat aliran setelah bergabung dan memiliki tiga kolom yang dimasukkan ke dalamnya (T1.ind
, T2.col1
, T2.col1
) dan GROUP BY
milik T1.ind
sedangkan yang terakhir dihitung sebagai agregat skalar, dengan agregat aliran sebelum bergabung, hanya memiliki T2.col1
memasukkannya ke dalamnya dan tidak memiliki GROUP BY
properti ditetapkan sama sekali. Pengaturan yang lebih sederhana ini dapat dilihat memiliki manfaat yang terukur dalam hal pengurangan waktu CPU.
Untuk kasus 10.000 baris ada perbedaan tambahan dalam rencana sub kueri. Itu menambahkan spool eager
yang menyalin semua ind,cast(col1 as bigint)
nilai ke dalam tempdb
. Dalam hal isolasi snapshot pada ini bekerja lebih kompak daripada struktur indeks berkerumun dan efek bersihnya adalah mengurangi jumlah pembacaan sekitar 25% (karena tabel dasar mempertahankan cukup banyak ruang kosong untuk info versi), ketika opsi ini dimatikan, hasilnya kurang ringkas (mungkin karena bigint
vs int
perbedaan) dan lebih banyak membaca hasil. Ini mengurangi kesenjangan antara subkueri dan grup menurut versi, tetapi subkueri tetap menang.
Namun pemenang yang jelas adalah CTE Rekursif. Untuk versi "tanpa celah" pembacaan logis dari tabel dasar sekarang 2 x (n + 1)
mencerminkan n
index mencari ke dalam indeks 2 level untuk mengambil semua baris ditambah yang tambahan di akhir yang tidak mengembalikan apa pun dan menghentikan rekursi. Itu masih berarti 20.002 pembacaan untuk memproses tabel 22 halaman!
Pembacaan tabel kerja logis untuk versi CTE rekursif sangat tinggi. Tampaknya berhasil pada 6 pembacaan meja kerja per baris sumber. Ini berasal dari spool indeks yang menyimpan output dari baris sebelumnya kemudian dibaca lagi di iterasi berikutnya (penjelasan yang bagus tentang ini oleh Umachandar Jayachandran di sini ). Meskipun angkanya tinggi, ini tetap yang terbaik.