31

这是我遇到的问题:我有一个大型查询,需要比较 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)导致我浪费了很多精力。

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

4

10 回答 10

73

这更简洁:

where 
  datediff(day, date1, date2) = 0
于 2008-08-22T15:02:46.300 回答
21

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

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

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

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

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

于 2008-08-22T15:16:00.980 回答
6
where
year(date1) = year(date2)
and month(date1) = month(date2)
and day(date1) = day(date2)
于 2008-08-22T14:56:16.363 回答
4

确保只阅读在数据库中可以通过更改几行代码获得 1000% + 改进,以便确保优化器在弄乱日期时可以有效地利用索引

于 2008-08-22T15:21:21.107 回答
2

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

select dateadd(d, datediff(d, 0, current_timestamp), 0)
于 2008-08-22T15:06:59.410 回答
2

埃里克 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

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

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

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

于 2008-08-22T16:33:13.243 回答
1

您在这里的选项方面被宠坏了。如果您使用的是 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)
于 2008-08-22T15:22:51.087 回答
1

埃里克 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 上有一点开销。

于 2008-08-23T02:14:08.480 回答
0

我会使用 datepart 的 dayofyear 函数:


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

请参阅此处的日期部分参考。

于 2008-08-22T15:27:02.123 回答
0

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

于 2008-08-22T15:31:35.637 回答