Answers:
将非当前UTC日期转换为本地时间的最佳方法是使用CLR。代码本身很简单;困难的部分通常是说服人们CLR并不是纯粹的邪恶或可怕的事物...
有关众多示例之一,请查看Harsh Chawla关于该主题的博客文章。
不幸的是,除了基于CLR的解决方案外,没有任何内置函数可以处理这种类型的转换。您可以编写一个执行以下操作的T-SQL函数,但是您必须自己实现日期更改逻辑,而我断定这绝对不容易。
我已经开发并出版了 在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一起工作。如有疑问,请告诉我。
我总是使用此TSQL命令。
-- the utc value
declare @utc datetime = '20/11/2014 05:14'
-- the local time
select DATEADD(hh, DATEDIFF(hh, getutcdate(), getdate()), @utc)
这非常简单,可以完成工作。
我找到了这个答案在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
对于堆栈溢出中的类似问题,有两个很好的答案。我从鲍勃·奥尔布赖特(Bob Albright)的第二个回答中开始使用T-SQL方法来清理由数据转换顾问引起的混乱。
它适用于几乎所有数据,但后来我意识到他的算法仅适用于1987年4月5日之前的日期,而且我们有一些1940年代的日期仍无法正确转换。最终,我们需要UTC
SQL Server数据库中的日期与第3方程序中使用Java API从UTC
时间为本地时间。
我喜欢的CLR
例子在凯文Feasel的回答上面使用刺激性乔拉的例子,而且我也喜欢比较它使用Java,因为我们的前端使用Java做的解决方案UTC
,以本地时间转换。
Wikipedia提到了8种不同的宪法修正案,其中涉及1987年之前的时区调整,其中许多修正案非常局限在不同的州,因此CLR和Java可能会以不同的方式解释它们。您的前端应用程序代码是使用dotnet还是Java,还是1987年之前的日期对您来说是一个问题?
您可以使用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();
}
}
}
}
}
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规则中所有日期的确切偏移量。
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)
这是一个针对特定英国应用的答案,完全基于SELECT。
夏令时开始的午夜至凌晨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