Wie kann ich den korrekten Versatz zwischen UTC und Ortszeit für ein Datum vor oder nach der Sommerzeit ermitteln?

29

Ich verwende derzeit Folgendes, um eine lokale Datumszeit von einer UTC-Datumszeit zu erhalten:

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

Mein Problem ist, dass, wenn Sommerzeit zwischen GetUTCDate()und auftritt @utcDateTime, die@localDateTime Ende eine Stunde frei ist.

Gibt es eine einfache Möglichkeit, ein Datum, das nicht das aktuelle Datum ist, von UTC auf Ortszeit umzurechnen?

Ich verwende SQL Server 2005

Rachel
quelle

Antworten:

18

Die beste Möglichkeit, ein nicht aktuelles UTC-Datum in die Ortszeit umzuwandeln, ist die Verwendung der CLR. Der Code selbst ist einfach; Das Schwierige ist normalerweise, die Leute davon zu überzeugen, dass die CLR nicht nur böse oder beängstigend ist ...

Schauen Sie sich für eines der vielen Beispiele Harsh Chawlas Blogpost zum Thema an .

Leider gibt es nichts Eingebautes, das diese Art der Konvertierung verarbeiten kann, außer für CLR-basierte Lösungen. Sie könnten eine T-SQL-Funktion schreiben, die so etwas macht, aber dann müssten Sie die Datumsänderungslogik selbst implementieren, und ich würde das entschieden nicht einfach nennen.

Kevin Feasel
quelle
Angesichts der tatsächlichen Komplexität regionaler Unterschiede im Zeitverlauf ist es wahrscheinlich nicht einfach, dies in reinem T-SQL zu versuchen ;-). Ja, SQLCLR ist die einzige zuverlässige und effiziente Methode, um diese Operation auszuführen. +1 dafür. Zu Ihrer Information: Der verlinkte Blog-Beitrag ist funktional korrekt, folgt jedoch nicht den bewährten Methoden und ist daher leider ineffizient. Funktionen zum Konvertieren zwischen UTC- und Server-Ortszeit sind in der SQL # -Bibliothek (deren Autor ich bin) verfügbar , jedoch nicht in der kostenlosen Version.
Solomon Rutzky
1
CLR wird böse, wenn es hinzugefügt werden muss WITH PERMISSION_SET = UNSAFE. In einigen Umgebungen ist dies wie in AWS RDS nicht möglich. Und es ist unsicher. Leider gibt es keine vollständige Implementierung der .Net-Zeitzone, die ohne unsafeErlaubnis verwendet werden kann. Sehen Sie hier und hier .
Frédéric
15

Ich habe die T-SQL Toolbox entwickelt und veröffentlicht Projekt für Codeplex , um allen zu helfen, die mit der Verarbeitung von Datum und Uhrzeit in Microsoft SQL Server zu kämpfen haben. Es ist Open Source und völlig kostenlos zu benutzen.

Es bietet einfache UDFs für die Datums- und Uhrzeitkonvertierung mit einfachem T-SQL (keine CLRs) sowie vorgefüllte Konfigurationstabellen. Und es hat volle Sommerzeitunterstützung.

Eine Liste aller unterstützten Zeitzonen finden Sie in der Tabelle "DateTimeUtil.Timezone" (in der T-SQL Toolbox-Datenbank enthalten).

In Ihrem Beispiel können Sie das folgende Beispiel verwenden:

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
)

Dadurch wird der konvertierte lokale Datums- / Uhrzeitwert zurückgegeben.

Leider wird es für SQL Server 2008 oder höher nur aufgrund neuerer Datentypen (DATE, TIME, DATETIME2) unterstützt. Da der vollständige Quellcode bereitgestellt wird, können Sie die Tabellen und UDFs problemlos anpassen, indem Sie sie durch DATETIME ersetzen. Ich habe kein MSSQL 2005 zum Testen zur Verfügung, aber es sollte dann auch mit MSSQL 2005 funktionieren. Bei Fragen lass es mich einfach wissen.

adss
quelle
12

Ich benutze immer diesen TSQL-Befehl.

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

-- the local time

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

Es ist sehr einfach und es macht den Job.

Ludo Bernaerts
quelle
2
Es gibt Zeitzonen, die nicht um eine volle Stunde von UTC versetzt sind. Daher kann die Verwendung dieses DATEPART zu Problemen führen.
Michael Green
4
Bezüglich des Kommentars von Michael Green können Sie das Problem beheben, indem Sie es in SELECT DATEADD (MINUTE, DATEDIFF (MINUTE, GETUTCDATE (), GETDATE ()), @utc) ändern.
Registrierter Benutzer
4
Dies funktioniert nicht, da Sie nur bestimmen, ob die aktuelle Zeit DST ist oder nicht, und dann eine Zeit vergleichen, die DST sein könnte oder nicht. Wenn Sie den obigen Beispielcode und die Datums- und Uhrzeitangabe in Großbritannien verwenden, sollte dies derzeit 6:14 Uhr sein, der November liegt jedoch außerhalb der Sommerzeit. Daher sollte es 5:14 Uhr sein, da GMT und UTC übereinstimmen.
Matt
Ich bin damit einverstanden, dass dies die eigentliche Frage nicht anspricht. In Bezug auf diese Antwort halte ich Folgendes für besser: SELECT DATEADD (MINUTE, DATEPART (TZoffset, SYSDATETIMEOFFSET ()), @utc)
Eamon
@Ludo Bernaerts: Erstens Millisekunden verwenden, zweitens: Dies funktioniert nicht, da der UTC-Versatz heute möglicherweise zu einer bestimmten Zeit (Sommer- / Winterzeit) vom UTC-Versatz abweicht ...
Quandary
11

Ich habe diese Antwort gefunden auf StackOverflow gefunden, das eine benutzerdefinierte Funktion bereitstellt, die die Datumsangaben genau zu übersetzen scheint

Das einzige, was Sie ändern müssen, ist die @offset Variable oben, um sie auf den Zeitzonenoffset des SQL-Servers festzulegen, auf dem diese Funktion ausgeführt wird. In meinem Fall verwendet unser SQL-Server EST (GMT-5)

Es ist nicht perfekt und wird wahrscheinlich in vielen Fällen nicht funktionieren, wenn TZ-Offsets für eine halbe Stunde oder 15 Minuten vorliegen (für diejenigen, für die ich eine CLR-Funktion wie die von Kevin empfohlene empfehlen würde ), aber es funktioniert gut genug für die meisten allgemeinen Zeitzonen im Norden Amerika.

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
Rachel
quelle
3

Auf eine ähnliche Frage , die bei Stack Overflow gestellt wird, gibt es einige gute Antworten . Abschließend habe ich einen T-SQL-Ansatz aus der zweiten Antwort von Bob Albright verwendet , um ein Durcheinander zu beseitigen, das von einem Datenkonvertierungsberater verursacht wurde.

Es funktionierte für fast alle unsere Daten, aber später wurde mir klar, dass sein Algorithmus nur für Datumsangaben bis zum 5. April 1987 funktioniert , und wir hatten einige Datumsangaben aus den 1940er Jahren, die immer noch nicht richtig konvertiert wurden. Letztendlich mussten die UTCDaten in unserer SQL Server-Datenbank mit einem Algorithmus in einem Programm eines Drittanbieters übereinstimmen, das die Java-API für die Konvertierung von UTCin die Ortszeit verwendete.

Ich mag das obigeCLR Beispiel in Kevin Feasels Antwort unter Verwendung von Harsh Chawlas Beispiel und ich möchte es auch mit einer Lösung vergleichen, die Java verwendet, da unser Front-End Java verwendet, um die UTCKonvertierung in die lokale Zeit durchzuführen.

Wikipedia erwähnt 8 verschiedene Verfassungsänderungen, die Zeitzonenanpassungen vor 1987 beinhalten, und viele davon sind stark auf verschiedene Bundesstaaten beschränkt, so dass die CLR und Java sie möglicherweise unterschiedlich interpretieren. Verwendet Ihr Front-End-Anwendungscode Dotnet oder Java oder sind Daten vor 1987 ein Problem für Sie?

kkarns
quelle
2

Sie können dies problemlos mit einer gespeicherten CLR-Prozedur tun.

[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);
}

Sie können die verfügbaren Zeitzonen in einer Tabelle speichern:

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

Und diese gespeicherte Prozedur füllt die Tabelle mit den möglichen Zeitzonen auf Ihrem Server.

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();
                }
            }
        }
    }
}
Tim Cooke
quelle
CLR wird böse, wenn es hinzugefügt werden muss WITH PERMISSION_SET = UNSAFE. In einigen Umgebungen ist dies wie in AWS RDS nicht möglich. Und es ist unsicher. Leider gibt es keine vollständige Implementierung der .Net-Zeitzone, die ohne unsafeErlaubnis verwendet werden kann. Sehen Sie hier und hier .
Frédéric
2

Die SQL Server-Version 2016 wird dieses Problem ein für alle Mal lösen . Für frühere Versionen ist eine CLR-Lösung wahrscheinlich am einfachsten. Oder für eine bestimmte DST-Regel (wie nur für USA) kann eine T-SQL-Funktion relativ einfach sein.

Ich denke jedoch, dass eine generische T-SQL-Lösung möglich sein könnte. xp_regreadVersuchen Sie Folgendes, solange es funktioniert:

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

Eine (komplexe) T-SQL-Funktion könnte diese Daten verwenden , um den genauen Versatz für alle Daten während der aktuellen DST-Regel zu bestimmen.

Michel de Ruiter
quelle
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)
Joost Versteegen
quelle
2
Hallo Joost! Danke fürs Schreiben. Wenn Sie Ihrer Antwort eine Erklärung hinzufügen, ist dies möglicherweise viel einfacher zu verstehen.
LowlyDBA
2

Hier ist eine Antwort, die für eine bestimmte britische Anwendung geschrieben wurde und ausschließlich auf SELECT basiert.

  1. Kein Zeitzonenversatz (zB UK)
  2. Aus Gründen der Sommerzeit ab dem letzten Sonntag im März und bis zum letzten Sonntag im Oktober (UK-Regeln)
  3. Bei Beginn der Sommerzeit zwischen Mitternacht und 1 Uhr morgens nicht anwendbar. Dies könnte korrigiert werden, aber die Anwendung, für die es geschrieben wurde, erfordert es nicht.

    -- 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
colinp_1
quelle
Möglicherweise möchten Sie die langen Datumsteilnamen anstelle der kurzen verwenden. Nur zur Klarheit. Siehe Aaron Bertrands hervorragenden Artikel über verschiedene "schlechte Gewohnheiten"
Max Vernon,
Willkommen auch bei den Datenbankadministratoren - bitte nehmen Sie an der Tour teil, falls Sie dies noch nicht getan haben!
Max Vernon
1
Vielen Dank an alle, hilfreiche Kommentare und hilfreiche Bearbeitungsvorschläge, ich bin ein totaler Neuling hier, irgendwie habe ich es geschafft, 1 Punkt zu sammeln, was fabelhaft ist :-).
colinp_1
Jetzt haben Sie 11.
Max Vernon