Jak uzyskać prawidłowe przesunięcie między czasem UTC a czasem lokalnym dla daty poprzedzającej czas letni lub później?


29

Obecnie używam następujących danych, aby uzyskać lokalną datę / godzinę z godziny UTC:

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

Mój problem polega na tym, że jeśli czas letni pojawia się między GetUTCDate()a @utcDateTime, @localDateTimekończy się godzina przerwy.

Czy istnieje prosty sposób na konwersję z utc na czas lokalny dla daty, która nie jest bieżącą datą?

Używam SQL Server 2005

Odpowiedzi:


18

Najlepszym sposobem na konwersję nieaktualnej daty UTC na czas lokalny jest użycie CLR. Sam kod jest łatwy; trudną częścią jest zwykle przekonywanie ludzi, że CLR nie jest czystym złem ani przerażającym ...

Jeden z wielu przykładów znajduje się na blogu Harsha Chawli na ten temat .

Niestety, nie ma nic wbudowanego, co mogłoby poradzić sobie z tego typu konwersją, z wyjątkiem rozwiązań opartych na CLR. Mógłbyś napisać funkcję T-SQL, która robi coś takiego, ale wtedy musiałbyś sam zaimplementować logikę zmiany daty, a ja nazwałbym to zdecydowanie niełatwe.


Biorąc pod uwagę faktyczną złożoność regionalnych zmian w czasie, stwierdzenie, że „zdecydowanie nie jest łatwo” spróbować tego w czystym języku T-SQL, prawdopodobnie nie jest wystarczające ;-). Tak więc, SQLCLR jest jedynym niezawodnym i wydajnym sposobem wykonania tej operacji. +1 za to. Do Twojej wiadomości: link do blogu jest funkcjonalnie poprawny, ale nie przestrzega najlepszych praktyk, więc jest niestety nieefektywny. Funkcje konwersji między czasem UTC i czasem lokalnym serwera są dostępne w bibliotece SQL # (której jestem autorem), ale nie w wersji darmowej.
Solomon Rutzky

1
CLR dostaje zło, gdy trzeba je dodać WITH PERMISSION_SET = UNSAFE. Niektóre środowiska nie pozwalają na to, tak jak AWS RDS. I to jest niebezpieczne. Niestety nie ma pełnej implementacji strefy czasowej .Net, którą można wykorzystać bez unsafepozwolenia. Zobacz tutaj i tutaj .
Frédéric

15

Opracowałem i opublikowałem projekt T-SQL Toolbox na codeplex, aby pomóc każdemu, kto zmaga się z obsługą czasu i strefy czasowej w Microsoft SQL Server. Jest open source i całkowicie darmowy.

Oferuje łatwe UDF do konwersji danych za pomocą zwykłego T-SQL (bez CLR) oraz wstępnie wypełnione tabele konfiguracji po wyjęciu z pudełka. I ma pełną obsługę DST (czasu letniego).

Lista wszystkich obsługiwanych stref czasowych znajduje się w tabeli „DateTimeUtil.Timezone” (podanej w bazie danych T-SQL Toolbox).

W twoim przykładzie możesz użyć następującej próbki:

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
)

Zwróci to skonwertowaną wartość lokalnej daty i godziny.

Niestety jest obsługiwany w SQL Server 2008 lub nowszym tylko z powodu nowszych typów danych (DATE, TIME, DATETIME2). Ponieważ jednak podano pełny kod źródłowy, możesz łatwo dostosować tabele i UDF, zastępując je DATETIME. Nie mam MSSQL 2005 dostępnego do testowania, ale powinien on również działać z MSSQL 2005. W przypadku pytań daj mi znać.


12

Zawsze używam tego polecenia TSQL.

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

-- the local time

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

To jest bardzo proste i działa.


2
Istnieją strefy czasowe, które nie są przesunięciem pełnej godziny w stosunku do UTC, więc użycie tego DATEPART może powodować problemy.
Michael Green,

4
Jeśli chodzi o komentarz Michaela Greena, możesz rozwiązać ten problem, zmieniając go na SELECT DATEADD (MINUTE, DATEDIFF (MINUTE, GETUTCDATE (), GETDATE ()), @utc).
Zarejestrowany użytkownik

4
To nie działa, ponieważ określasz tylko, czy bieżący czas jest czasem letnim, czy nie, a następnie porównujesz czas, który może być czasem letnim. Użycie powyższego przykładowego kodu i daty i godziny w Wielkiej Brytanii mówi mi obecnie, że powinna to być godzina 6:14, jednak listopad jest poza czasem letnim, więc powinna być godzina 5:14, ponieważ GMT i UTC pokrywają się.
Matt

Chociaż zgadzam się, że to nie dotyczy rzeczywistego pytania, w odniesieniu do tej odpowiedzi uważam, że lepiej jest wybrać: WYBIERZ DATĘDATA (MINUTA, DATEPART (TZoffset, SYSDATETIMEOFFSET ()), @utc)
Eamon

@Ludo Bernaerts: Pierwsze użycie milisekund, po drugie: to nie działa, ponieważ przesunięcie UTC dzisiaj może być inne niż przesunięcie UTC w określonym czasie (czas letni - czas letni i zimowy) ...
Quandary

11

Znalazłem tę odpowiedź na StackOverflow, która udostępnia funkcję zdefiniowaną przez użytkownika, która wydaje się dokładnie tłumaczyć czasy danych

Jedyną rzeczą, którą musisz zmodyfikować, jest @offsetzmienna u góry, aby ustawić przesunięcie strefy czasowej serwera SQL, na którym działa ta funkcja. W moim przypadku nasz serwer SQL używa EST, czyli GMT - 5

To nie jest idealne i prawdopodobnie nie zadziała w wielu przypadkach, np. Ma półgodzinne lub 15-minutowe przesunięcia TZ (dla tych poleciłbym funkcję CLR jak Kevin ), jednak działa wystarczająco dobrze w większości ogólnych stref czasowych na północy Ameryka.

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

Istnieje kilka dobrych odpowiedzi na podobne pytanie dotyczące przepełnienia stosu. Skończyło się na użyciu T-SQL z drugiej odpowiedzi Boba Albrighta, aby posprzątać bałagan spowodowany przez konsultanta konwersji danych.

Działało ono dla prawie wszystkich naszych danych, ale później zdałem sobie sprawę, że jego algorytm działa tylko w przypadku dat sięgających 5 kwietnia 1987 r. , A my mieliśmy pewne daty z lat 40. XX wieku, które nadal nie zostały poprawnie przekonwertowane. Ostatecznie potrzebowaliśmy UTCdat w naszej bazie danych SQL Server, aby dopasować się do algorytmu w programie innej firmy, który używał interfejsu API Java do konwersji z UTCczasu lokalnego.

Podoba mi się CLRprzykład w powyższej odpowiedzi Kevina Feasela z wykorzystaniem przykładu Harsha Chawli , a także chciałbym porównać go z rozwiązaniem wykorzystującym Javę, ponieważ nasz interfejs używa Javy do UTCkonwersji na czas lokalny.

Wikipedia wspomina o 8 różnych poprawkach do konstytucji, które wymagają dostosowania stref czasowych przed 1987 r., A wiele z nich jest bardzo zlokalizowanych w różnych stanach, więc istnieje szansa, że ​​CLR i Java mogą je interpretować inaczej. Czy Twój kod aplikacji frontonu używa dotnet lub Java, czy też daty przed 1987 r. Stanowią dla ciebie problem?


2

Możesz to łatwo zrobić za pomocą procedury przechowywanej 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);
}

Dostępne strefy czasowe możesz przechowywać w tabeli:

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

Ta procedura składowana wypełni tabelę możliwymi strefami czasowymi na twoim serwerze.

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 dostaje zło, gdy trzeba je dodać WITH PERMISSION_SET = UNSAFE. Niektóre środowiska nie pozwalają na to, tak jak AWS RDS. I to jest niebezpieczne. Niestety nie ma pełnej implementacji strefy czasowej .Net, którą można wykorzystać bez unsafepozwolenia. Zobacz tutaj i tutaj .
Frédéric

2

Wersja SQL Server 2016 rozwiąże ten problem raz na zawsze . We wcześniejszych wersjach rozwiązanie CLR jest prawdopodobnie najłatwiejsze. Lub dla określonej reguły DST (jak tylko USA), funkcja T-SQL może być stosunkowo prosta.

Myślę jednak, że ogólne rozwiązanie T-SQL może być możliwe. Tak długo, jak xp_regreaddziała, spróbuj tego:

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

(Złożona) funkcja T-SQL mogłaby użyć tych danych do ustalenia dokładnego przesunięcia dla wszystkich dat podczas bieżącej reguły 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
Cześć Joost! Dzięki za wysłanie. Jeśli dodasz jakieś wyjaśnienie do swojej odpowiedzi, może okazać się łatwiejsze do zrozumienia.
LowlyDBA,

2

Oto odpowiedź napisana dla konkretnej aplikacji w Wielkiej Brytanii i oparta wyłącznie na SELECT.

  1. Brak przesunięcia strefy czasowej (np. Wielka Brytania)
  2. Napisane dla czasu letniego rozpoczynającego się w ostatnią niedzielę marca i kończącego się w ostatnią niedzielę października (przepisy brytyjskie)
  3. Nie dotyczy między północą a 1 nad ranem w dniu rozpoczęcia dnia. Można to poprawić, ale aplikacja, dla której została napisana, nie wymaga tego.

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

Możesz rozważyć użycie długich nazw części zamiast krótkich. Dla jasności. Zobacz doskonały artykuł Aarona Bertranda na temat kilku „złych nawyków”
Max Vernon

Ponadto zapraszamy do administratorów baz danych - proszę wziąć wycieczkę , jeśli jeszcze nie masz!
Max Vernon

1
Dzięki wszystkim, pomocne komentarze i pomocne sugestie dotyczące edycji, jestem tu całkowicie początkującym, jakoś udało mi się zgromadzić 1 punkt, co jest super :-).
colinp_1

teraz masz 11.
Max Vernon
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.