Ada banyak kasus penggunaan untuk menghasilkan urutan nilai di SQL Server. Saya tidak berbicara tentang IDENTITY
yang bertahan kolom (atau SEQUENCE
baru di SQL Server 2012), melainkan set sementara untuk digunakan hanya selama masa berlaku kueri. Atau bahkan kasus yang paling sederhana – seperti hanya menambahkan nomor baris ke setiap baris dalam kumpulan hasil – yang mungkin melibatkan penambahan ROW_NUMBER()
berfungsi untuk kueri (atau, lebih baik lagi, di tingkat presentasi, yang tetap harus mengulang hasil baris demi baris).
Saya berbicara tentang kasus yang sedikit lebih rumit. Misalnya, Anda mungkin memiliki laporan yang menunjukkan penjualan menurut tanggal. Kueri tipikal mungkin:
SELECT OrderDate = CONVERT(DATE, OrderDate), OrderCount = COUNT(*) FROM dbo.Orders GROUP BY CONVERT(DATE, OrderDate) ORDER BY OrderDate;
Masalah dengan kueri ini adalah, jika tidak ada pesanan pada hari tertentu, tidak akan ada baris untuk hari itu. Hal ini dapat menyebabkan kebingungan, data yang menyesatkan, atau bahkan perhitungan yang salah (pikirkan rata-rata harian) untuk konsumen hilir data.
Jadi ada kebutuhan untuk mengisi celah tersebut dengan tanggal yang tidak ada dalam data. Dan terkadang orang akan memasukkan data mereka ke tabel #temp dan menggunakan WHILE
loop atau kursor untuk mengisi tanggal yang hilang satu per satu. Saya tidak akan menunjukkan kode itu di sini karena saya tidak ingin menganjurkan penggunaannya, tetapi saya telah melihatnya di mana-mana.
Namun, sebelum kita masuk terlalu jauh ke dalam tanggal, pertama-tama mari kita bicara tentang angka, karena Anda selalu dapat menggunakan urutan angka untuk mendapatkan urutan tanggal.
Tabel angka
Saya telah lama menjadi pendukung penyimpanan "tabel angka" tambahan pada disk (dan, dalam hal ini, tabel kalender juga).
Berikut adalah salah satu cara untuk menghasilkan tabel angka sederhana dengan nilai 1.000.000:
SELECT TOP (1000000) n = CONVERT(INT, ROW_NUMBER() OVER (ORDER BY s1.[object_id])) INTO dbo.Numbers FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2 OPTION (MAXDOP 1); CREATE UNIQUE CLUSTERED INDEX n ON dbo.Numbers(n) -- WITH (DATA_COMPRESSION = PAGE) ;
Kenapa MAXDOP 1? Lihat entri blog Paul White dan item Connect-nya terkait dengan sasaran baris.
Namun, banyak orang menentang pendekatan tabel bantu. Argumen mereka:mengapa menyimpan semua data itu di disk (dan di memori) ketika mereka bisa menghasilkan data dengan cepat? Penghitung saya adalah bersikap realistis dan memikirkan apa yang Anda optimalkan; komputasi bisa mahal, dan apakah Anda yakin menghitung rentang angka dengan cepat akan selalu lebih murah? Sejauh ruang, tabel Numbers hanya membutuhkan sekitar 11 MB terkompresi, dan 17 MB tidak terkompresi. Dan jika tabel direferensikan cukup sering, tabel itu harus selalu ada di memori, membuat akses cepat.
Mari kita lihat beberapa contoh, dan beberapa pendekatan yang lebih umum digunakan untuk memuaskan mereka. Saya harap kita semua dapat sepakat bahwa, bahkan pada nilai 1.000, kita tidak ingin menyelesaikan masalah ini menggunakan loop atau kursor.
Membuat urutan 1.000 angka
Mulai dari yang sederhana, mari buat kumpulan angka dari 1 hingga 1.000.
Tabel angka
Tentu saja dengan tabel angka, tugas ini cukup sederhana:
SELECT TOP (1000) n FROM dbo.Numbers ORDER BY n;
Rencana:
spt_values
Ini adalah tabel yang digunakan oleh prosedur tersimpan internal untuk berbagai tujuan. Penggunaannya secara online tampaknya cukup lazim, meskipun tidak didokumentasikan, tidak didukung, mungkin suatu hari akan hilang, dan karena hanya berisi kumpulan nilai yang terbatas, tidak unik, dan tidak berdekatan. Ada 2.164 unik dan 2.508 nilai total di SQL Server 2008 R2; pada tahun 2012 ada 2.167 unik dan 2.515 total. Ini termasuk duplikat, nilai negatif, dan bahkan jika menggunakan DISTINCT
, banyak celah setelah Anda melampaui angka 2.048. Jadi solusinya adalah menggunakan ROW_NUMBER()
untuk menghasilkan urutan yang berdekatan, mulai dari 1, berdasarkan nilai dalam tabel.
SELECT TOP (1000) n = ROW_NUMBER() OVER (ORDER BY number) FROM [master]..spt_values ORDER BY n;
Rencana:
Artinya, hanya untuk 1.000 nilai, Anda dapat menulis kueri yang sedikit lebih sederhana untuk menghasilkan urutan yang sama:
SELECT DISTINCT n = number FROM master..[spt_values] WHERE number BETWEEN 1 AND 1000;
Ini mengarah ke rencana yang lebih sederhana, tentu saja, tetapi rusak cukup cepat (setelah urutan Anda harus lebih dari 2.048 baris):
Bagaimanapun, saya tidak merekomendasikan penggunaan tabel ini; Saya menyertakannya untuk tujuan perbandingan, hanya karena saya tahu seberapa banyak dari ini di luar sana, dan betapa menggoda untuk menggunakan kembali kode yang Anda temukan.
sys.all_objects
Pendekatan lain yang telah menjadi salah satu favorit saya selama bertahun-tahun adalah menggunakan sys.all_objects
. Seperti spt_values
, tidak ada cara yang dapat diandalkan untuk menghasilkan urutan yang berdekatan secara langsung, dan kami memiliki masalah yang sama dalam menangani set yang terbatas (hanya di bawah 2.000 baris di SQL Server 2008 R2, dan lebih dari 2.000 baris di SQL Server 2012), tetapi untuk 1.000 baris kita dapat menggunakan ROW_NUMBER()
yang sama menipu. Alasan saya menyukai pendekatan ini adalah karena (a) ada sedikit kekhawatiran bahwa tampilan ini akan hilang dalam waktu dekat, (b) tampilan itu sendiri didokumentasikan dan didukung, dan (c) akan berjalan di database apa pun pada versi apa pun sejak SQL Server 2005 tanpa harus melewati batas basis data (termasuk basis data yang ada).
SELECT TOP (1000) n = ROW_NUMBER() OVER (ORDER BY [object_id]) FROM sys.all_objects ORDER BY n;
Rencana:
CTE bertumpuk
Saya percaya Itzik Ben-Gan layak mendapatkan pujian tertinggi untuk pendekatan ini; pada dasarnya Anda membuat CTE dengan sejumlah kecil nilai, lalu Anda membuat produk Cartesian terhadap dirinya sendiri untuk menghasilkan jumlah baris yang Anda butuhkan. Dan lagi, alih-alih mencoba menghasilkan kumpulan yang berdekatan sebagai bagian dari kueri yang mendasarinya, kita cukup menerapkan ROW_NUMBER()
ke hasil akhir.
;WITH e1(n) AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 ), -- 10 e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b), -- 10*10 e3(n) AS (SELECT 1 FROM e1 CROSS JOIN e2) -- 10*100 SELECT n = ROW_NUMBER() OVER (ORDER BY n) FROM e3 ORDER BY n;
Rencana:
CTE Rekursif
Akhirnya, kami memiliki CTE rekursif, yang menggunakan 1 sebagai jangkar, dan menambahkan 1 hingga kami mencapai maksimum. Untuk keamanan saya tentukan maksimum di kedua WHERE
klausa bagian rekursif, dan dalam MAXRECURSION
pengaturan. Tergantung pada berapa banyak nomor yang Anda butuhkan, Anda mungkin harus mengatur MAXRECURSION
ke 0
.
;WITH n(n) AS ( SELECT 1 UNION ALL SELECT n+1 FROM n WHERE n < 1000 ) SELECT n FROM n ORDER BY n OPTION (MAXRECURSION 1000);
Rencana:
Kinerja
Tentu saja dengan 1.000 nilai, perbedaan performa dapat diabaikan, tetapi akan berguna untuk melihat performa opsi yang berbeda ini:
Waktu proses, dalam milidetik, untuk menghasilkan 1.000 angka yang berurutan
Saya menjalankan setiap kueri 20 kali dan mengambil runtime rata-rata. Saya juga menguji dbo.Numbers
tabel, dalam format terkompresi dan tidak terkompresi, dan dengan cache dingin dan cache hangat. Dengan cache hangat, ini sangat menyaingi opsi tercepat lainnya di luar sana (spt_values
, tidak direkomendasikan, dan CTE bertumpuk), tetapi hit pertama relatif mahal (walaupun saya hampir tertawa menyebutnya begitu).
Bersambung…
Jika ini adalah kasus penggunaan khas Anda, dan Anda tidak akan menjelajah jauh melampaui 1.000 baris, maka saya harap saya telah menunjukkan cara tercepat untuk menghasilkan angka-angka itu. Jika kasus penggunaan Anda adalah angka yang lebih besar, atau jika Anda mencari solusi untuk menghasilkan urutan tanggal, pantau terus. Nanti di seri ini, saya akan mengeksplorasi menghasilkan urutan 50.000 dan 1.000.000 angka, dan rentang tanggal mulai dari satu minggu hingga satu tahun.
[ Bagian 1 | Bagian 2 | Bagian 3 ]