在SQL Server中创建持续时间函数

时间:2017-01-03 15:13:13

标签: sql sql-server function date

我正在尝试创建一个函数,该函数将返回两个日期之间经过的整个周期数,而不是跨越的边界数。

例如myFunc(小时,' 01:31',' 03:20')= 1(在" 03:31"将成为2)

我遇到了3个问题:

  1. 日期计算很复杂,我想保持我的代码整洁,尽可能简单,所以我想使用内置的日期函数,而不是重新发明格里高利历。

  2. DateDiff接受一个datepart参数,该参数抵制了我作为变量传递的所有尝试,除了通过动态sql。

  3. 我需要在select语句中调用它,因此它不能成为存储过程,但SQL中的函数拒绝运行动态SQL或存储过程。

  4. (由于我正在运行SSMS 2012,因此我遇到了一些超载DateDiff的问题因此没有DateDiff_BIG)

  5. 这是我的代码:

        [dbo].[DDif]
    (
        @Start datetime
        ,@End datetime
        ,@Period varchar = null
    )
    RETURNS bigint
    Declare @Dif bigint
    if @Period is null set @Period = 'Y'
    @p varchar(20) = case
        when @Period = 'Y' then 'yy'
        when @Period in('S','Q') then 'q'
        when @Period = 'M' then 'm'
        when @Period in('B','W') then 'ww'
        when @Period = 'D' then 'd' 
        when @Period = 'h' then 'hh'
        when @Period = 'm' then 'n'
        when @Period = 's' then 's'
    End
    @sQry varchar(8000) = 'Set @Dif = DateDiff('+@p+',@Start,@End) + case when DateAdd('+@p+',DateDiff('+@p+',@Start,@End),@Start)>@End then -1 else 0 end'
    execute @sQry
    if @Period in ('S','B') return @Dif/2
    return @Dif
    

    我正在寻找3种解决方案中的任何一种:

    1. 将dateparts作为变量传递的非动态方式

    2. 从函数

    3. 运行动态SQL的方法
    4. 用于计算用户指定期间长度的两个日期时间之间的持续时间的不同方法的简洁/简单实现。

    5. 注意:如果要删除堆栈溢出的代码中的一些小部分,那么我知道它不会像它出现的那样完全运行。

      编辑: 感谢Mark的解决方案。这是我目前的实施:

      FUNCTION [dbo].[DDif]
      (
          @Start datetime
          ,@End datetime
          ,@Period varchar = null
      )
      RETURNS bigint
      AS
      BEGIN
          if @Period is null set @Period = 'Y'
          declare @m int = DATEDIFF(Month,@Start,@End) + case when DATEADD(MONTH,DATEDIFF(Month,@Start,@End),@Start)>@End then -1 else 0 END
          declare @s bigint = 86400*DATEDIFF(DAY, CAST(@Start as date),CAST(@End as date))+datediff(s,cast(@Start as time),cast(@End as time))
          declare @num bigint = case 
              when @Period = 'Y' then 12
              when @Period = 'S' then 6
              when @Period = 'Q' then 3
              when @Period = 'M' then 1
              when @Period = 'B' then 1209600
              when @Period = 'W' then 604800
              when @Period = 'D' then 86400
              when @Period = 'h' then 3600
              when @Period = 'm' then 60
              when @Period = 's' then 1
          end
          return case
              when @Period in ('Y','S','Q','M') then @m
              when @Period in ('B','W','D','h','m','s') then @s
          end / @num
      

3 个答案:

答案 0 :(得分:4)

您可以使用与时间戳中最小有意义精度对应的日期部分调用DateDiff,然后除以以正确单位获取间隔。

例如,如果您将时间存储到第二个,要将小时数DateDiff调用ss,将日期部分设为date_diff,然后再除以3600.想法是,如果您的时间只有精确到秒,然后“交叉1秒边界”意味着与所有可测量目的的“花费1秒钟”相同,然后你只需转换为你真正需要的单位。

如果你的间隔很长而你的时间戳是高精度的 - 例如如果你需要使用微秒精度一年的间隔 - 你可能不得不调用DateDiff_Big。

<强>更新

如果您需要更长的时间间隔的更高精度并且不能使用DateDiff_Big,那么您必须变得有点棘手。一种方法是:

首先将日期与时间分开。

然后date_diff日期部分,以天为单位。 (这有几百万年的范围。)将结果乘以86,400,000(结果将需要64位存储。)

现在date_diff ms精度的时间。

注意@import url('https://fonts.googleapis.com/css?family=Roboto'); 进行了签名比较,因此后者差异可能是正面的,也可能是负面的。通过乘以日期部分的差异将其添加到您获得的部分,现在您可以准确计算ms。

除以得到你想要的单位。

更新2 - 我原先声称纳秒会起作用,但那是因为我无法正确移动小数点。

答案 1 :(得分:0)

试试这个,
你可以添加你需要的所有DATEPARTS。

它也可以作为独立查询工作,只有当你发现它有用时才真正需要功能。

如果您不需要纳秒精度,则可以使用datetime数据类型而不是datetime2,它可以更容易处理。

编辑:更正了边界行为

UPDATE2:大量语法简化

UPDATE3:恢复原始语法以避免闰年和不同月份的长度问题

DROP FUNCTION FN_CALC_DATE_DIFF;
GO

CREATE FUNCTION FN_CALC_DATE_DIFF(
    @INTERVALTYPE AS INT = NULL,
    @START AS datetime,
    @END AS datetime
)
RETURNS BIGINT
AS
BEGIN

RETURN  (SELECT CASE @INTERVALTYPE    
            WHEN 0 THEN DATEDIFF(NS, DATEADD(NS, DATEDIFF(NS, 0, @START), 0), DATEADD(NS, DATEDIFF(NS, 0, @START), 0) + @Start-@End) 
            WHEN 1 THEN DATEDIFF(MCS, DATEADD(MCS, DATEDIFF(MCS, 0, @START), 0), DATEADD(MCS, DATEDIFF(MCS, 0, @START), 0) + @END-@START)
            WHEN 2 THEN DATEDIFF(MS, DATEADD(MS, DATEDIFF(MS, 0, @START), 0), DATEADD(MS, DATEDIFF(MS, 0, @START), 0) + @END-@START)
            WHEN 3 THEN DATEDIFF(SS, DATEADD(SS, DATEDIFF(SS, 0, @START), 0), DATEADD(SS, DATEDIFF(SS, 0, @START), 0) + @END-@START)
            WHEN 4 THEN DATEDIFF(MI, DATEADD(MI, DATEDIFF(MI, 0, @START), 0), DATEADD(MI, DATEDIFF(MI, 0, @START), 0) + @END-@START)
            WHEN 5 THEN DATEDIFF(HH, DATEADD(HH, DATEDIFF(HH, 0, @START), 0), DATEADD(HH, DATEDIFF(HH, 0, @START), 0) + @END-@START)
            WHEN 6 THEN DATEDIFF(DD, DATEADD(DD, DATEDIFF(DD, 0, @START), 0), DATEADD(DD, DATEDIFF(DD, 0, @START), 0) + @END-@START)
            WHEN 7 THEN DATEDIFF(WK, DATEADD(WK, DATEDIFF(WK, 0, @START), 0), DATEADD(WK, DATEDIFF(WK, 0, @START), 0) + @END-@START)
            WHEN 8 THEN DATEDIFF(MM, DATEADD(MM, DATEDIFF(MM, 0, @START), 0), DATEADD(MM, DATEDIFF(MM, 0, @START), 0) + @END-@START)
            WHEN 9 THEN DATEDIFF(YY, DATEADD(YY, DATEDIFF(YY, 0, @START), 0), DATEADD(YY, DATEDIFF(YY, 0, @START), 0) + @END-@START)
        ELSE
            DATEDIFF(SS, DATEADD(SS, DATEDIFF(SS, 0, @START), 0), DATEADD(SS, DATEDIFF(SS, 0, @START), 0) + @END-@START) -- default to
       END )
END
GO

这个测试

declare @d1 datetime = '31/12/2016'
declare @d2 datetime = '01/01/2017'

declare @h1 datetime = '01:31'
declare @h2 datetime = '03:20'


select 
    DBO.FN_CALC_DATE_DIFF(5, @h1, @h2) DIFF_HOURS,
    DBO.FN_CALC_DATE_DIFF(9, @d1, @d2) DIFF_YEARS

将产生

DIFF_HOURS  DIFF_YEARS
1           0

答案 2 :(得分:0)

考虑到所有的答案和评论,我终于找到了解决这个问题的方法。

我已经实现了一个函数来处理DATETIME2数据类型的基本加/减,然后我在函数中使用它来获取两个日期之间的间隔。

所有计算的最大精确度均为DATETIME2BIGINT,并应处理所有情况。

这是实现基本DATETIME2算术的函数:

DROP FUNCTION FN_DATE2_MATH;
GO

CREATE FUNCTION FN_DATE2_MATH(
    @D1 AS DATETIME2(7),
    @D2 AS DATETIME2(7),
    @OP AS INT=1, -- 1 = SUM, -1 = SUBTRACT
    @OVERFLOW AS INT = NULL -- NULL = NULL VALUE, ELSE OVERFLOW ERROR
)
RETURNS DATETIME2(7)
AS
BEGIN

    IF (@OP = -1) AND (@D1>@D2) BEGIN
        DECLARE @DT DATETIME2(7) = @D1
        SET @D1 = @D2
        SET @D2 = @DT
    END

    DECLARE @B1 VARBINARY(8) = CONVERT(VARBINARY(8), REVERSE(SUBSTRING(CONVERT(VARBINARY(9), @D1),2,8)))
    DECLARE @DD1 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @B1),1,3)
    DECLARE @NS1 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @B1),4,5)

    DECLARE @B2 VARBINARY(8) = CONVERT(VARBINARY(8), REVERSE(SUBSTRING(CONVERT(VARBINARY(9), @D2),2,8)))
    DECLARE @DD2 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @B2),1,3)
    DECLARE @NS2 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @B2),4,5)

    DECLARE @DDR AS BIGINT 
    DECLARE @NSR AS BIGINT 

    IF @OP = 1 BEGIN
        SET @NSR = CONVERT(BIGINT, @NS2) + CONVERT(BIGINT, @NS1)
        IF @NSR>=864000000000 BEGIN
            SET @NSR = @NSR - CONVERT(BIGINT, 864000000000)
            SET @DD1 = CONVERT(VARBINARY(8), CONVERT(BIGINT, @DD1)-1)
        END
        SET @DDR = CONVERT(BIGINT, @DD2) + CONVERT(BIGINT, @DD1) 
    END ELSE 
        IF @OP = -1 BEGIN
            SET @NSR = CONVERT(BIGINT, @NS2) - CONVERT(BIGINT, @NS1)
            IF @NSR<0 BEGIN
                SET @NSR = @NSR + CONVERT(BIGINT, 864000000000)
                SET @DD1 = CONVERT(VARBINARY(8), CONVERT(BIGINT, @DD1)+1)
            END
            SET @DDR = CONVERT(BIGINT, @DD2) - CONVERT(BIGINT, @DD1) 
        END

    -- CHECK OVERFLOW
    IF @DDR NOT BETWEEN 0 AND 3652058 BEGIN

        IF @OVERFLOW IS NULL
            RETURN NULL
        ELSE
            RETURN DATEADD(DD, -1, CONVERT(DATETIME2(7), 0x070000000000000000)) -- GENERATE OVERFLOW            
    END

    DECLARE @BR VARBINARY(8) = CONVERT(VARBINARY(3), @DDR)+CONVERT(VARBINARY(5), @NSR)

    SET @BR = CONVERT(VARBINARY(8), REVERSE(@BR))

    RETURN  CONVERT(DATETIME2(7), 0x07+@BR)

END
GO

这是让时间段过去的功能:

DROP FUNCTION FN_DATE_DIFF2;
GO

CREATE FUNCTION FN_DATE_DIFF2(
    @INTERVALTYPE AS VARCHAR(11),
    @START AS DATETIME2(7),
    @END AS DATETIME2(7)
)
RETURNS BIGINT
AS
BEGIN

    DECLARE @DATEPART INT = CASE
        WHEN @INTERVALTYPE IN ('0','nanosecond','ns')   THEN 0
        WHEN @INTERVALTYPE IN ('1','microsecond','mcs') THEN 1
        WHEN @INTERVALTYPE IN ('2','millisecond','ms')  THEN 2
        WHEN @INTERVALTYPE IN ('3','second','ss','s')   THEN 3
        WHEN @INTERVALTYPE IN ('4','minute','mi','n')   THEN 4
        WHEN @INTERVALTYPE IN ('5','hour','hh')         THEN 5
        WHEN @INTERVALTYPE IN ('6','day','dd','d')      THEN 6
        WHEN @INTERVALTYPE IN ('7','week','wk','ww')    THEN 7
        WHEN @INTERVALTYPE IN ('8','month','mm','m')    THEN 8
        WHEN @INTERVALTYPE IN ('9','quarter','qq','q')  THEN 9
        WHEN @INTERVALTYPE IN ('10','year','yy','yyyy') THEN 10
    ELSE
        6 -- DEFAULT TO DAYS
    END


    DECLARE @BN0 VARBINARY(8) = 0x0000000000000000  -- 0001-01-01 00:00:00.0000000
    DECLARE @DT0 AS DATETIME2(7) = CONVERT(DATETIME2(7), 0x07+@BN0) -- datetime2(7) = 0
    --DECLARE @BNX VARBINARY(8) = 0xFFBF692AC9DAB937  -- 9999-12-31 23:59:59.9999999
    --DECLARE @DTX AS DATETIME2(7) = CONVERT(DATETIME2(7), 0x07+@BNX) -- datetime2(7) = 0

    DECLARE @DT1 AS DATETIME2(7) 
    DECLARE @DT2 AS DATETIME2(7)
    DECLARE @DP AS DATETIME2(7)

    DECLARE @VB1 VARBINARY(8) = CONVERT(VARBINARY(8), REVERSE(SUBSTRING(CONVERT(VARBINARY(9), @START),2,8)))
    DECLARE @DD1 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @VB1),1,3) -- DAYS FROM 0 TO START
    DECLARE @NS1 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @VB1),4,5) -- NS FROM 0 TO START

    DECLARE @VB2 VARBINARY(8) = CONVERT(VARBINARY(8), REVERSE(SUBSTRING(CONVERT(VARBINARY(9), @END),2,8)))
    DECLARE @DD2 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @VB2),1,3) -- DAYS FROM 0 TO END
    DECLARE @NS2 VARBINARY(8) = SUBSTRING(CONVERT(VARBINARY(8), @VB2),4,5) -- NS FROM 0 TO END

    DECLARE @NSR AS BIGINT = CONVERT(BIGINT, @NS2) - CONVERT(BIGINT, @NS1) -- NS RESULT NOT BIASED
    IF @NSR<0 BEGIN
        SET @NSR = @NSR + CONVERT(BIGINT, 864000000000) -- NS RESULT
        SET @DD1 = CONVERT(VARBINARY(8), CONVERT(BIGINT, @DD1)+1) -- ADD CARRY
    END
    DECLARE @DDR AS BIGINT = CONVERT(BIGINT, @DD2) - CONVERT(BIGINT, @DD1) -- DAYS RESULT

    DECLARE @RES BIGINT

    SET @RES = CASE @DATEPART
        WHEN 0 THEN @DDR*CONVERT(BIGINT, 864000000000)+ @NSR -- NS
        WHEN 1 THEN @DDR*CONVERT(BIGINT, 86400000000) + @NSR/CONVERT(BIGINT, 10) -- MCS
        WHEN 2 THEN @DDR*CONVERT(BIGINT, 86400000)    + @NSR/CONVERT(BIGINT, 10000)  -- MS
        WHEN 3 THEN @DDR*CONVERT(BIGINT, 86400)       + @NSR/CONVERT(BIGINT, 10000000) -- SS
        WHEN 4 THEN @DDR*CONVERT(BIGINT, 1440)        + @NSR/CONVERT(BIGINT, 600000000) -- MI
        WHEN 5 THEN @DDR*CONVERT(BIGINT, 24)          + @NSR/CONVERT(BIGINT, 36000000000) -- HH 
        WHEN 6 THEN @DDR -- DD
        WHEN 7 THEN @DDR / 7 -- WK (BOTH INT, RES = INT)
    END

    IF @DATEPART IN (8,9,10) BEGIN

        SET @DT1 = CASE @DATEPART
            WHEN 8  THEN DATEADD(MM, DATEDIFF(MM, @DT0, @START), @DT0)
            WHEN 9  THEN DATEADD(QQ, DATEDIFF(QQ, @DT0, @START), @DT0)
            WHEN 10 THEN DATEADD(YY, DATEDIFF(YY, @DT0, @START), @DT0)
        END 

        SET @DP = DBO.FN_DATE2_MATH(@START, @END, -1, 0) -- ELAPSED TIME (DIFF)
        SET @DT2 = DBO.FN_DATE2_MATH(@DT1, @DP, 1, 0) -- SHIFT DATE (ADD)

        SET @RES = CASE @DATEPART
            WHEN 8  THEN DATEDIFF(MM,  @DT1, @DT2)
            WHEN 9  THEN DATEDIFF(QQ,  @DT1, @DT2)
            WHEN 10 THEN DATEDIFF(YY,  @DT1, @DT2)
        END

    END

    RETURN @RES    
END
GO

您可以这样称呼它:

DECLARE @D1 DATETIME2(7) 
DECLARE @D2 DATETIME2(7) 
DECLARE @DP VARCHAR(20)

SET @D1 = '31/12/2016'
SET @D2 = '01/01/2017'
SET @DP = 'YY'

SELECT @D1 DATE_START, @D2 DATE_END, @DP DATE_PART, DBO.FN_DATE_DIFF2(@DP, @D1, @D2) INTERVALS

SET @D1 = '01:31'
SET @D2 = '03:20'
SET @DP = 'HH'

SELECT @D1 DATE_START, @D2 DATE_END, @DP DATE_PART, DBO.FN_DATE_DIFF2(@DP, @D1, @D2) INTERVALS

SET @D1 = '01/01/0001'
SET @D2 = '31/12/9999 23:59:59.9999999'
SET @DP = 'NS'

SELECT @D1 DATE_START, @D2 DATE_END, @DP DATE_PART, DBO.FN_DATE_DIFF2(@DP, @D1, @D2) INTERVALS

你会得到:

DATE_START                  DATE_END                    DATE_PART   INTERVALS
2016-12-31 00:00:00.0000000 2017-01-01 00:00:00.0000000 YY          0
1900-01-01 01:31:00.0000000 1900-01-01 03:20:00.0000000 HH          1
0001-01-01 00:00:00.0000000 9999-12-31 23:59:59.9999999 NS          3155378975999999999

这应该有效