【问题标题】:What's a good way to check if two datetimes are on the same calendar day in TSQL?什么是检查 TSQL 中两个日期时间是否在同一日历日的好方法?
【发布时间】:2010-09-06 13:25:15
【问题描述】:

这是我遇到的问题:我有一个大型查询,需要比较 where 子句中的日期时间,以查看两个日期是否在同一天。我目前的解决方案很糟糕,是将日期时间发送到 UDF 以将它们转换为同一天的午夜,然后检查这些日期是否相等。当涉及到查询计划时,这是一场灾难,几乎所有连接或 where 子句中的 UDF 也是如此。这是我的应用程序中仅有的几个我无法根除函数并为查询优化器提供一些它实际上可以用来定位最佳索引的地方之一。

在这种情况下,将函数代码合并回查询中似乎不切实际。

我想我在这里遗漏了一些简单的东西。

这里的函数供参考。

if not exists (select * from dbo.sysobjects 
              where id = object_id(N'dbo.f_MakeDate') and               
              type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
  exec('create function dbo.f_MakeDate() returns int as 
         begin declare @retval int return @retval end')
go

alter function dbo.f_MakeDate
(
    @Day datetime, 
    @Hour int, 
    @Minute int
)
returns datetime
as

/*

Creates a datetime using the year-month-day portion of @Day, and the 
@Hour and @Minute provided

*/

begin

declare @retval datetime
set @retval = cast(
    cast(datepart(m, @Day) as varchar(2)) + 
    '/' + 
    cast(datepart(d, @Day) as varchar(2)) + 
    '/' + 
    cast(datepart(yyyy, @Day) as varchar(4)) + 
    ' ' + 
    cast(@Hour as varchar(2)) + 
    ':' + 
    cast(@Minute as varchar(2)) as datetime)
return @retval
end

go

为了使事情复杂化,我加入时区表以检查日期与当地时间,每行可能不同:

where 
dbo.f_MakeDate(dateadd(hh, tz.Offset + 
    case when ds.LocalTimeZone is not null 
    then 1 else 0 end, t.TheDateINeedToCheck), 0, 0) = @activityDateMidnight

[编辑]

我正在采纳@Todd 的建议:

where datediff(day, dateadd(hh, tz.Offset + 
    case when ds.LocalTimeZone is not null 
    then 1 else 0 end, t.TheDateINeedToCheck), @ActivityDate) = 0

我对 datediff 工作原理的误解(连续年份的同一天产生 366,而不是我预期的 0)导致我浪费了很多精力。

但是查询计划没有改变。我想我需要把整个事情重新回到绘图板上。

【问题讨论】:

  • 请参阅 Mark Brackett 的答案,这是正确的答案。我知道您的问题已有 2 年历史了,但请不要让访问此问题的人走错路。托德·廷根(Todd Tingen)的答案有效,但性能很糟糕,正如您当时发现的那样!
  • Mark 对于这种情况的回答是正确的,因为优化器依赖于它。以我的方式使用 dateadd 是一种简单、简洁的方法,可以查看两个日期是否在原来的问题所在的同一日历日。

标签: sql sql-server tsql datetime user-defined-functions


【解决方案1】:
where
year(date1) = year(date2)
and month(date1) = month(date2)
and day(date1) = day(date2)

【讨论】:

    【解决方案2】:

    这更简洁:

    where 
      datediff(day, date1, date2) = 0
    

    【讨论】:

    • 为什么这有 9 票赞成并获得了答案?它很简洁,好吧,并导致 OP 走错了路。由于第二个日期@ActivityDate 是固定的,我们可以将数学移到右侧并获得更好的性能。
    • 同意@emtucifor。这个逻辑不能被sql server sargable。
    • @ErikE,我不太明白你的意思,你有代码 sn-p 吗?
    • @Joyce,看看Mark Brackett's answer。他将MyDateTime 放在任何数学运算符的一侧,单独。这意味着查询引擎可以执行 范围搜索 并直接在 b 树中导航到两次之间的行。有了这个可怕的、可怕的、可怕的建议,表中的每一行都必须对其进行数学运算,这是一个扫描,对性能来说很糟糕。如果表有 1 亿行,并且只有 10 行符合条件,想想看全部找到 10 行是多么糟糕!!!!!!
    【解决方案3】:

    这将为您从日期中删除时间部分:

    select dateadd(d, datediff(d, 0, current_timestamp), 0)
    

    【讨论】:

      【解决方案4】:

      您几乎必须保持 where 子句的左侧干净。因此,通常情况下,您会执行以下操作:

      WHERE MyDateTime >= @activityDateMidnight 
            AND MyDateTime < (@activityDateMidnight + 1)
      

      (有些人更喜欢 DATEADD(d, 1, @activityDateMidnight) - 但它是一回事)。

      不过,TimeZone 表使事情变得有些复杂。您的 sn-p 有点不清楚,但看起来 t.TheDateInTable 在 GMT 中,带有时区标识符,然后您添加偏移量以与 @activityDateMidnight 进行比较 - 这是当地时间。不过,我不确定 ds.LocalTimeZone 是什么。

      如果是这种情况,那么您需要将 @activityDateMidnight 改为 GMT。

      【讨论】:

        【解决方案5】:

        请务必阅读Only In A Database Can You Get 1000% + Improvement By Changing A Few Lines Of Code,以确保优化器在处理日期时可以有效利用索引

        【讨论】:

          【解决方案6】:

          您在此处的选项方面被宠坏了。如果您使用的是 Sybase 或 SQL Server 2008,您可以创建日期类型的变量并为它们分配您的日期时间值。数据库引擎为您摆脱了时间。这是一个快速而肮脏的测试来说明(代码是 Sybase 方言):

          declare @date1 date
          declare @date2 date
          set @date1='2008-1-1 10:00'
          set @date2='2008-1-1 22:00'
          if @date1=@date2
              print 'Equal'
          else
              print 'Not equal'
          

          对于 SQL 2005 及更早版本,您可以将日期转换为不包含时间组件的格式的 varchar。例如以下返回 2008.08.22

          select convert(varchar,'2008-08-22 18:11:14.133',102)
          

          102部分指定格式(在线书籍可以为您列出所有可用格式)

          因此,您可以做的是编写一个函数,该函数采用日期时间并提取日期元素并丢弃时间。像这样:

          create function MakeDate (@InputDate datetime) returns datetime as
          begin
              return cast(convert(varchar,@InputDate,102) as datetime);
          end
          

          然后您可以使用同伴功能

          Select * from Orders where dbo.MakeDate(OrderDate) = dbo.MakeDate(DeliveryDate)
          

          【讨论】:

            【解决方案7】:

            我会使用 datepart 的 dayofyear 函数:

            
            Select *
            from mytable
            where datepart(dy,date1) = datepart(dy,date2)
            and
            year(date1) = year(date2) --assuming you want the same year too
            

            查看日期部分参考here

            【讨论】:

              【解决方案8】:

              关于时区,还有一个理由将所有日期存储在一个时区(最好是 UTC)中。无论如何,我认为使用 datediff、datepart 和不同的内置日期函数的答案是您最好的选择。

              【讨论】:

                【解决方案9】:

                埃里克·Z 胡须:

                我确实将所有日期存储在 GMT 中。这是用例:美国东部标准时间 1 日晚上 11:00 发生了一些事情,这是格林威治标准时间的第二个。我想看第一天的活动,我在美国东部时间,所以我想看晚上 11 点的活动。如果我只是比较原始 GMT 日期时间,我会错过一些东西。报告中的每一行都可以代表不同时区的活动。

                没错,但是当您说您对 2008 年 1 月 1 日美国东部标准时间的活动感兴趣时:

                SELECT @activityDateMidnight = '1/1/2008', @activityDateTZ = 'EST'
                

                您只需将 转换为 GMT(我忽略了查询 EST 到 EDT 前一天的复杂性,反之亦然):

                Table: TimeZone
                Fields: TimeZone, Offset
                Values: EST, -4
                
                --Multiply by -1, since we're converting EST to GMT.
                --Offsets are to go from GMT to EST.
                SELECT @activityGmtBegin = DATEADD(hh, Offset * -1, @activityDateMidnight)
                FROM TimeZone
                WHERE TimeZone = @activityDateTZ
                

                这应该给你 '1/1/2008 4:00 AM'。然后,您可以在 GMT 中搜索:

                SELECT * FROM EventTable
                WHERE 
                   EventTime >= @activityGmtBegin --1/1/2008 4:00 AM
                   AND EventTime < (@activityGmtBegin + 1) --1/2/2008 4:00 AM
                

                相关事件的 GMT 事件时间为 2008 年 1 月 2 日凌晨 3:00。您甚至不需要 EventTable 中的 TimeZone(至少为此目的)。

                由于 EventTime 不在函数中,因此这是一个直接索引扫描 - 这应该非常有效。让 EventTime 成为你的聚集索引,它就会飞起来。 ;)

                就个人而言,我会让应用在运行查询之前将搜索时间转换为 GMT。

                【讨论】:

                • 如果改用 BETWEEN 语句,会不会产生负面影响?
                • @FelipeRodriguez - 不是真的;请注意,BETWEEN 包含两端,因此可能存在重叠范围。
                【解决方案10】:

                埃里克·Z 胡须:

                活动日期表示当地时区,而不是特定时区

                好的 - 回到绘图板。试试这个:

                where t.TheDateINeedToCheck BETWEEN (
                    dateadd(hh, (tz.Offset + ISNULL(ds.LocalTimeZone, 0)) * -1, @ActivityDate)
                    AND
                    dateadd(hh, (tz.Offset + ISNULL(ds.LocalTimeZone, 0)) * -1, (@ActivityDate + 1))
                )
                

                这会将@ActivityDate 转换为当地时间,并与之进行比较。这是您使用索引的最佳机会,尽管我不确定它是否会起作用 - 您应该尝试一下并检查查询计划。

                下一个选项是索引视图,具有索引的、计算的 TimeINeedToCheck在本地时间。然后你就回到:

                where v.TheLocalDateINeedToCheck BETWEEN @ActivityDate AND (@ActivityDate + 1)
                

                这肯定会使用索引 - 尽管那时您在 INSERT 和 UPDATE 上有一点开销。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2023-03-07
                  • 1970-01-01
                  • 2013-04-11
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多