วิธีสร้างแถวสำหรับทุกวันในช่วงวันที่โดยใช้กระบวนงานที่เก็บไว้ได้อย่างไร


11

ฉันต้องการสร้างกระบวนงานที่เก็บไว้ซึ่งจะสร้างแถวในตารางสำหรับทุกวันในช่วงวันที่ที่กำหนด กระบวนงานที่เก็บไว้ยอมรับสองอินพุต - วันที่เริ่มต้นและวันที่สิ้นสุดของช่วงวันที่ที่ผู้ใช้ต้องการ

ดังนั้นสมมติว่าฉันมีตารางดังนี้

SELECT Day, Currency
FROM ConversionTable

Day คือ DateTime และ Currency เป็นเพียงจำนวนเต็ม

เพื่อให้ง่ายขึ้นสมมติว่าฉันต้องการให้คอลัมน์สกุลเงินเป็น 1 สำหรับแต่ละแถวที่แทรกเหล่านี้ ดังนั้นหากมีคนป้อน '5 มีนาคม 2017' เป็นวันที่เริ่มต้นและ '11 เมษายน 2017' เป็นวันที่สิ้นสุดฉันต้องการสร้างแถวต่อไปนี้:

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

วิธีที่ดีที่สุดในการเขียนโค้ดของกระบวนงานที่เก็บไว้เพื่อทำสิ่งนี้คืออะไร ฉันใช้ SQL Server 2008 R2 ในสภาพแวดล้อมการทดสอบของฉัน แต่สภาพแวดล้อมจริงของเราใช้ SQL Server 2012 ดังนั้นฉันสามารถอัพเกรดเครื่องทดสอบของฉันได้หากมีฟังก์ชั่นใหม่ที่เปิดตัวในปี 2012 ที่ทำให้งานนี้ง่ายขึ้น

คำตอบ:


15

ทางเลือกหนึ่งคือ CTE แบบเรียกซ้ำ:

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

( MAXRECURSIONเพิ่มคำใบ้ขอบคุณข้อคิดเห็นของ Scott Hodgin ด้านล่าง)


โอ้ใช่! ฉันให้ความคิดกับวิธีการ "ตัวเลข" ของตาราง (คำตอบของ Scott Hodgin) สำหรับจำนวนวันที่มากมันอาจทำงานได้ดีขึ้น และวิธีการที่นำกลับมาใช้ใหม่ได้ดี (คำตอบของ John C) นั้นดีเสมอ นี่เป็นเพียงคำตอบที่ง่ายและตรงไปตรงมาที่สุดที่ฉันนึกได้
RDFozz

1
เพิ่งทราบ - การเรียกซ้ำมีขีด จำกัด เริ่มต้นที่ 100 เมื่อคุณไม่ระบุ MAXRECURSION - ฉันเกลียดที่ SP ของคุณจะระเบิดเมื่อผ่านช่วงวันที่กว้างขึ้นใน :)
Scott Hodgin

2
@Scott Hodgin คุณพูดถูก เพิ่มMAXRECURSIONคำใบ้ลงในคิวรีเพื่อแก้ไข
RDFozz

วิธีนี้มีพฤติกรรมที่ตั้งใจหรือไม่ตั้งใจซึ่งรวมถึงวันถัดไปของค่า EndDate ที่มีเวลา หากพฤติกรรมนี้ไม่เป็นที่พึงปรารถนาให้เปลี่ยนตำแหน่งเป็นวันที่ (วันที่ 1, วันที่) <@EndDate
Chris Porter

1
@ChrisPorter - จุดสุดยอด! อย่างไรก็ตามถ้าคุณทำDATEADD(DAY, 1, theDate) < @EndDateคุณจะไม่ได้รับช่วงเวลาที่สิ้นสุดเมื่อค่าวันที่และเวลามีองค์ประกอบเวลาเดียวกัน ผมปรับเปลี่ยนคำตอบที่เหมาะสม <= @EndDateแต่การใช้ หากคุณไม่ต้องการให้รวมค่าจุดสิ้นสุดของช่วงโดยไม่คำนึงว่า< @EndDateจะถูกต้องแน่นอน
RDFozz

6

ตัวเลือกอื่นคือการใช้ฟังก์ชั่น Table-Valued-Function วิธีนี้รวดเร็วและให้ความยืดหยุ่นมากกว่าเล็กน้อย คุณใส่วันที่ / ช่วงเวลา DatePart และเพิ่ม ยังมีข้อได้เปรียบของการรวมไว้ใน CROSS APPLY

ตัวอย่างเช่น

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

ผลตอบแทน

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 ถ้าสนใจ

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 จริงมาก เรียนรู้เมื่อนานมาแล้วไม่มีคำตอบที่แท้จริงเพียงข้อเดียว
John Cappelletti

ขอบคุณสำหรับการตอบกลับ. ดูเหมือนว่ามีหลายวิธีในการทำเช่นนี้ :) อีกคนหนึ่งตอบคำถามของฉันในแบบที่แก้ปัญหาผลลัพธ์ที่ต้องการได้อย่างแม่นยำ แต่คุณก็ถูกต้อง
Rob V

@JohnCappelletti สิ่งนี้เหมาะสำหรับสิ่งที่ฉันต้องทำ แต่ฉันต้องการให้ลบวันหยุดสุดสัปดาห์และวันหยุด ... ฉันทำสิ่งนี้โดยเปลี่ยนบรรทัดสุดท้ายเป็นบรรทัดต่อไปนี้โดยที่ D <= @ R2 และ DATEPART (วันธรรมดา, D) ไม่อยู่ใน (1,7) และไม่ได้อยู่ใน (เลือกวันหยุดจากวันหยุด) Holidays เป็นตารางที่มีวันที่วันหยุด
MattE

@MattE ทำได้ดีมาก! หากต้องการยกเว้นวันหยุดสุดสัปดาห์และวันหยุดฉันจะทำเช่นเดียวกัน :)
John Cappelletti

3

ใช้โพสต์ของ Aaron Bertrand เกี่ยวกับวิธีสร้างตารางส่วนข้อมูลวันที่ เป็นตัวอย่างฉันมาด้วยสิ่งนี้

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]

คุณควรใส่ตรรกะประเภทนี้ในขั้นตอนการจัดเก็บของคุณและเพิ่มสิ่งอื่นที่คุณต้องการ


2

ฉันต้องแก้ปัญหาที่คล้ายกันเมื่อเร็ว ๆ นี้ใน Redshift ที่ซึ่งฉันมีสิทธิ์เข้าถึงแบบอ่านเท่านั้นจึงจำเป็นต้องใช้โซลูชันที่ใช้ SQL ล้วนๆ (ไม่มีโพรซีเดอร์ที่เก็บไว้) เพื่อรับแถวทุกชั่วโมงในช่วงวันที่เป็นจุดเริ่มต้นสำหรับชุดผลลัพธ์ของฉัน ฉันแน่ใจว่าคนอื่นสามารถทำให้สิ่งนี้สวยงามยิ่งขึ้นและปรับเปลี่ยนเพื่อจุดประสงค์ของพวกเขา แต่สำหรับผู้ที่ต้องการนี่คือโซลูชันที่ถูกแฮ็กของฉัน:

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;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.