Làm cách nào tôi có thể có được phần bù chính xác giữa UTC và giờ địa phương cho một ngày trước hoặc sau DST?


29

Tôi hiện đang sử dụng các cách sau để lấy datetime cục bộ từ datetime UTC:

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

Vấn đề của tôi là nếu thời gian tiết kiệm ánh sáng ban ngày xảy ra giữa GetUTCDate()@utcDateTime, các @localDateTimeđầu lên được một tiếng đồng hồ đi.

Có cách nào dễ dàng để chuyển đổi từ utc sang giờ địa phương cho một ngày không phải là ngày hiện tại không?

Tôi đang sử dụng SQL Server 2005

Câu trả lời:


18

Cách tốt nhất để chuyển đổi ngày UTC không hiện tại thành giờ địa phương là sử dụng CLR. Bản thân mã là dễ dàng; phần khó khăn thường thuyết phục mọi người rằng CLR không hoàn toàn xấu xa hay đáng sợ ...

Để biết một trong nhiều ví dụ, hãy xem bài đăng trên blog của Harsh Chawla về chủ đề này .

Thật không may, không có gì tích hợp có thể xử lý loại chuyển đổi này, tiết kiệm cho các giải pháp dựa trên CLR. Bạn có thể viết một hàm T-SQL thực hiện một cái gì đó như thế này, nhưng sau đó bạn phải tự thực hiện logic thay đổi ngày và tôi gọi đó là quyết định không dễ dàng.


Với sự phức tạp thực tế của các biến thể khu vực theo thời gian, nói rằng "thật không dễ dàng" để thử điều này trong T-SQL thuần túy có lẽ là dưới mức đó ;-). Vì vậy, có, SQLCLR là phương tiện hiệu quả và đáng tin cậy duy nhất để thực hiện thao tác này. +1 cho điều đó. FYI: bài đăng trên blog được liên kết là đúng chức năng nhưng không tuân theo các thực tiễn tốt nhất nên rất tiếc là không hiệu quả. Các chức năng chuyển đổi giữa UTC và máy chủ giờ địa phương có sẵn trong thư viện SQL # (mà tôi là tác giả), nhưng không có trong phiên bản Miễn phí.
Solomon Rutzky 10/03/2015

1
CLR trở nên xấu xa khi nó phải được thêm vào WITH PERMISSION_SET = UNSAFE. Một số môi trường không cho phép nó như AWS RDS. Và nó là, tốt, không an toàn. Thật không may, không có triển khai hoàn thành múi giờ .Net mà có thể sử dụng mà không unsafeđược phép. Xem ở đâyở đây .
Frédéric

15

Tôi đã phát triển và xuất bản dự án Hộp công cụ T-SQL trên codeplex để giúp bất kỳ ai đấu tranh với việc xử lý datetime và múi giờ trong Microsoft SQL Server. Đó là nguồn mở và hoàn toàn miễn phí sử dụng.

Nó cung cấp các UDF chuyển đổi thời gian dễ dàng bằng cách sử dụng T-SQL đơn giản (không có CLR) cùng với các bảng cấu hình được điền sẵn ngoài hộp. Và nó có hỗ trợ DST (tiết kiệm thời gian ban ngày) đầy đủ.

Có thể tìm thấy danh sách tất cả các múi giờ được hỗ trợ trong bảng "DateTimeUtil.Timezone" (được cung cấp trong cơ sở dữ liệu Hộp công cụ T-SQL).

Trong ví dụ của bạn, bạn có thể sử dụng mẫu sau:

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
)

Điều này sẽ trả về giá trị datetime cục bộ đã chuyển đổi.

Thật không may, nó được hỗ trợ cho SQL Server 2008 trở lên chỉ vì các loại dữ liệu mới hơn (DATE, TIME, DATETIME2). Nhưng khi mã nguồn đầy đủ được cung cấp, bạn có thể dễ dàng điều chỉnh các bảng và UDF bằng cách thay thế chúng bằng DATETIME. Tôi không có sẵn MSSQL 2005 để thử nghiệm, nhưng nó cũng hoạt động với MSSQL 2005. Trong trường hợp câu hỏi, chỉ cần cho tôi biết.


12

Tôi luôn sử dụng lệnh TSQL này.

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

-- the local time

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

Nó rất đơn giản và nó làm công việc.


2
Có những múi giờ không phải là thời gian bù toàn bộ giờ từ UTC, vì vậy sử dụng DATEPART này có thể gây ra sự cố cho bạn.
Michael Green

4
Về nhận xét của Michael Green, bạn có thể giải quyết vấn đề bằng cách thay đổi nó thành CHỌN DATEADD (MINUTE, DATEDIFF (MINUTE, GETUTCDATE (), GETDATE ()), @utc).
Người dùng đã đăng ký

4
Điều này không hoạt động vì bạn chỉ xác định xem thời gian hiện tại có phải là DST hay không, sau đó so sánh thời gian có thể là DST hay không. Sử dụng mã ví dụ và thời gian trên của bạn ở Vương quốc Anh hiện tại cho tôi biết rằng phải là 6:14 sáng, tuy nhiên tháng 11 nằm ngoài DST nên sẽ là 5:14 sáng khi GMT và UTC trùng nhau.
Matt

Mặc dù tôi đồng ý rằng điều này không giải quyết được câu hỏi thực tế, theo như câu trả lời này có liên quan, tôi nghĩ rằng những điều sau đây là tốt hơn: CHỌN DATEADD (MINUTE, DATEPART (TZoffset, SYSDATETIMEOFFSET ()), @utc)
Eamon

@Ludo Bernaerts: Lần đầu tiên sử dụng mili giây, giây: điều này không hoạt động vì bù UTC ngày nay có thể khác với bù UTC vào một thời điểm nhất định (tiết kiệm ánh sáng ban ngày - mùa hè so với mùa đông) ...
Quandary

11

Tôi đã tìm thấy câu trả lời này trên StackOverflow cung cấp Hàm do người dùng xác định xuất hiện để dịch chính xác các mốc thời gian

Điều duy nhất bạn cần sửa đổi là @offset biến ở trên cùng để đặt nó thành phần bù múi giờ của máy chủ SQL chạy chức năng này. Trong trường hợp của tôi, máy chủ SQL của chúng tôi sử dụng EST, đó là GMT - 5

Nó không hoàn hảo và có thể sẽ không hoạt động trong nhiều trường hợp như có các TZ bù nửa giờ hoặc 15 phút (đối với những người tôi khuyên dùng chức năng CLR như Kevin khuyên dùng ), tuy nhiên nó hoạt động đủ tốt cho hầu hết các múi giờ chung ở miền Bắc Mỹ.

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

Có một vài câu trả lời hay cho một câu hỏi tương tự được hỏi trên Stack Overflow. Tôi cố gắng sử dụng cách tiếp cận T-SQL từ câu trả lời thứ hai của Bob Albright để dọn dẹp mớ hỗn độn gây ra bởi một nhà tư vấn chuyển đổi dữ liệu.

Nó hoạt động với hầu hết tất cả dữ liệu của chúng tôi, nhưng sau đó tôi nhận ra rằng thuật toán của anh ta chỉ hoạt động cho đến ngày 5 tháng 4 năm 1987 và chúng tôi đã có một số ngày từ những năm 1940 vẫn chưa chuyển đổi chính xác. Cuối cùng, chúng tôi cần UTCngày trong cơ sở dữ liệu SQL Server của chúng tôi để phù hợp với thuật toán trong chương trình bên thứ 3 sử dụng API Java để chuyển đổi từ UTCgiờ địa phương.

Tôi thích CLRví dụ trong câu trả lời của Kevin Feasel ở trên bằng cách sử dụng ví dụ của Harsh Chawla và tôi cũng muốn so sánh nó với một giải pháp sử dụng Java, vì giao diện người dùng của chúng tôi sử dụng Java để thực hiện UTCchuyển đổi theo thời gian cục bộ.

Wikipedia đề cập đến 8 sửa đổi hiến pháp khác nhau liên quan đến điều chỉnh múi giờ trước năm 1987 và nhiều trong số đó được bản địa hóa ở các quốc gia khác nhau, do đó, có khả năng CLR và Java có thể diễn giải chúng khác nhau. Mã ứng dụng mặt trước của bạn có sử dụng dotnet hoặc Java hay là ngày trước năm 1987 là một vấn đề đối với bạn?


2

Bạn có thể dễ dàng làm điều này với Thủ tục lưu trữ 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);
}

Bạn có thể lưu trữ các múi giờ có sẵn trong một bảng:

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

Và thủ tục được lưu trữ này sẽ lấp đầy bảng với các múi giờ có thể có trên máy chủ của bạn.

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 trở nên xấu xa khi nó phải được thêm vào WITH PERMISSION_SET = UNSAFE. Một số môi trường không cho phép nó như AWS RDS. Và nó là, tốt, không an toàn. Thật không may, không có triển khai hoàn thành múi giờ .Net mà có thể sử dụng mà không unsafeđược phép. Xem ở đâyở đây .
Frédéric

2

SQL Server phiên bản 2016 sẽ giải quyết vấn đề này một lần và mãi mãi . Đối với các phiên bản trước, giải pháp CLR có lẽ là dễ nhất. Hoặc đối với quy tắc DST cụ thể (chỉ ở Hoa Kỳ), hàm T-SQL có thể tương đối đơn giản.

Tuy nhiên, tôi nghĩ rằng một giải pháp T-SQL chung có thể khả thi. Miễn là xp_regreadhoạt động, hãy thử điều này:

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

Hàm T-SQL (phức tạp) có thể sử dụng dữ liệu này để xác định phần bù chính xác cho tất cả các ngày trong quy tắc DST hiện tại.


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
Xin chào! Cảm ơn vì đăng. Nếu bạn thêm một số lời giải thích cho câu trả lời của bạn, nó có thể chứng minh là dễ hiểu hơn rất nhiều.
LowlyDBA

2

Dưới đây là câu trả lời được viết cho một ứng dụng cụ thể của Vương quốc Anh và hoàn toàn dựa trên CHỌN.

  1. Không có bù múi giờ (ví dụ: Vương quốc Anh)
  2. Viết để tiết kiệm ánh sáng ban ngày bắt đầu vào Chủ nhật cuối tháng 3 và kết thúc vào Chủ nhật cuối tháng 10 (theo quy định của Anh)
  3. Không áp dụng từ nửa đêm đến 1 giờ sáng khi bắt đầu tiết kiệm ánh sáng ban ngày. Điều này có thể được sửa nhưng ứng dụng được viết không yêu cầu.

    -- 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

Bạn có thể muốn xem xét sử dụng tên phần ngày dài, thay vì tên ngắn. Chỉ cho rõ ràng. Xem bài viết tuyệt vời của Aaron Bertrand về một số "thói quen xấu"
Max Vernon

Ngoài ra, chào mừng bạn đến với Quản trị viên Cơ sở dữ liệu - vui lòng tham quan nếu bạn chưa có!
Max Vernon

1
Cảm ơn tất cả, các bình luận hữu ích và các đề xuất chỉnh sửa hữu ích, tôi là một người mới hoàn toàn ở đây, bằng cách nào đó tôi đã quản lý để tích lũy được 1 điểm đó là fab :-).
colinp_1

bây giờ bạn có 11.
Max Vernon
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.