在SQL 2005中有效地转换UTC和本地(即PST)时间之间的date

将UTCdate时间转换为本地date时间的最佳方法是什么? 它不像getutcdate()和getdate()的差异那么简单,因为差异根据date而变化。

CLR集成也不是我的select。

几个月前,我提出了解决这个问题的解决scheme,就是build立一个夏令时表,在接下来的100年左右的时间里存储夏令时的开始和结束时间,这个解决scheme看起来不够好,但是转换很快(简单表查找)

创build两个表,然后join他们将存储的GMTdate转换为当地时间:

TimeZones eg --------- ---- TimeZoneId 19 Name Eastern (GMT -5) Offset -5 

创build夏令时表并用尽可能多的信息填充它(当地法律总是在变化,所以没有办法预测未来数据的年代)

 DaylightSavings --------------- TimeZoneId 19 BeginDst 3/9/2008 2:00 AM EndDst 11/2/2008 2:00 AM 

像这样join他们:

 inner join TimeZones tz on x.TimeZoneId=tz.TimeZoneId left join DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone and x.TheDateToConvert between ds.BeginDst and ds.EndDst 

像这样转换date:

 dateadd(hh, tz.Offset + case when ds.LocalTimeZone is not null then 1 else 0 end, TheDateToConvert) 

如果您在美国,只希望从UTC / GMT转到固定时区(如EDT),则此代码应该足够了。 我今天鞭打它,并相信这是正确的,但使用风险自负。

假设您的date位于“date”列,将计算列添加到表“myTable”中。 希望别人认为这个有用。

 ALTER TABLE myTable ADD date_edt AS dateadd(hh, -- The schedule through 2006 in the United States was that DST began on the first Sunday in April -- (April 2, 2006), and changed back to standard time on the last Sunday in October (October 29, 2006). -- The time is adjusted at 02:00 local time. CASE WHEN YEAR(date) <= 2006 THEN CASE WHEN date >= '4/' + CAST(abs(8-DATEPART(dw,'4/1/' + CAST(YEAR(date) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00' AND date < '10/' + CAST(32-DATEPART(dw,'10/31/' + CAST(YEAR(date) as varchar)) as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00' THEN -4 ELSE -5 END ELSE -- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007. -- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on -- the first Sunday of November, one week later than in years past. This change resulted in a new DST period -- that is four weeks (five in years when March has five Sundays) longer than in previous years.[35] In 2008 -- daylight saving time ended at 02:00 on Sunday, November 2, and in 2009 it began at 02:00 on Sunday, March 8.[36] CASE WHEN date >= '3/' + CAST(abs(8-DATEPART(dw,'3/1/' + CAST(YEAR(date) as varchar)))%7 + 8 as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00' AND date < '11/' + CAST(abs(8-DATEPART(dw,'11/1/' + CAST(YEAR(date) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date) as varchar) + ' 2:00' THEN -4 ELSE -5 END END ,date) 

考虑夏时制的简单而通用的解决scheme。 给定“YourDateHere”中的UTCdate:

 --Use Minutes ("MI") here instead of hours because sometimes -- the UTC offset may be half an hour (eg 9.5 hours). SELECT DATEADD(MI, DATEDIFF(MI, SYSUTCDATETIME(),SYSDATETIME()), YourUtcDateHere)[LocalDateTime] 

如果这些问题中的任何一个影响到你,你不应该在数据库中存储本地时间:

  1. 有了夏令时,在当地时间无法明确转换的倒退时期,有一个“不确定的时刻”。 如果确切的date和时间是必需的,然后存储在UTC。
  2. 如果您想在自己的时区中显示用户的date和时间,而不是在发生操作的时区,请以UTC格式保存。

在Eric Z Beard的回答中 ,下面的SQL

 inner join TimeZones tz on x.TimeZoneId=tz.TimeZoneId left join DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone and x.TheDateToConvert between ds.BeginDst and ds.EndDst 

可能更准确地说是:

 inner join TimeZones tz on x.TimeZoneId=tz.TimeZoneId left join DaylightSavings ds on tz.TimeZoneId=ds.LocalTimeZone and x.TheDateToConvert >= ds.BeginDst and x.TheDateToConvert < ds.EndDst 

(以上代码未经testing)

这样做的原因是,SQL“之间”的声明是包容性的。 在DST的后端,这会导致2AM时间不被转换为1AM。 当然,2AM的可能性确实很小,但可能发生,并导致无效的转换。

为了只读使用(由鲍勃·奥尔布赖特的不正确的解决scheme启发):

 SELECT date1, dateadd(hh, -- The schedule through 2006 in the United States was that DST began on the first Sunday in April -- (April 2, 2006), and changed back to standard time on the last Sunday in October (October 29, 2006). -- The time is adjusted at 02:00 local time (which, for edt, is 07:00 UTC at the start, and 06:00 GMT at the end). CASE WHEN YEAR(date1) <= 2006 THEN CASE WHEN date1 >= '4/' + CAST((8-DATEPART(dw,'4/1/' + CAST(YEAR(date1) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 7:00' AND date1 < '10/' + CAST(32-DATEPART(dw,'10/31/' + CAST(YEAR(date1) as varchar)) as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 6:00' THEN -4 ELSE -5 END ELSE -- By the Energy Policy Act of 2005, daylight saving time (DST) was extended in the United States in 2007. -- DST starts on the second Sunday of March, which is three weeks earlier than in the past, and it ends on -- the first Sunday of November, one week later than in years past. This change resulted in a new DST period -- that is four weeks (five in years when March has five Sundays) longer than in previous years. In 2008 -- daylight saving time ended at 02:00 edt (06:00 UTC) on Sunday, November 2, and in 2009 it began at 02:00 edt (07:00 UTC) on Sunday, March 8 CASE WHEN date1 >= '3/' + CAST((8-DATEPART(dw,'3/1/' + CAST(YEAR(date1) as varchar)))%7 + 8 as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 7:00' AND date1 < '11/' + CAST((8-DATEPART(dw,'11/1/' + CAST(YEAR(date1) as varchar)))%7 + 1 as varchar) + '/' + CAST(YEAR(date1) as varchar) + ' 6:00' THEN -4 ELSE -5 END END , date1) as date1Edt from MyTbl 

我试图编辑Bob Albright的错误答案后发布了这个答案 。 我纠正了时间,删除了多余的abs(),但我的编辑被拒绝多次。 我尝试解释,但作为noob被解雇。 他是一个伟大的方法来解决这个问题! 这让我开始了正确的方向。 我讨厌创造这个单独的答案时,他只是需要一个小的调整,但我试过(\)(¯)_ /¯

维护一个TimeZone表,或者用一个扩展的存储过程(xp_cmdshell或一个COM组件,或者你自己的)提取出来,然后让操作系统去做。 如果你走的XP路线,你可能会想caching一天的偏移量。

我喜欢@Eric Z Beard提供的答案。

但是,为了避免每次都进行连接,这个怎么样?

 TimeZoneOffsets --------------- TimeZoneId 19 Begin 1/4/2008 2:00 AM End 1/9/2008 2:00 AM Offset -5 TimeZoneId 19 Begin 1/9/2008 2:00 AM End 1/4/2009 2:00 AM Offset -6 TimeZoneId 20 --Hong Kong for example - no DST Begin 1/1/1900 End 31/12/9999 Offset +8 

然后

  Declare @offset INT = (Select IsNull(tz.Offset,0) from YourTable ds join TimeZoneOffsets tz on tz.TimeZoneId=ds.LocalTimeZoneId and x.TheDateToConvert >= ds.Begin and x.TheDateToConvert < ds.End) 

终于成为

  dateadd(hh, @offset, TheDateToConvert) 

我已经阅读了很多关于这个问题的StackOverflowpost,并发现了很多方法。 一些“类”的确定。 我也发现这个MS参考( https://msdn.microsoft.com/en-us/library/mt612795.aspx ),我试图在我的脚本中使用。 我已经设法达到所需的结果,但我不知道这是否会运行在2005年的版本。 无论哪种方式,我希望这有助于。

Fnc从系统UTC默认返回PST

 CREATE FUNCTION dbo.GetPst() RETURNS DATETIME AS BEGIN RETURN SYSDATETIMEOFFSET() AT TIME ZONE 'Pacific Standard Time' END SELECT dbo.GetPst() 

Fnc从提供的时间戳返回PST

 CREATE FUNCTION dbo.ConvertUtcToPst(@utcTime DATETIME) RETURNS DATETIME AS BEGIN RETURN DATEADD(HOUR, 0 - DATEDIFF(HOUR, CAST(SYSDATETIMEOFFSET() AT TIME ZONE 'Pacific Standard Time' AS DATETIME), SYSDATETIME()), @utcTime) END SELECT dbo.ConvertUtcToPst('2016-04-25 22:50:01.900') 

我正在使用这个,因为我所有的date都是从现在开始的。

 DATEADD(HH,(DATEPART(HOUR, GETUTCDATE())-DATEPART(HOUR, GETDATE()))*-1, GETDATE()) 

对于历史date(或处理未来DST的变化,我猜鲍勃·奥尔布赖特的解决scheme将是一条路。

我对我的代码所做的修改是使用目标列:

 DATEADD(HH,(DATEPART(HOUR, GETUTCDATE())-DATEPART(HOUR, GETDATE()))*-1, [MySourceColumn]) 

到目前为止,这似乎工作,但我很高兴收到反馈。

这是我用来制作我的时区表的代码。 这有点幼稚,但通常是够好的。

假设:

  1. 它假设美国唯一的规则(DST是在某些预先定义的星期天上午2点等)。
  2. 假定你在1970年之前没有date
  3. 它假定你知道当地的时区偏移(即:EST = -05:00,EDT = -04:00等)

这里是SQL:

 -- make a table (#dst) of years 1970-2101. Note that DST could change in the future and -- everything was all custom and jacked before 1970 in the US. declare @first_year varchar(4) = '1970' declare @last_year varchar(4) = '2101' -- make a table of all the years desired if object_id('tempdb..#years') is not null drop table #years ;with cte as ( select cast(@first_year as int) as int_year ,@first_year as str_year ,cast(@first_year + '-01-01' as datetime) as start_of_year union all select int_year + 1 ,cast(int_year + 1 as varchar(4)) ,dateadd(year, 1, start_of_year) from cte where int_year + 1 <= @last_year ) select * into #years from cte option (maxrecursion 500); -- make a staging table of all the important DST dates each year if object_id('tempdb..#dst_stage') is not null drop table #dst_stage select dst_date ,time_period ,int_year ,row_number() over (order by dst_date) as ordinal into #dst_stage from ( -- start of year select y.start_of_year as dst_date ,'start of year' as time_period ,int_year from #years y union all select dateadd(year, 1, y.start_of_year) ,'start of year' as time_period ,int_year from #years y where y.str_year = @last_year -- start of dst union all select case when y.int_year >= 2007 then -- second sunday in march dateadd(day, ((7 - datepart(weekday, y.str_year + '-03-08')) + 1) % 7, y.str_year + '-03-08') when y.int_year between 1987 and 2006 then -- first sunday in april dateadd(day, ((7 - datepart(weekday, y.str_year + '-04-01')) + 1) % 7, y.str_year + '-04-01') when y.int_year = 1974 then -- special case cast('1974-01-06' as datetime) when y.int_year = 1975 then -- special case cast('1975-02-23' as datetime) else -- last sunday in april dateadd(day, ((7 - datepart(weekday, y.str_year + '-04-24')) + 1) % 7, y.str_year + '-04-24') end ,'start of dst' as time_period ,int_year from #years y -- end of dst union all select case when y.int_year >= 2007 then -- first sunday in november dateadd(day, ((7 - datepart(weekday, y.str_year + '-11-01')) + 1) % 7, y.str_year + '-11-01') else -- last sunday in october dateadd(day, ((7 - datepart(weekday, y.str_year + '-10-25')) + 1) % 7, y.str_year + '-10-25') end ,'end of dst' as time_period ,int_year from #years y ) y order by 1 -- assemble a final table if object_id('tempdb..#dst') is not null drop table #dst select a.dst_date + case when a.time_period = 'start of dst' then ' 03:00' when a.time_period = 'end of dst' then ' 02:00' else ' 00:00' end as start_date ,b.dst_date + case when b.time_period = 'start of dst' then ' 02:00' when b.time_period = 'end of dst' then ' 01:00' else ' 00:00' end as end_date ,cast(case when a.time_period = 'start of dst' then 1 else 0 end as bit) as is_dst ,cast(0 as bit) as is_ambiguous ,cast(0 as bit) as is_invalid into #dst from #dst_stage a join #dst_stage b on a.ordinal + 1 = b.ordinal union all select a.dst_date + ' 02:00' as start_date ,a.dst_date + ' 03:00' as end_date ,cast(1 as bit) as is_dst ,cast(0 as bit) as is_ambiguous ,cast(1 as bit) as is_invalid from #dst_stage a where a.time_period = 'start of dst' union all select a.dst_date + ' 01:00' as start_date ,a.dst_date + ' 02:00' as end_date ,cast(0 as bit) as is_dst ,cast(1 as bit) as is_ambiguous ,cast(0 as bit) as is_invalid from #dst_stage a where a.time_period = 'end of dst' order by 1 ------------------------------------------------------------------------------- -- Test Eastern select the_date as eastern_local ,todatetimeoffset(the_date, case when b.is_dst = 1 then '-04:00' else '-05:00' end) as eastern_local_tz ,switchoffset(todatetimeoffset(the_date, case when b.is_dst = 1 then '-04:00' else '-05:00' end), '+00:00') as utc_tz --,b.* from ( select cast('2015-03-08' as datetime) as the_date union all select cast('2015-03-08 02:30' as datetime) as the_date union all select cast('2015-03-08 13:00' as datetime) as the_date union all select cast('2015-11-01 01:30' as datetime) as the_date union all select cast('2015-11-01 03:00' as datetime) as the_date ) a left join #dst b on b.start_date <= a.the_date and a.the_date < b.end_date