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

时间:2008-08-24 02:08:16

标签: sql-server

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

CLR集成对我来说也不是一种选择。

几个月前我提出的针对这个问题的解决方案是有一个夏令时表,存储了接下来100年左右的开始和结束夏令时,这个解决方案似乎不太优雅,但是转换是快速(简单的表查找)

12 个答案:

答案 0 :(得分:27)

创建两个表,然后加入它们以将存储的GMT日期转换为本地时间:

TimeZones     e.g.
---------     ----
TimeZoneId    19
Name          Eastern (GMT -5)
Offset        -5

创建夏令时表并使用尽可能多的信息填充它(当地法律一直在变化,因此无法预测未来数年的数据)

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

像这样加入他们:

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

转换日期如下:

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

答案 1 :(得分:15)

如果您在美国并且只对从UTC / GMT到固定时区(例如EDT)感兴趣,则此代码就足够了。我今天掀起了它,并认为这是正确的,但使用风险自负。

假设您的日期位于“日期”列中,则将计算列添加到表'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)

答案 2 :(得分:7)

FOR READ-ONLY 使用此功能(受Bob Albright's incorrect solution启发):

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's wrong answer后发布了此答案。我纠正了时间并删除了多余的abs(),但我的编辑被多次拒绝了。我尝试解释,但被解雇为一个菜鸟。他是解决这个问题的绝佳方法!它让我开始朝着正确的方向前进。当他只是需要一个小调整时,我讨厌创建这个单独的答案,但我试过了¯\ _(ツ)_ /¯

答案 3 :(得分:7)

一种考虑夏令时的简单通用解决方案。给定“YourDateHere”中的UTC日期:

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

答案 4 :(得分:5)

如果这些问题中的任何一个对您有影响,则不应将本地时间存储在数据库中:

  1. DST表示在回落期间存在“不确定时数”,当地时间无法明确转换。如果确切日期&amp;需要时间,然后以UTC格式存储。
  2. 如果您想向用户显示日期&amp;时间在他们自己的时区,而不是行动发生的时区,以UTC格式存储。

答案 5 :(得分:5)

Eric Z Beard's answer中,以下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 

(以上代码未经测试)

原因是sql“between”语句是包含的。在DST的后端,这将导致2AM时间未转换为1AM。当然,时间2AM的可能性很小,但可能会发生,并且会导致转换无效。

答案 6 :(得分:0)

维护一个TimeZone表,或者使用扩展存储过程(xp_cmdshell或COM组件,或您自己的)来shell,并要求操作系统执行此操作。如果你去xp路线,你可能想要缓存一天的偏移量。

答案 7 :(得分:0)

我喜欢提供的答案@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)

答案 8 :(得分:0)

我已经阅读了很多关于这个问题的StackOverflow帖子,并发现了很多方法。一些“有点”可以。我还发现了我试图在我的脚本中使用的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')

答案 9 :(得分:0)

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

button.keyEquivalent = String(Character(UnicodeScalar(NSLeftArrowFunctionKey)!))

对于历史日期(或处理DST的未来变化,我猜测Bob Albright的解决方案将是最佳选择。

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

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

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

答案 10 :(得分:0)

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

假设:

  1. 它假定美国唯一的规则(DST在某个预定义的星期日凌晨2点, 等)。
  2. 假设您没有1970年之前的日期
  3. 假设您知道当地时区偏移(即:EST = -05:00,EDT = -04:00等)
  4. 这是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
    

答案 11 :(得分:0)

--Adapted Bob Albright and WillDeStijl suggestions for SQL server 2014
--
--In this instance I had no dates prior to 2006, therefore I simplified the case example
--I had to add the variables for the assignment to allow trimming the timestamp from my resultset 

DECLARE @MARCH_DST as DATETIME
SET @MARCH_DST='3/' + CAST((8-DATEPART(dw,'3/1/' + CAST(YEAR(getdate()) as varchar)))%7 + 8 as varchar) +  '/' + CAST(YEAR(getdate()) as varchar) + ' 7:00'

DECLARE @NOV_DST as DATETIME
SET @NOV_DST='11/' + CAST((8-DATEPART(dw,'11/1/' + CAST(YEAR(getdate()) as varchar)))%7 + 1 as varchar) +  '/' + CAST(YEAR(getdate()) as varchar) + ' 6:00'

select cast(dateadd(HOUR,
-- 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 >=@MARCH_DST
            AND
                date1< @NOV_DST
       THEN -4 ELSE -5 END
       , date1) as DATE) as date1_edited