如何获得夏令时之前或之后的某个日期的UTC与当地时间之间的正确偏移量?


29

我目前使用以下方法从UTC日期时间获取本地日期时间:

SET @offset = DateDiff(minute, GetUTCDate(), GetDate())
SET @localDateTime = DateAdd(minute, @offset, @utcDateTime)

我的问题是,如果夏令时发生在GetUTCDate()和之间@utcDateTime,那么@localDateTime最终会减少一个小时。

是否有简单的方法可以将utc转换为非当前日期的本地时间?

我正在使用SQL Server 2005

Answers:


18

将非当前UTC日期转换为本地时间的最佳方法是使用CLR。代码本身很简单;困难的部分通常是说服人们CLR并不是纯粹的邪恶或可怕的事物...

有关众多示例之一,请查看Harsh Chawla关于该主题的博客文章

不幸的是,除了基于CLR的解决方案外,没有任何内置函数可以处理这种类型的转换。您可以编写一个执行以下操作的T-SQL函数,但是您必须自己实现日期更改逻辑,而我断定这绝对不容易。


考虑到随着时间的推移区域变化的实际复杂性,说在纯T-SQL中尝试这样做“绝对不容易”可能会低估它。因此,是的,SQLCLR是执行此操作的唯一可靠且有效的方法。+1。仅供参考:链接的博客帖子在功能上是正确的,但未遵循最佳实践,因此很遗憾效率低下。对于UTC和服务器本地时间之间的转换功能是可用的SQL#库(其中我的作者),但不是免费版本。
所罗门·鲁兹基2015年

1
必须添加CLR时,它会变得邪恶WITH PERMISSION_SET = UNSAFE。某些环境不允许这样做,例如AWS RDS。而且,这是不安全的。不幸的是,没有.Net时区的完整实现,未经unsafe允许就无法使用。看到这里这里
弗雷德里克

15

我已经开发并出版了 在codeplex上 T-SQL Toolbox项目,以帮助在Microsoft SQL Server中处理日期时间和时区问题的任何人。它是开源的,完全免费使用。

除了开箱即用的预填充配置表外,它还提供了使用普通T-SQL(无CLR)的简单日期时间转换UDF。并且具有完整的DST(夏令时)支持。

可在表“ DateTimeUtil.Timezone”(在T-SQL Toolbox数据库中提供)中找到所有受支持的时区的列表。

在您的示例中,您可以使用以下示例:

SELECT [DateTimeUtil].[UDF_ConvertUtcToLocalByTimezoneIdentifier] (
    'W. Europe Standard Time', -- the target local timezone
    '2014-03-30 00:55:00' -- the original UTC datetime you want to convert
)

这将返回转换后的本地日期时间值。

不幸的是,仅由于更新的数据类型(DATE,TIME,DATETIME2)才支持SQL Server 2008或更高版本。但是,由于提供了完整的源代码,您可以通过将表和UDF替换为DATETIME来轻松地调整它们。我没有可用于测试的MSSQL 2005,但是它也应该与MSSQL 2005一起工作。如有疑问,请告诉我。


12

我总是使用此TSQL命令。

-- the utc value 
declare @utc datetime = '20/11/2014 05:14'

-- the local time

select DATEADD(hh, DATEDIFF(hh, getutcdate(), getdate()), @utc)

这非常简单,可以完成工作。


2
有些时区与UTC的时差不是整小时,因此使用此DATEPART可能会给您带来麻烦。
Michael Green

4
关于Michael Green的评论,您可以通过将其更改为SELECT DATEADD(MINUTE,DATEDIFF(MINUTE,GETUTCDATE(),GETDATE()),@utc)来解决该问题。
注册用户

4
这不起作用,因为您仅确定当前时间是否是DST,然后比较可能是DST的时间。使用上面的示例代码和日期时间(目前在英国),我现在知道应该是上午6:14,但是11月在夏令时之外,所以应该是格林尼治标准时间和UTC一致的上午5:14。
马特

虽然我不同意这并不能解决实际问题,但就此答案而言,我认为以下更好:SELECT DATEADD(MINUTE,DATEPART(TZoffset,SYSDATETIMEOFFSET())@utc)
Eamon

@Ludo Bernaerts报告:首次使用毫秒,第二:这不起作用,因为UTC偏移今天可能比UTC偏移在一定的时间不同(夏时制-夏VS冬季时间)......
困惑

11

我找到了这个答案在StackOverflow上,提供了一个用户定义的函数,该函数似乎可以准确转换日期时间

您唯一需要修改的是 @offset顶部变量,将其设置为运行此功能的SQL Server的时区偏移量。就我而言,我们的SQL Server使用的是EST,即GMT-5

它不是完美的,可能在许多情况下都无法使用,例如具有半小时或15分钟的TZ偏移量(对于那些我建议像Kevin推荐的CLR函数那样的时间),但是对于北方的大多数时区来说,它已经足够好了美国。

CREATE FUNCTION [dbo].[UDTToLocalTime](@UDT AS DATETIME)  
RETURNS DATETIME
AS
BEGIN 
--====================================================
--Set the Timezone Offset (NOT During DST [Daylight Saving Time])
--====================================================
DECLARE @Offset AS SMALLINT
SET @Offset = -5

--====================================================
--Figure out the Offset Datetime
--====================================================
DECLARE @LocalDate AS DATETIME
SET @LocalDate = DATEADD(hh, @Offset, @UDT)

--====================================================
--Figure out the DST Offset for the UDT Datetime
--====================================================
DECLARE @DaylightSavingOffset AS SMALLINT
DECLARE @Year as SMALLINT
DECLARE @DSTStartDate AS DATETIME
DECLARE @DSTEndDate AS DATETIME
--Get Year
SET @Year = YEAR(@LocalDate)

--Get First Possible DST StartDay
IF (@Year > 2006) SET @DSTStartDate = CAST(@Year AS CHAR(4)) + '-03-08 02:00:00'
ELSE              SET @DSTStartDate = CAST(@Year AS CHAR(4)) + '-04-01 02:00:00'
--Get DST StartDate 
WHILE (DATENAME(dw, @DSTStartDate) <> 'sunday') SET @DSTStartDate = DATEADD(day, 1,@DSTStartDate)


--Get First Possible DST EndDate
IF (@Year > 2006) SET @DSTEndDate = CAST(@Year AS CHAR(4)) + '-11-01 02:00:00'
ELSE              SET @DSTEndDate = CAST(@Year AS CHAR(4)) + '-10-25 02:00:00'
--Get DST EndDate 
WHILE (DATENAME(dw, @DSTEndDate) <> 'sunday') SET @DSTEndDate = DATEADD(day,1,@DSTEndDate)

--Get DaylightSavingOffset
SET @DaylightSavingOffset = CASE WHEN @LocalDate BETWEEN @DSTStartDate AND @DSTEndDate THEN 1 ELSE 0 END

--====================================================
--Finally add the DST Offset 
--====================================================
RETURN DATEADD(hh, @DaylightSavingOffset, @LocalDate)
END



GO


3

对于堆栈溢出中的类似问题,有两个很好的答案。我从鲍勃·奥尔布赖特(Bob Albright)第二个回答中开始使用T-SQL方法来清理由数据转换顾问引起的混乱。

它适用于几乎所有数据,但后来我意识到他的算法仅适用于1987年4月5日之前的日期,而且我们有一些1940年代的日期仍无法正确转换。最终,我们需要UTCSQL Server数据库中的日期与第3方程序中使用Java API从UTC时间为本地时间。

我喜欢的CLR例子在凯文Feasel的回答上面使用刺激性乔拉的例子,而且我也喜欢比较它使用Java,因为我们的前端使用Java做的解决方案UTC,以本地时间转换。

Wikipedia提到了8种不同的宪法修正案,其中涉及1987年之前的时区调整,其中许多修正案非常局限在不同的州,因此CLR和Java可能会以不同的方式解释它们。您的前端应用程序代码是使用dotnet还是Java,还是1987年之前的日期对您来说是一个问题?


2

您可以使用CLR存储过程轻松地做到这一点。

[SqlFunction]
public static SqlDateTime ToLocalTime(SqlDateTime UtcTime, SqlString TimeZoneId)
{
    if (UtcTime.IsNull)
        return UtcTime;

    var timeZone = TimeZoneInfo.FindSystemTimeZoneById(TimeZoneId.Value);
    var localTime = TimeZoneInfo.ConvertTimeFromUtc(UtcTime.Value, timeZone);
    return new SqlDateTime(localTime);
}

您可以将可用的时区存储在表中:

CREATE TABLE TimeZones
(
    TimeZoneId NVARCHAR(32) NOT NULL CONSTRAINT PK_TimeZones PRIMARY KEY,
    DisplayName NVARCHAR(64) NOT NULL,
    SupportsDaylightSavingTime BIT NOT NULL,
)

并且此存储过程将用服务器上可能的时区填充表格。

public partial class StoredProcedures
{
    [SqlProcedure]
    public static void PopulateTimezones()
    {
        using (var sql = new SqlConnection("Context Connection=True"))
        {
            sql.Open();

            using (var cmd = sql.CreateCommand())
            {
                cmd.CommandText = "DELETE FROM TimeZones";
                cmd.ExecuteNonQuery();

                cmd.CommandText = "INSERT INTO [dbo].[TimeZones]([TimeZoneId], [DisplayName], [SupportsDaylightSavingTime]) VALUES(@TimeZoneId, @DisplayName, @SupportsDaylightSavingTime);";
                var Id = cmd.Parameters.Add("@TimeZoneId", SqlDbType.NVarChar);
                var DisplayName = cmd.Parameters.Add("@DisplayName", SqlDbType.NVarChar);
                var SupportsDaylightSavingTime = cmd.Parameters.Add("@SupportsDaylightSavingTime", SqlDbType.Bit);

                foreach (var zone in TimeZoneInfo.GetSystemTimeZones())
                {
                    Id.Value = zone.Id;
                    DisplayName.Value = zone.DisplayName;
                    SupportsDaylightSavingTime.Value = zone.SupportsDaylightSavingTime;

                    cmd.ExecuteNonQuery();
                }
            }
        }
    }
}

必须添加CLR时,它会变得邪恶WITH PERMISSION_SET = UNSAFE。某些环境不允许这样做,例如AWS RDS。而且,这是不安全的。不幸的是,没有.Net时区的完整实现,未经unsafe允许就无法使用。看到这里这里
弗雷德里克

2

SQL Server版本将于2016年解决这个问题一劳永逸。对于早期版本,CLR解决方案可能是最简单的。或者对于特定的DST规则(仅适用于美国),T-SQL函数可能相对简单。

但是,我认为可以使用通用的T-SQL解决方案。只要xp_regread有效,请尝试以下操作:

CREATE TABLE #tztable (Value varchar(50), Data binary(56));
DECLARE @tzname varchar(150) = 'SYSTEM\CurrentControlSet\Control\TimeZoneInformation'
EXEC master.dbo.xp_regread 'HKEY_LOCAL_MACHINE', @tzname, 'TimeZoneKeyName', @tzname OUT;
SELECT @tzname = 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\' + @tzname
INSERT INTO #tztable
EXEC master.dbo.xp_regread 'HKEY_LOCAL_MACHINE', @tzname, 'TZI';
SELECT                                                                                  -- See http://msdn.microsoft.com/ms725481
 CAST(CAST(REVERSE(SUBSTRING(Data,  1, 4)) AS binary(4))      AS int) AS BiasMinutes,   -- UTC = local + bias: > 0 in US, < 0 in Europe!
 CAST(CAST(REVERSE(SUBSTRING(Data,  5, 4)) AS binary(4))      AS int) AS ExtraBias_Std, --   0 for most timezones
 CAST(CAST(REVERSE(SUBSTRING(Data,  9, 4)) AS binary(4))      AS int) AS ExtraBias_DST, -- -60 for most timezones: DST makes UTC 1 hour earlier
 -- When DST ends:
 CAST(CAST(REVERSE(SUBSTRING(Data, 13, 2)) AS binary(2)) AS smallint) AS StdYear,       -- 0 = yearly (else once)
 CAST(CAST(REVERSE(SUBSTRING(Data, 15, 2)) AS binary(2)) AS smallint) AS StdMonth,      -- 0 = no DST
 CAST(CAST(REVERSE(SUBSTRING(Data, 17, 2)) AS binary(2)) AS smallint) AS StdDayOfWeek,  -- 0 = Sunday to 6 = Saturday
 CAST(CAST(REVERSE(SUBSTRING(Data, 19, 2)) AS binary(2)) AS smallint) AS StdWeek,       -- 1 to 4, or 5 = last <DayOfWeek> of <Month>
 CAST(CAST(REVERSE(SUBSTRING(Data, 21, 2)) AS binary(2)) AS smallint) AS StdHour,       -- Local time
 CAST(CAST(REVERSE(SUBSTRING(Data, 23, 2)) AS binary(2)) AS smallint) AS StdMinute,
 CAST(CAST(REVERSE(SUBSTRING(Data, 25, 2)) AS binary(2)) AS smallint) AS StdSecond,
 CAST(CAST(REVERSE(SUBSTRING(Data, 27, 2)) AS binary(2)) AS smallint) AS StdMillisec,
 -- When DST starts:
 CAST(CAST(REVERSE(SUBSTRING(Data, 29, 2)) AS binary(2)) AS smallint) AS DSTYear,       -- See above
 CAST(CAST(REVERSE(SUBSTRING(Data, 31, 2)) AS binary(2)) AS smallint) AS DSTMonth,
 CAST(CAST(REVERSE(SUBSTRING(Data, 33, 2)) AS binary(2)) AS smallint) AS DSTDayOfWeek,
 CAST(CAST(REVERSE(SUBSTRING(Data, 35, 2)) AS binary(2)) AS smallint) AS DSTWeek,
 CAST(CAST(REVERSE(SUBSTRING(Data, 37, 2)) AS binary(2)) AS smallint) AS DSTHour,
 CAST(CAST(REVERSE(SUBSTRING(Data, 39, 2)) AS binary(2)) AS smallint) AS DSTMinute,
 CAST(CAST(REVERSE(SUBSTRING(Data, 41, 2)) AS binary(2)) AS smallint) AS DSTSecond,
 CAST(CAST(REVERSE(SUBSTRING(Data, 43, 2)) AS binary(2)) AS smallint) AS DSTMillisec
FROM #tztable;
DROP TABLE #tztable

(复杂的)T-SQL函数可以使用此数据来确定当前DST规则中所有日期的确切偏移量。


2
DECLARE @TimeZone VARCHAR(50)
EXEC MASTER.dbo.xp_regread 'HKEY_LOCAL_MACHINE', 'SYSTEM\CurrentControlSet\Control\TimeZoneInformation', 'TimeZoneKeyName', @TimeZone OUT
SELECT @TimeZone
DECLARE @someUtcTime DATETIME
SET @someUtcTime = '2017-03-05 15:15:15'
DECLARE @TimeBiasAtSomeUtcTime INT
SELECT @TimeBiasAtSomeUtcTime = DATEDIFF(MINUTE, @someUtcTime, @someUtcTime AT TIME ZONE @TimeZone)
SELECT DATEADD(MINUTE, @TimeBiasAtSomeUtcTime * -1, @someUtcTime)

2
嗨,乔斯特!感谢您的发布。如果您在答案中添加一些解释,可能会更容易理解。
LowlyDBA

2

这是一个针对特定英国应用的答案,完全基于SELECT。

  1. 没有时区偏移(例如英国)
  2. 为夏令时而写,从三月的最后一个星期日开始,到十月的最后一个星期日结束(英国规定)
  3. 夏令时开始的午夜至凌晨1点之间不适用。可以更正此问题,但编写该应用程序不需要它。

    -- A variable holding an example UTC datetime in the UK, try some different values:
    DECLARE
    @App_Date datetime;
    set @App_Date = '20250704 09:00:00'
    
    -- Outputting the local datetime in the UK, allowing for daylight saving:
    SELECT
    case
    when @App_Date >= dateadd(day, 1 - datepart(weekday, dateadd(day, -1, dateadd(month, 3, dateadd(year, datediff(year, 0, @App_Date), 0)))), dateadd(day, -1, dateadd(month, 3, dateadd(year, datediff(year, 0, @App_Date), 0))))
        and @App_Date < dateadd(day, 1 - datepart(weekday, dateadd(day, -1, dateadd(month, 10, dateadd(year, datediff(year, 0, @App_Date), 0)))), dateadd(day, -1, dateadd(month, 10, dateadd(year, datediff(year, 0, @App_Date), 0))))
        then DATEADD(hour, 1, @App_Date) 
    else @App_Date 
    end

您可能要考虑使用长日期部件名称,而不是短名称。为了清楚起见。参见亚伦·伯特兰(Aaron Bertrand)关于几种“不良习惯”的出色文章
Max Vernon

另外,欢迎使用数据库管理员 - 如果您还没有的话,请参加这次游览
Max Vernon

1
谢谢大家,有用的评论和有用的编辑建议,我在这里是一个新手,以某种方式我已经设法累积了1分,这是fab :-)。
colinp_1

现在你有11
马克斯弗农
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.