在 TSQL 中检查两个日期时间是否在同一日历日的好方法是什么?
-
09-06-2019 - |
题
这是我遇到的问题:我有一个大型查询,需要比较 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)导致我浪费了很多精力。
但查询计划没有改变。我想我需要重新审视整个事情。
解决方案
这更加简洁:
where
datediff(day, date1, date2) = 0
其他提示
您几乎必须保持 where 子句的左侧干净。所以,通常情况下,你会这样做:
WHERE MyDateTime >= @activityDateMidnight
AND MyDateTime < (@activityDateMidnight + 1)
(有些人更喜欢 DATEADD(d, 1, @activityDateMidnight) - 但它是同一件事)。
不过,时区表使事情变得有点复杂。从您的代码片段来看,有点不清楚,但看起来 t.TheDateInTable 是 GMT 格式,带有时区标识符,然后您添加偏移量以与 @activityDateMidnight 进行比较 - 这是当地时间。不过,我不确定 ds.LocalTimeZone 是什么。
如果是这种情况,那么您需要将 @activityDateMidnight 改为 GMT。
where
year(date1) = year(date2)
and month(date1) = month(date2)
and day(date1) = day(date2)
请务必阅读 只有在数据库中才能通过更改几行代码获得 1000%+ 的改进 这样您就可以确定优化器在处理日期时可以有效地利用索引
埃里克·Z·比尔德:
我确实将所有日期存储为 GMT。这是用例:美国东部时间 1 日晚上 11:00 发生了一些事情,这是格林尼治标准时间的第二个。我想查看 1 日的活动,而且我现在是东部时间,所以我想查看晚上 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 事件时间存储。您甚至不需要事件表中的时区(至少出于此目的)。
由于 EventTime 不在函数中,因此这是直接索引扫描 - 这应该非常有效。将 EventTime 作为您的聚集索引,它就会飞起来。;)
就我个人而言,我会让应用程序在运行查询之前将搜索时间转换为 GMT。
这将为您删除日期中的时间部分:
select dateadd(d, datediff(d, 0, current_timestamp), 0)
这里的选择让您眼花缭乱。如果您使用的是 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)
埃里克·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 上有一点开销。
我会使用 datepart 的 dayofyear 函数:
Select *
from mytable
where datepart(dy,date1) = datepart(dy,date2)
and
year(date1) = year(date2) --assuming you want the same year too
请参阅日期部分参考 这里.
关于时区,还有一个原因是将所有日期存储在单个时区(最好是 UTC)中。无论如何,我认为使用 datediff、datepart 和不同的内置日期函数的答案是最好的选择。