Bagaimana cara membuat baris untuk setiap hari dalam rentang tanggal menggunakan prosedur tersimpan?

11

Saya ingin membuat prosedur tersimpan yang akan membuat baris dalam tabel untuk setiap hari dalam rentang tanggal tertentu. Prosedur Tersimpan menerima dua input - Tanggal mulai dan tanggal akhir dari rentang tanggal yang diinginkan oleh pengguna.

Jadi, katakanlah saya punya tabel seperti ini:

SELECT Day, Currency
FROM ConversionTable

Hari adalah DateTime, dan Mata uang hanyalah bilangan bulat.

Agar semuanya sederhana, anggap saja saya selalu ingin kolom Mata Uang menjadi 1 untuk setiap baris yang disisipkan ini. Jadi, jika seseorang memasukkan '5 Maret 2017' sebagai tanggal mulai dan '11 April 2017' sebagai tanggal akhir, saya ingin baris-baris berikut dibuat:

2017-03-05 00:00:00, 1
2017-03-06 00:00:00, 1
...
2017-04-11 00:00:00, 1

Apa cara terbaik untuk mengkode prosedur tersimpan untuk melakukan ini? Saya menggunakan SQL Server 2008 R2 di lingkungan pengujian saya, tetapi lingkungan nyata kami menggunakan SQL Server 2012, jadi saya dapat memutakhirkan mesin pengujian saya jika ada fungsi baru yang diperkenalkan pada 2012 yang membuat tugas ini lebih mudah.

Rob V
sumber

Jawaban:

15

Salah satu opsi adalah CTE rekursif:

DECLARE @StartDate datetime = '2017-03-05'
       ,@EndDate   datetime = '2017-04-11'
;

WITH theDates AS
     (SELECT @StartDate as theDate
      UNION ALL
      SELECT DATEADD(day, 1, theDate)
        FROM theDates
       WHERE DATEADD(day, 1, theDate) <= @EndDate
     )
SELECT theDate, 1 as theValue
  FROM theDates
OPTION (MAXRECURSION 0)
;

( MAXRECURSIONPetunjuk ditambahkan berkat komentar Scott Hodgin, di bawah.)

RDFozz
sumber
Oh ya! Saya telah memikirkan pendekatan tabel "angka" (jawaban Scott Hodgin). Untuk jumlah hari yang sangat besar, mungkin kinerjanya lebih baik. Dan pendekatan yang dapat digunakan kembali yang bagus (jawaban John C) selalu baik. Ini hanya jawaban paling sederhana dan paling mudah yang bisa saya pikirkan.
RDFozz
1
Ingat saja - rekursi memiliki batas default 100 ketika Anda tidak menentukan MAXRECURSION - Saya benci SP Anda meledak ketika rentang tanggal yang lebih luas diteruskan :)
Scott Hodgin
2
@Scott Hodgin Anda benar. Menambahkan MAXRECURSIONpetunjuk ke kueri untuk diselesaikan.
RDFozz
Metode ini memiliki perilaku yang disengaja atau tidak disengaja termasuk hari berikutnya nilai EndDate termasuk waktu. Jika perilaku ini tidak diinginkan, ubah WHERE ke DATEADD (DAY, 1, theDate) <@EndDate
Chris Porter
1
@ ChrisPorter - Poin luar biasa! Namun, jika Anda membuatnya DATEADD(DAY, 1, theDate) < @EndDate, Anda tidak mendapatkan akhir rentang ketika kedua nilai datetime memiliki komponen waktu yang sama. Saya memodifikasi jawaban dengan tepat, tetapi menggunakan <= @EndDate. Jika Anda tidak ingin akhir dari nilai rentang dimasukkan, maka < @EndDatememang akan benar.
RDFozz
6

Pilihan lain adalah menggunakan Table-Valued-Function. Pendekatan ini sangat cepat, dan menawarkan sedikit lebih banyak fleksibilitas. Anda memberikan Rentang Tanggal / Waktu, DatePart, dan Penambahan. Juga menawarkan keuntungan dengan memasukkannya ke dalam CROSS APPLY

Sebagai contoh

Select * from [dbo].[udf-Range-Date]('2017-03-05','2017-04-11','DD',1) 

Kembali

RetSeq  RetVal
1   2017-03-05 00:00:00.000
2   2017-03-06 00:00:00.000
3   2017-03-07 00:00:00.000
4   2017-03-08 00:00:00.000
5   2017-03-09 00:00:00.000
...
36  2017-04-09 00:00:00.000
37  2017-04-10 00:00:00.000
38  2017-04-11 00:00:00.000

UDF jika Tertarik

CREATE FUNCTION [dbo].[udf-Range-Date] (@R1 datetime,@R2 datetime,@Part varchar(10),@Incr int)
Returns Table
Return (
    with cte0(M)   As (Select 1+Case @Part When 'YY' then DateDiff(YY,@R1,@R2)/@Incr When 'QQ' then DateDiff(QQ,@R1,@R2)/@Incr When 'MM' then DateDiff(MM,@R1,@R2)/@Incr When 'WK' then DateDiff(WK,@R1,@R2)/@Incr When 'DD' then DateDiff(DD,@R1,@R2)/@Incr When 'HH' then DateDiff(HH,@R1,@R2)/@Incr When 'MI' then DateDiff(MI,@R1,@R2)/@Incr When 'SS' then DateDiff(SS,@R1,@R2)/@Incr End),
         cte1(N)   As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
         cte2(N)   As (Select Top (Select M from cte0) Row_Number() over (Order By (Select NULL)) From cte1 a, cte1 b, cte1 c, cte1 d, cte1 e, cte1 f, cte1 g, cte1 h ),
         cte3(N,D) As (Select 0,@R1 Union All Select N,Case @Part When 'YY' then DateAdd(YY, N*@Incr, @R1) When 'QQ' then DateAdd(QQ, N*@Incr, @R1) When 'MM' then DateAdd(MM, N*@Incr, @R1) When 'WK' then DateAdd(WK, N*@Incr, @R1) When 'DD' then DateAdd(DD, N*@Incr, @R1) When 'HH' then DateAdd(HH, N*@Incr, @R1) When 'MI' then DateAdd(MI, N*@Incr, @R1) When 'SS' then DateAdd(SS, N*@Incr, @R1) End From cte2 )

    Select RetSeq = N+1
          ,RetVal = D 
     From  cte3,cte0 
     Where D<=@R2
)
/*
Max 100 million observations -- Date Parts YY QQ MM WK DD HH MI SS
Syntax:
Select * from [dbo].[udf-Range-Date]('2016-10-01','2020-10-01','YY',1) 
Select * from [dbo].[udf-Range-Date]('2016-01-01','2017-01-01','MM',1) 
*/
John Cappelletti
sumber
@RobV Sangat benar. Belajar sejak dulu tidak ada satu jawaban yang benar.
John Cappelletti
Terima kasih balasannya. Sepertinya ada beberapa cara untuk melakukan ini :) Orang lain menjawab pertanyaan saya dengan cara yang secara tepat menyelesaikan output yang diinginkan, tetapi Anda benar, milik Anda sepertinya lebih fleksibel.
Rob V
@JohnCappelletti Ini sempurna untuk apa yang harus saya lakukan, tetapi saya membutuhkannya untuk menghapus Akhir Pekan dan Liburan ... Saya melakukan ini dengan mengubah baris terakhir ke berikut Di mana D <= @ R2 DAN DATEPART (Hari Kerja, D) tidak dalam (1,7) DAN TIDAK DILAKUKAN (SELECT Holiday_Date FROM Holidays). Liburan adalah tabel yang berisi tanggal liburan.
MattE
@ MatE Bagus sekali! Untuk mengecualikan akhir pekan dan hari libur, saya akan melakukan hal yang sama. :)
John Cappelletti
3

Menggunakan pos Aaron Bertrand tentang cara membuat tabel dimensi tanggal sebagai contoh, saya membuat ini:

DECLARE @StartDate DATE ='2017-03-05 00:00:00'
DECLARE @EndDate DATE ='2017-04-11 00:00:00'

Declare @DateTable table ([date]       DATE PRIMARY KEY);

-- use the catalog views to generate as many rows as we need

INSERT @DateTable ([date])
SELECT d
FROM (
    SELECT d = DATEADD(DAY, rn - 1, @StartDate)
    FROM (
        SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate)) rn = ROW_NUMBER() OVER (
                ORDER BY s1.[object_id]
                )
        FROM sys.all_objects AS s1
        CROSS JOIN sys.all_objects AS s2
        -- on my system this would support > 5 million days
        ORDER BY s1.[object_id]
        ) AS x
    ) AS y;

SELECT *
FROM @DateTable
ORDER BY [date]

Anda harus dapat menempatkan jenis logika ini dalam prosedur tersimpan Anda dan menambahkan apa pun yang Anda butuhkan.

Scott Hodgin
sumber
2

Saya harus memecahkan masalah yang sama baru-baru ini di Redshift di mana saya hanya memiliki akses baca dan karenanya membutuhkan solusi murni berbasis SQL (tidak ada prosedur yang tersimpan) untuk mendapatkan baris untuk setiap jam dalam rentang tanggal sebagai titik awal untuk set hasil saya. Saya yakin orang lain dapat membuat ini lebih elegan dan memodifikasinya untuk keperluan mereka, tetapi bagi mereka yang membutuhkan, berikut ini adalah solusi saya yang diretas:

with hours as
   (select 0 clockhour union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9 union select 10 union select 11 union select 12 
    union select 13 union select 14 union select 15 union select 16 union select 17 union select 18 union select 19 union select 20 union select 21 union select 22 union select 23)
, days as
   (select *
    from 
       (select to_number(n0.number || n1.number, '99') daynum
        from
           (select 0 as number union select 1 union select 2 union select 3) as n0
           cross join
           (select 1 as number union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9 union select 0) as n1)
    where daynum between 1 and 31)
, months as
   (select 1 as monthnum, 'jan' as themonth, 31 as numdays union select 2, 'feb', 28 union select 3, 'mar', 31 union select 4, 'apr', 30 union select 5, 'may', 31 union select 6, 'jun', 30 
    union select 7, 'jul', 31 union select 8, 'aug', 31 union select 9, 'sep', 30 union select 10, 'oct', 31 union select 11, 'nov', 30 union select 12, 'dec', 31)
, years as
   (select century || decade || yr as yr
    from 
       (select 19 century union select 20) 
    cross join
       (select 0 decade union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) 
    cross join
       (select 0 yr union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9))
select cast(daynum || '-' || themonth || '-' || yr || ' ' || clockhour || ':00:00' as timestamp) dayhour
from hours
cross join days
cross join months
cross join years
where cast(daynum || '-' || themonth || '-' || yr || ' ' || clockhour || ':00:00' as timestamp) 
between date_trunc('month', dateadd('month', -$MONTHS_AGO, getdate()))
and     date_trunc('month', dateadd('month', $MONTHS_AHEAD, getdate()))
and   daynum <= numdays
order by yr, monthnum, daynum, clockhour;
Nathan
sumber