Saklı yordamı kullanarak bir tarih aralığındaki her gün için bir satır nasıl oluşturulur?


11

Belirli bir tarih aralığında her gün için bir tabloda bir satır oluşturacak saklı yordam oluşturmak istiyorum. Saklı Yordam iki girişi kabul eder - kullanıcının istediği tarih aralığının başlangıç ​​tarihi ve bitiş tarihi.

Diyelim ki şöyle bir masam var:

SELECT Day, Currency
FROM ConversionTable

Gün bir DateTime ve Para Birimi yalnızca bir tamsayıdır.

İşleri basitleştirmek için, yalnızca eklenen bu satırların her biri için Para Birimi sütununun her zaman 1 olmasını istediğimi varsayalım. Birisi başlangıç ​​tarihi olarak '5 Mart 2017' ve bitiş tarihi olarak '11 Nisan 2017' girerse, aşağıdaki satırların oluşturulmasını isterim:

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

Bunu yapmak için saklı yordamı kodlamanın en iyi yolu nedir? Test ortamımda SQL Server 2008 R2 kullanıyorum, ancak gerçek ortamımız SQL Server 2012 kullanıyor, bu nedenle 2012'de tanıtılan bu görevi kolaylaştıran yeni işlevler varsa test makinemi yükseltebilirim.

Yanıtlar:


16

Bir seçenek, özyinelemeli bir CTE'dir:

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)
;

( MAXRECURSIONaşağıdaki Scott Hodgin'in yorumu sayesinde ipucu eklendi.)


Oh evet! Ben bir "sayılar" masa yaklaşımı (Scott Hodgin'in cevabı) düşündüm. Çok sayıda gün boyunca daha iyi performans gösterebilir. Ve güzel bir yeniden kullanılabilir yaklaşım (John C'nin cevabı) her zaman iyidir. Bu sadece aklıma gelen en basit ve en basit cevaptı.
RDFozz

1
Unutmayın - bir MAXRECURSION belirtmediğinizde yinelemenin varsayılan sınırı 100'dür - Daha geniş tarih aralıkları geçtiği zaman SP'nizin patlamasından nefret ediyorum :)
Scott Hodgin

2
@Scott Hodgin Haklısın. MAXRECURSIONÇözmek için sorguya ipucu eklendi .
RDFozz

Bu yöntem, EndDate değerinin ertesi gününü içeren bir kasıtlı veya kasıtsız davranışa sahiptir. Bu davranış istenmeyen bir durumsa, WHERE değerini DATEADD (DAY, 1, theDate) olarak değiştirin <@EDDate
Chris Porter

1
@ChrisPorter - Mükemmel nokta! Ancak, bunu yaparsanız DATEADD(DAY, 1, theDate) < @EndDate, her iki datetime değerinin de aynı zaman bileşenine sahip olduğu zaman aralığın sonunu alamazsınız. Cevabı uygun şekilde değiştirdim, ama kullandım <= @EndDate. Eğer yoksa istediğiniz aralık değerinin sonu ne olursa olsun dahil, o zaman < @EndDategerçekten de doğru olacaktır.
RDFozz

6

Başka bir seçenek de Tablo Değerli İşlev kullanmaktır. Bu yaklaşım çok hızlıdır ve biraz daha esneklik sunar. Tarih / Saat Aralığı, DatePart ve Artışı sağlarsınız. Ayrıca ÇAPRAZ UYGULAMAYA dahil etme avantajı sunar

Örneğin

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

İadeler

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

İlgileniyorsa UDF

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) 
*/

@RobV Çok doğru. Uzun zaman önce öğrendim ASLA tek bir gerçek cevap.
John Cappelletti

Yanıtınız için teşekkürler. Görünüşe göre bunu yapmanın birkaç yolu var :) Diğer adam sorumu istenen poz çıktısını tam olarak çözecek şekilde yanıtladı, ama haklısın, seninki daha esnek gibi görünüyor.
Rob V

@JohnCappelletti Bu yapmam gereken şey için mükemmel, ancak Hafta Sonları ve Tatiller'i kaldırmam gerekiyordu ... Bunu, son satırı aşağıdaki gibi değiştirerek yaptım Nerede D <= @ R2 VE DATEPART (Hafta içi, D) değil (1,7) VE D DEĞİL (Tatillerden Tatil_Tarih SEÇ). Tatiller, tatil tarihlerini içeren bir tablodur.
Mat

@MattE Aferin! Hafta sonlarını ve tatil günlerini hariç tutmak için aynı şeyi yapardım. :)
John Cappelletti

3

Aaron Bertrand'ın örnek bir tarih tablosu nasıl oluşturulacağına dair yazısını kullanarak , bununla geldim:

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]

Bu tür bir mantığı saklı yordamınıza koyabilmeniz ve ihtiyacınız olan başka bir şeyi eklemeniz gerekir.


2

Son zamanlarda Redshift üzerinde benzer bir sorunu çözmek zorunda kaldım, burada sadece okuma erişimim vardı ve bu yüzden sonuç kümem için bir başlangıç ​​noktası olarak bir tarih aralığında her saat için bir satır almak için tamamen SQL tabanlı bir çözüme (saklı yordamlar) ihtiyaç duydum. Eminim başkaları bunu daha zarif hale getirebilir ve amaçları için değiştirebilirler, ancak muhtaç olanlar için işte hacklenmiş çözümüm:

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;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.