Làm cách nào để tạo một hàng cho mỗi ngày trong phạm vi ngày bằng cách sử dụng quy trình được lưu trữ?


10

Tôi muốn tạo một thủ tục được lưu trữ sẽ tạo một hàng trong bảng cho mỗi ngày trong một phạm vi ngày nhất định. Quy trình được lưu trữ chấp nhận hai đầu vào - Ngày bắt đầu và ngày kết thúc của phạm vi ngày mà người dùng mong muốn.

Vì vậy, hãy nói rằng tôi có một bảng như vậy:

SELECT Day, Currency
FROM ConversionTable

Ngày là một DateTime và Tiền tệ chỉ là một số nguyên.

Để đơn giản, hãy nói rằng tôi luôn muốn cột Tiền tệ là 1 cho mỗi hàng được chèn này. Vì vậy, nếu ai đó nhập 'ngày 5 tháng 3 năm 2017' làm ngày bắt đầu và 'ngày 11 tháng 4 năm 2017' là ngày kết thúc, tôi muốn các hàng sau được tạo:

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

Cách tốt nhất để mã hóa thủ tục được lưu trữ để làm điều này là gì? Tôi đang sử dụng SQL Server 2008 R2 trong môi trường thử nghiệm của mình, nhưng môi trường thực của chúng tôi sử dụng SQL Server 2012, vì vậy tôi có thể nâng cấp máy thử nghiệm của mình nếu có chức năng mới được giới thiệu vào năm 2012 giúp công việc này dễ dàng hơn.

Câu trả lời:


14

Một lựa chọn là CTE đệ quy:

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

( MAXRECURSIONgợi ý thêm vào nhờ bình luận của Scott Hodgin, bên dưới.)


Ồ, vâng! Tôi đã nghĩ đến cách tiếp cận bảng "số" (câu trả lời của Scott Hodgin). Đối với số ngày rất lớn, nó có thể hoạt động tốt hơn. Và một cách tiếp cận tái sử dụng tốt đẹp (câu trả lời của John C) luôn tốt. Đây chỉ là câu trả lời đơn giản và dễ hiểu nhất mà tôi có thể nghĩ ra.
RDFozz ngày

1
Chỉ cần nhớ - đệ quy có giới hạn mặc định là 100 khi bạn không chỉ định MAXRECURSION - Tôi ghét SP của bạn sẽ bị thổi khi phạm vi ngày rộng hơn được thông qua :)
Scott Hodgin

2
@ Hủy bỏ Hodgin Bạn nói đúng. Đã thêm MAXRECURSIONgợi ý cho truy vấn để giải quyết.
RDFozz ngày

Phương pháp này có hành vi cố ý hoặc vô ý bao gồm cả ngày tiếp theo của giá trị EndDate bao gồm cả thời gian. Nếu hành vi này là không mong muốn, hãy thay đổi WHERE thành DATEADD (NGÀY, 1, Ngày) <@EndDate
Chris Porter

1
@ChrisPorter - Điểm tuyệt vời! Tuy nhiên, nếu bạn thực hiện nó DATEADD(DAY, 1, theDate) < @EndDate, bạn sẽ không nhận được kết thúc của phạm vi khi cả hai giá trị datetime có cùng thành phần thời gian. Tôi đã sửa đổi câu trả lời một cách thích hợp, nhưng sử dụng <= @EndDate. Nếu bạn không muốn kết thúc giá trị phạm vi bao gồm bất kể, thì < @EndDatethực sự sẽ đúng.
RDFozz

6

Một tùy chọn khác là sử dụng Hàm-Giá trị Bảng. Cách tiếp cận này rất nhanh, và cung cấp một chút linh hoạt hơn. Bạn cung cấp Phạm vi Ngày / Giờ, DatePart và Tăng. Cũng cung cấp lợi thế bao gồm nó trong ỨNG DỤNG CROSS

Ví dụ

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

Trả về

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 nếu quan tâm

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 Rất đúng. Đã học từ lâu, KHÔNG BAO GIỜ có một câu trả lời đúng.
John Cappelletti ngày

Cảm ơn vi đa trả lơi. Có vẻ như có một số cách để làm điều này :) Người kia trả lời câu hỏi của tôi theo cách giải quyết chính xác đầu ra mong muốn được đặt ra, nhưng bạn đúng là bạn có vẻ linh hoạt hơn.
Rob V

@JohnCappelletti Điều này hoàn hảo cho những gì tôi cần làm, nhưng tôi cần nó để xóa Cuối tuần và Ngày lễ ... Tôi đã làm điều này bằng cách thay đổi dòng cuối cùng thành dòng sau D (1,7) VÀ KHÔNG VÀO (CHỌN Holiday_Date TỪ ngày lễ). Ngày lễ là một bảng chứa ngày nghỉ lễ.
MattE

@MattE Tốt lắm! Để loại trừ cuối tuần và ngày lễ, tôi cũng sẽ làm như vậy. :)
John Cappelletti

3

Sử dụng bài đăng của Aaron Bertrand về cách tạo bảng thứ nguyên ngày làm ví dụ, tôi đã đưa ra điều này:

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]

Bạn sẽ có thể đặt loại logic này trong quy trình được lưu trữ của bạn và thêm bất cứ thứ gì bạn cần.


2

Gần đây tôi đã phải giải quyết một vấn đề tương tự trên Redshift khi tôi chỉ có quyền truy cập đọc và do đó cần một giải pháp hoàn toàn dựa trên SQL (không có thủ tục được lưu trữ) để nhận một hàng cho mỗi giờ trong phạm vi ngày làm điểm bắt đầu cho tập kết quả của tôi. Tôi chắc rằng những người khác có thể làm cho điều này trở nên thanh lịch hơn và sửa đổi nó cho mục đích của họ, nhưng đối với những người cần, đây là giải pháp bị hack của tôi:

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;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.