Sebelumnya di seri ini (Bagian 1 | Bagian 2) kita berbicara tentang menghasilkan serangkaian angka menggunakan berbagai teknik. Meskipun menarik, dan berguna dalam beberapa skenario, aplikasi yang lebih praktis adalah menghasilkan serangkaian tanggal yang berdekatan; misalnya, laporan yang mengharuskan menampilkan semua hari dalam sebulan, meskipun beberapa hari tidak ada transaksi.
Dalam posting sebelumnya saya menyebutkan bahwa mudah untuk mendapatkan serangkaian hari dari serangkaian angka. Karena kita telah menetapkan beberapa cara untuk menurunkan serangkaian angka, mari kita lihat bagaimana tampilan langkah selanjutnya. Mari kita mulai dengan sangat sederhana, dan berpura-pura ingin menjalankan laporan selama tiga hari, dari 1 Januari hingga 3 Januari, dan memasukkan satu baris untuk setiap hari. Cara kuno adalah dengan membuat tabel #temp, membuat loop, memiliki variabel yang menyimpan hari ini, di dalam loop, masukkan baris ke tabel #temp hingga akhir rentang, lalu gunakan # tabel temp ke luar bergabung dengan data sumber kami. Itu lebih banyak kode daripada yang ingin saya tunjukkan di sini, apalagi dimasukkan ke dalam produksi, pemeliharaan, dan buat rekan kerja belajar darinya.
Mulai sederhana
Dengan urutan angka yang ditetapkan (terlepas dari metode yang Anda pilih), tugas ini menjadi lebih mudah. Untuk contoh ini saya dapat mengganti generator urutan kompleks dengan serikat yang sangat sederhana, karena saya hanya perlu tiga hari. Saya akan membuat set ini berisi empat baris, sehingga juga mudah untuk mendemonstrasikan cara memotong ke seri yang Anda butuhkan.
Pertama, kami memiliki beberapa variabel untuk menahan awal dan akhir rentang yang kami minati:
DECLARE @s DATE = '2012-01-01', @e DATE = '2012-01-03';
Sekarang, jika kita mulai dengan generator seri sederhana, mungkin terlihat seperti ini. Saya akan menambahkan ORDER BY
di sini juga, hanya untuk amannya, karena kita tidak pernah bisa mengandalkan asumsi yang kita buat tentang ketertiban.
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT n FROM n ORDER BY n; -- result: n ---- 1 2 3 4
Untuk mengubahnya menjadi serangkaian tanggal, kita cukup menerapkan DATEADD()
dari tanggal mulai:
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT DATEADD(DAY, n, @s) FROM n ORDER BY n; -- result: ---- 2012-01-02 2012-01-03 2012-01-04 2012-01-05
Ini masih kurang tepat, karena jangkauan kami dimulai pada tanggal 2, bukan tanggal 1. Jadi untuk menggunakan tanggal mulai sebagai basis, kita perlu mengonversi set kita dari berbasis 1 ke berbasis 0. Kita bisa melakukannya dengan mengurangkan 1:
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT DATEADD(DAY, n-1, @s) FROM n ORDER BY n; -- result: ---- 2012-01-01 2012-01-02 2012-01-03 2012-01-04
Hampir sampai! Kami hanya perlu membatasi hasil dari sumber seri kami yang lebih besar, yang dapat kami lakukan dengan memberi makan DATEDIFF
, dalam hari, antara awal dan akhir rentang, ke TOP
operator – dan kemudian menambahkan 1 (sejak DATEDIFF
dasarnya melaporkan rentang terbuka).
;WITH n(n) AS (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM n ORDER BY n; -- result: ---- 2012-01-01 2012-01-02 2012-01-03
Menambahkan data nyata
Sekarang untuk melihat bagaimana kita akan bergabung dengan tabel lain untuk mendapatkan laporan, kita bisa menggunakan kueri baru dan gabungan luar itu untuk data sumber.
;WITH n(n) AS ( SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 ), d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM n ORDER BY n ) SELECT d.OrderDate, OrderCount = COUNT(o.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeader AS o ON o.OrderDate >= d.OrderDate AND o.OrderDate < DATEADD(DAY, 1, d.OrderDate) GROUP BY d.OrderDate ORDER BY d.OrderDate;
(Perhatikan bahwa kita tidak bisa lagi mengatakan COUNT(*)
, karena ini akan menghitung sisi kiri, yang akan selalu menjadi 1.)
Cara lain untuk menulis ini adalah:
;WITH d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM ( SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 ) AS n(n) ORDER BY n ) SELECT d.OrderDate, OrderCount = COUNT(o.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeader AS o ON o.OrderDate >= d.OrderDate AND o.OrderDate < DATEADD(DAY, 1, d.OrderDate) GROUP BY d.OrderDate ORDER BY d.OrderDate;
Ini akan memudahkan untuk membayangkan bagaimana Anda akan mengganti CTE terkemuka dengan pembuatan urutan tanggal dari sumber apa pun yang Anda pilih. Kami akan membahasnya (dengan pengecualian pendekatan CTE rekursif, yang hanya berfungsi untuk mencondongkan grafik), menggunakan AdventureWorks2012, tetapi kami akan menggunakan SalesOrderHeaderEnlarged
tabel yang saya buat dari skrip ini oleh Jonathan Kehayias. Saya menambahkan indeks untuk membantu kueri khusus ini:
CREATE INDEX d_so ON Sales.SalesOrderHeaderEnlarged(OrderDate);
Perhatikan juga bahwa saya memilih rentang tanggal arbitrer yang saya tahu ada di tabel.
Tabel angka
;WITH d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) DATEADD(DAY, n-1, @s) FROM dbo.Numbers ORDER BY n ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND CONVERT(DATE, s.OrderDate) = d.OrderDate WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
Rencana (klik untuk memperbesar):
spt_values
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; ;WITH d(OrderDate) AS ( SELECT DATEADD(DAY, n-1, @s) FROM (SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) ROW_NUMBER() OVER (ORDER BY Number) FROM master..spt_values) AS x(n) ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND CONVERT(DATE, s.OrderDate) = d.OrderDate WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
Rencana (klik untuk memperbesar):
sys.all_objects
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; ;WITH d(OrderDate) AS ( SELECT DATEADD(DAY, n-1, @s) FROM (SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) ROW_NUMBER() OVER (ORDER BY [object_id]) FROM sys.all_objects) AS x(n) ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND CONVERT(DATE, s.OrderDate) = d.OrderDate WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
Rencana (klik untuk memperbesar):
CTE bertumpuk
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; ;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 ), e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b), d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY n)-1, @s) FROM e2 ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND d.OrderDate = CONVERT(DATE, s.OrderDate) WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
Rencana (klik untuk memperbesar):
Nah, untuk rentang satu tahun, ini tidak akan memotongnya, karena hanya menghasilkan 100 baris. Selama satu tahun kita harus menutupi 366 baris (untuk memperhitungkan potensi tahun kabisat), sehingga akan terlihat seperti ini:
DECLARE @s DATE = '2006-10-23', @e DATE = '2007-10-22'; ;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 ), e2(n) AS (SELECT 1 FROM e1 CROSS JOIN e1 AS b), e3(n) AS (SELECT 1 FROM e2 CROSS JOIN (SELECT TOP (37) n FROM e2) AS b), d(OrderDate) AS ( SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) d = DATEADD(DAY, ROW_NUMBER() OVER (ORDER BY N)-1, @s) FROM e3 ) SELECT d.OrderDate, OrderCount = COUNT(s.SalesOrderID) FROM d LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND d.OrderDate = CONVERT(DATE, s.OrderDate) WHERE d.OrderDate >= @s AND d.OrderDate <= @e GROUP BY d.OrderDate ORDER BY d.OrderDate;
Rencana (klik untuk memperbesar):
Tabel kalender
Ini adalah yang baru yang tidak banyak kita bicarakan di dua posting sebelumnya. Jika Anda menggunakan deret tanggal untuk banyak kueri, maka Anda harus mempertimbangkan untuk memiliki tabel Angka dan tabel Kalender. Argumen yang sama berlaku tentang berapa banyak ruang yang benar-benar diperlukan dan seberapa cepat aksesnya ketika tabel sering ditanyakan. Misalnya, untuk menyimpan tanggal 30 tahun, dibutuhkan kurang dari 11.000 baris (jumlah pastinya tergantung pada berapa banyak tahun kabisat yang Anda rentangkan), dan hanya membutuhkan 200 KB. Ya, Anda tidak salah baca:200 kilobyte . (Dan dikompresi, hanya 136 KB.)
Untuk membuat tabel Kalender dengan data 30 tahun, dengan asumsi Anda telah yakin bahwa memiliki tabel Angka adalah hal yang baik, kita dapat melakukan ini:
DECLARE @s DATE = '2005-07-01'; -- earliest year in SalesOrderHeader DECLARE @e DATE = DATEADD(DAY, -1, DATEADD(YEAR, 30, @s)); SELECT TOP (DATEDIFF(DAY, @s, @e) + 1) d = CONVERT(DATE, DATEADD(DAY, n-1, @s)) INTO dbo.Calendar FROM dbo.Numbers ORDER BY n; CREATE UNIQUE CLUSTERED INDEX d ON dbo.Calendar(d);
Sekarang untuk menggunakan tabel Kalender dalam kueri laporan penjualan, kita dapat menulis kueri yang lebih sederhana:
DECLARE @s DATE = '2006-10-23', @e DATE = '2006-10-29'; SELECT OrderDate = c.d, OrderCount = COUNT(s.SalesOrderID) FROM dbo.Calendar AS c LEFT OUTER JOIN Sales.SalesOrderHeaderEnlarged AS s ON s.OrderDate >= @s AND s.OrderDate <= @e AND c.d = CONVERT(DATE, s.OrderDate) WHERE c.d >= @s AND c.d <= @e GROUP BY c.d ORDER BY c.d;
Rencana (klik untuk memperbesar):
Kinerja
Saya membuat salinan tabel Angka dan Kalender yang terkompresi dan tidak terkompresi, dan menguji rentang satu minggu, rentang satu bulan, dan rentang satu tahun. Saya juga menjalankan kueri dengan cache dingin dan cache hangat, tetapi ternyata sebagian besar tidak penting.
Durasi, dalam milidetik, untuk menghasilkan rentang selama seminggu
Durasi, dalam milidetik, untuk menghasilkan rentang sebulan
Durasi, dalam milidetik, untuk menghasilkan rentang sepanjang tahun
tambahan
Paul White (blog | @SQL_Kiwi) menunjukkan bahwa Anda dapat memaksa tabel Numbers untuk menghasilkan rencana yang jauh lebih efisien menggunakan kueri berikut:
SELECT OrderDate = DATEADD(DAY, n, 0), OrderCount = COUNT(s.SalesOrderID) FROM dbo.Numbers AS n LEFT OUTER JOIN Sales.SalesOrderHeader AS s ON s.OrderDate >= CONVERT(DATETIME, @s) AND s.OrderDate < DATEADD(DAY, 1, CONVERT(DATETIME, @e)) AND DATEDIFF(DAY, 0, OrderDate) = n WHERE n.n >= DATEDIFF(DAY, 0, @s) AND n.n <= DATEDIFF(DAY, 0, @e) GROUP BY n ORDER BY n;
Pada titik ini saya tidak akan menjalankan kembali semua tes kinerja (latihan untuk pembaca!), tetapi saya akan berasumsi bahwa itu akan menghasilkan pengaturan waktu yang lebih baik atau serupa. Namun, saya pikir tabel Kalender adalah hal yang berguna untuk dimiliki meskipun tidak terlalu diperlukan.
Kesimpulan
Hasilnya berbicara sendiri. Untuk menghasilkan serangkaian angka, pendekatan tabel Numbers menang, tetapi hanya sedikit – bahkan pada 1.000.000 baris. Dan untuk serangkaian kencan, di ujung bawah, Anda tidak akan melihat banyak perbedaan antara berbagai teknik. Namun, cukup jelas bahwa ketika rentang tanggal Anda semakin besar, terutama ketika Anda berurusan dengan tabel sumber yang besar, tabel Kalender benar-benar menunjukkan nilainya – terutama mengingat jejak memorinya yang rendah. Bahkan dengan sistem metrik aneh Kanada, 60 milidetik jauh lebih baik daripada sekitar 10 *detik* ketika hanya mengeluarkan 200 KB pada disk.
Saya harap Anda menikmati seri kecil ini; itu adalah topik yang sudah lama ingin saya kunjungi kembali.
[ Bagian 1 | Bagian 2 | Bagian 3 ]