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

Hasilkan satu set atau urutan tanpa loop – bagian 3

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 ]


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. WordPress – Di Balik Layar, Bagian 1

  2. Bung, siapa yang memiliki tabel #temp itu?

  3. Menghubungkan ke 4D dari Java

  4. Pertandingan Terdekat, Bagian 3

  5. Membandingkan Lapisan Abstraksi Database PHP dan Plugin CRUD