通过变量将 day/month/year 间隔传递给 T-SQL 中的内置 DATEADD 函数

Pass day/month/year interval via variable to built-in DATEADD function in T-SQL

我正在尝试在 t-sql 的 DATEADD 内置函数中为 'interval' 使用变量值。然而,这returns一个错误:'Invalid parameter 1 specified for dateadd'。在 t-sql 中有没有办法添加这个间隔输入参数,或者通过下面的变量,或者通过 table 列/临时 table 列?

DECLARE @start datetime = '20211122',
    @freq int = 7,
    @freqdaypart nvarchar(20) = N'day',
    @now datetime = GETDATE()

;WITH cte(st) AS (
    SELECT @start st
    UNION ALL
    SELECT DATEADD(@freqdaypart, @freq, st)
    FROM cte
    WHERE DATEADD(@freqdaypart, @freq, st) < @now
)
SELECT * 
FROM cte
OPTION(MAXRECURSION 0)

我的退路是做类似下面的事情,但我想尽可能避免这种情况..

DECLARE @start datetime = '20211122',
    @freq int = 7,
    @freqdaypart nvarchar(20) = N'day',
    @now datetime = GETDATE()

IF @freqdaypart = 'day'
BEGIN
    ;WITH cte(st) AS (
        SELECT @start st
        UNION ALL
        SELECT DATEADD(DAY, @freq, st)
        FROM cte
        WHERE DATEADD(DAY, @freq, st) < @now
    )
    SELECT * 
    FROM cte
    OPTION(MAXRECURSION 0)
END

IF @freqdaypart = 'month'
...

ps。以上是简化的,在实际使用中我打算从数据库table中检索间隔('day'或'month')并一次处理几行。

你可以使用动态 SQL,像这样

DECLARE @sqlToExecute NVARCHAR(4000);

DECLARE @start datetime = '20211122',
    @freq int = 7,
    @freqdaypart nvarchar(20) = N'day',
    @now datetime = GETDATE()

SET @sqlToExecute = N'

;WITH cte(st) AS (
    SELECT @start st
    UNION ALL
    SELECT DATEADD(' + @freqdaypart + ', @freq, st)
    FROM cte
    WHERE DATEADD(' + @freqdaypart + ', @freq, st) < @now
)
SELECT * 
FROM cte
OPTION(MAXRECURSION 0)
'

EXEC sp_executesql @sqlToExecute, 
    N'@start DATETIME, 
    @freq INT, 
    @now DATETIME
    ', @start, @freq, @now

特此向 SQLServerCentral.com 上的 Jonathan Roberts 致敬...

 CREATE OR ALTER FUNCTION dbo.DateRange 
/**********************************************************************************************************************
 Purpose:
 Given a start date, an end date, a "date part", and an increment, return a sequence of rows for the given date part
 according to the dates and the increment.
-----------------------------------------------------------------------------------------------------------------------
 Parameters:

 @StartDate: Start date of the series    - Required - May be greater than @EndDate 
 @EndDate  : End date of the series      - Required - May be less than @StartDate  
 @DatePart : The time unit for @interval - Optional (Default = 'dd')
     ns    : nanoseconds 
     mcs   : microseconds 
     ms    : milliseconds 
     ss    : seconds
     mi    : minutes
     hh    : hours
     dd    : days
     ww    : weeks
     mm    : months
     qq    : quarters
     yy    : years
 @Interval : The number of dateparts between each value returned - Optional (Default = 1)
------------
     Return: DT as a DATETIME2(7) Column of dates and times
-----------------------------------------------------------------------------------------------------------------------
 Sample Calls:
--===== Return a row for every other second in the date range.
 SELECT * FROM dbo.DateRange('2011-01-01 12:24:35', '2011-02-01 12:24:35', 'ss', 2)
;
--===== Return a coumt of rows for every millisecond in the date range (Default increment = 1)
     -- A simple DATEDIFF would do the trick but it does demonstate the scope of the function.
 SELECT COUNT(*) FROM dbo.DateRange('2018-01-01 00:00:00', '2018-01-25 20:31:23.646', 'ms', default)
;
--===== Return a row for each date in the date range (1st default is "dd" and second default is 1)
 SELECT * FROM dbo.DateRange('2011-01-01', '2012-02-03', default, default)
;
--===== Since @StartDate > @EndDate, this returns a row for every 7 days counting backwards.
 SELECT * FROM dbo.DateRange('2012-02-03', '2011-01-01', 'dd', 7)
;
--===== This demonstrates how you can do calculations in the SELECT list.
 SELECT DATEDIFF(ns,'2018-01-01 00:00:00.000',Value),Value,* 
   FROM dbo.DateRange('2018-01-01 00:00:00.000', '2018-01-01 00:00:00.00001', 'ns', 100)
;
--===== See the following link for an example that seriously simplified the task at hand as well as making it easy to 
     -- add other "granularities" (date parts).
        https://www.sqlservercentral.com/forums/topic/tsql-create-dynamic-partition-datarange#post-3944333
-----------------------------------------------------------------------------------------------------------------------
 Revision History:
 Rev 00 - 19 Aug 2019 - Jonathan Roberts
        - Initial Release
        - https://www.sqlservercentral.com/scripts/a-daterange-table-valued-function

 Rev 01 - 25 Oct 2021 - Jeff Moden.
        - Apply personal code standards and additional information in the documentation
        - Add link to extreme sample usage where the function greatly simplified the task at hand as well as making it
          easy to add other "granularities".
        - Add WITH SCHEMABINDING.
        - Code reduction by moving /@Interval from each THEN to END of CASE.
 *********************************************************************************************************************/  
--===== Function I/O
        (
         @StartDate DATETIME2
        ,@EndDate   DATETIME2
        ,@DatePart  VARCHAR(3) = 'dd'
        ,@Interval  INT        = 1
        )
RETURNS TABLE WITH SCHEMABINDING
     AS RETURN WITH
--===== 16 digit base for InLine Tally Table
H(Z) AS (SELECT 0 FROM (VALUES (0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))H0(Z)),
--===== InLine Tally Table returns rows starting at 0 based on the DATEPART divided by the interval.
T(N) AS (SELECT TOP(ABS(
                CASE @DatePart
                WHEN 'ns'  THEN DATEDIFF(ns, @EndDate,@StartDate)
                WHEN 'mcs' THEN DATEDIFF(mcs,@EndDate,@StartDate)
                WHEN 'ms'  THEN DATEDIFF(ms, @EndDate,@StartDate)
                WHEN 'ss'  THEN DATEDIFF(ss, @EndDate,@StartDate)
                WHEN 'mi'  THEN DATEDIFF(mi, @EndDate,@StartDate)
                WHEN 'hh'  THEN DATEDIFF(hh, @EndDate,@StartDate)
                WHEN 'dd'  THEN DATEDIFF(dd, @EndDate,@StartDate)
                WHEN 'ww'  THEN DATEDIFF(ww, @EndDate,@StartDate)
                WHEN 'mm'  THEN DATEDIFF(mm, @EndDate,@StartDate)
                WHEN 'qq'  THEN DATEDIFF(qq, @EndDate,@StartDate)
                WHEN 'yy'  THEN DATEDIFF(yy, @EndDate,@StartDate)
                ELSE DATEDIFF(dd --Ensures we get a correct positive value if dates are reversed
                             ,IIF(@StartDate < @EndDate, @StartDate, @EndDate)
                             ,IIF(@StartDate < @EndDate, @EndDate, @StartDate)
                             )
                END/@Interval) + 1) --End of TOP(ABS, Adds 1 interval to makeup for subtraction of INTs
            N = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 --So we start at ZERO
           FROM H a,H b,H c,H d,H e,H f,H g,H h -- A maximum of 16^8 (or 2^32) rows can be returned
        )
         SELECT DT = 
                CASE @DatePart            
                WHEN 'ns'  THEN DATEADD(ns, c.CountAmount,@StartDate)
                WHEN 'mcs' THEN DATEADD(mcs,c.CountAmount,@StartDate)
                WHEN 'ms'  THEN DATEADD(ms, c.CountAmount,@StartDate)
                WHEN 'ss'  THEN DATEADD(ss, c.CountAmount,@StartDate)
                WHEN 'mi'  THEN DATEADD(mi, c.CountAmount,@StartDate)
                WHEN 'hh'  THEN DATEADD(hh, c.CountAmount,@StartDate)
                WHEN 'dd'  THEN DATEADD(dd, c.CountAmount,@StartDate)
                WHEN 'ww'  THEN DATEADD(ww, c.CountAmount,@StartDate)
                WHEN 'mm'  THEN DATEADD(mm, c.CountAmount,@StartDate)
                WHEN 'qq'  THEN DATEADD(qq, c.CountAmount,@StartDate)
                WHEN 'yy'  THEN DATEADD(yy, c.CountAmount,@StartDate)
                ELSE            DATEADD(dd, c.CountAmount,@StartDate)
                END 
           FROM T t
          CROSS APPLY(VALUES(IIF(@StartDate<@EndDate
                                ,@Interval*(t.N)  --Count Up
                                ,@Interval*(-t.N) --Count Down
                                )))c(CountAmount)
;