Come posso ottenere l'offset corretto tra l'ora UTC e l'ora locale per una data precedente o successiva all'ora legale?


29

Attualmente uso quanto segue per ottenere un datetime locale da un datetime UTC:

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

Il mio problema è che se si verifica l'ora legale tra GetUTCDate()e @utcDateTime, il@localDateTime finisce per essere un off un'ora.

C'è un modo semplice per convertire da UTC all'ora locale per una data che non è la data corrente?

Sto usando SQL Server 2005

Risposte:


18

Il modo migliore per convertire una data UTC non corrente in ora locale è utilizzare il CLR. Il codice stesso è semplice; la parte difficile di solito è convincere la gente che il CLR non è puro male o paura ...

Per uno dei tanti esempi, dai un'occhiata al post sul blog di Harsh Chawla sull'argomento .

Sfortunatamente, non esiste nulla di integrato che possa gestire questo tipo di conversione, ad eccezione delle soluzioni basate su CLR. Potresti scrivere una funzione T-SQL che fa qualcosa del genere, ma poi dovresti implementare tu stesso la logica di cambio data e la definirei decisamente non facile.


Data l'effettiva complessità delle variazioni regionali nel tempo, dire che "decisamente non è facile" tentare questo in puro T-SQL probabilmente lo sta sottostimando ;-). Quindi sì, SQLCLR è l'unico mezzo affidabile ed efficiente per eseguire questa operazione. +1 per quello. Cordiali saluti: il post sul blog collegato è funzionalmente corretto ma non segue le migliori pratiche, quindi è purtroppo inefficiente. Le funzioni per la conversione tra l'ora locale UTC e l'ora del server sono disponibili nella libreria SQL # (di cui sono l'autore), ma non nella versione gratuita.
Solomon Rutzky,

1
CLR diventa malvagio quando deve essere aggiunto WITH PERMISSION_SET = UNSAFE. Alcuni ambienti non lo consentono come AWS RDS. Ed è, beh, non sicuro. Sfortunatamente, non esiste un'implementazione completa del fuso orario .Net utilizzabile senza unsafeautorizzazione. Vedi qui e qui .
Frédéric,

15

Ho sviluppato e pubblicato la T-SQL Toolbox su codeplex per aiutare chiunque abbia difficoltà a gestire il datetime e il fuso orario in Microsoft SQL Server. È open source e completamente gratuito da usare.

Offre UDF di conversione datetime semplici utilizzando T-SQL semplice (senza CLR) oltre a tabelle di configurazione precompilate pronte all'uso. E ha il pieno supporto DST (ora legale).

Un elenco di tutti i fusi orari supportati è disponibile nella tabella "DateTimeUtil.Timezone" (fornita nel database T-SQL Toolbox).

Nel tuo esempio, puoi usare il seguente esempio:

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
)

Ciò restituirà il valore datetime locale convertito.

Sfortunatamente, è supportato per SQL Server 2008 o versioni successive solo a causa di tipi di dati più recenti (DATA, ORA, DATA DATA2). Tuttavia, poiché viene fornito il codice sorgente completo, è possibile regolare facilmente le tabelle e gli UDF sostituendoli con DATETIME. Non ho un MSSQL 2005 disponibile per i test, ma dovrebbe funzionare anche con MSSQL 2005, quindi. In caso di domande, fammelo sapere.


12

Uso sempre questo comando TSQL.

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

-- the local time

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

È molto semplice e fa il lavoro.


2
Esistono fusi orari che non sono un offset di ora intera da UTC, quindi l'utilizzo di questo DATEPART può causare problemi.
Michael Green,

4
Per quanto riguarda il commento di Michael Green, è possibile risolvere il problema modificandolo in SELEZIONA DATA (MINUTI, DATAVIFF (MINUTI, GETUTCDATE (), GETDATE ()), @utc).
Utente registrato

4
Questo non funziona poiché stai solo determinando se l'ora corrente è l'ora legale o meno, quindi confronta un orario che potrebbe essere l'ora legale o meno. L'uso del codice di esempio sopra e datetime nel Regno Unito attualmente mi dice che dovrebbero essere le 6:14, tuttavia novembre è fuori ora legale, quindi dovrebbero essere le 5:14 in quanto GMT e UTC coincidono.
Matt,

Anche se sono d'accordo sul fatto che questo non affronta la vera domanda, per quanto riguarda questa risposta penso che sia meglio quanto segue: SELEZIONA DATA (MINUTI, DATA DATA (TZoffset, SYSDATETIMEOFFSET ()), @utc)
Eamon

@Ludo Bernaerts: Primo utilizzo millisecondi, secondo: questo non funziona perché l'offset UTC oggi potrebbe essere diverso dall'offset UTC in un determinato momento (ora legale - ora legale rispetto all'ora solare) ...
Quandary

11

Ho trovato questa risposta su StackOverflow che fornisce una funzione definita dall'utente che sembra tradurre con precisione i tempi dei dati

L'unica cosa che devi modificare è la @offsetvariabile in alto per impostarla sull'offset del fuso orario del server SQL che esegue questa funzione. Nel mio caso, il nostro server SQL utilizza EST, che è GMT - 5

Non è perfetto e probabilmente non funzionerà in molti casi, ad esempio ha offset TZ di mezz'ora o 15 minuti (per quelli che consiglierei una funzione CLR come Kevin consigliato ), tuttavia funziona abbastanza bene per la maggior parte dei fusi orari generici nel Nord America.

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

Ci sono un paio di buone risposte a una domanda simile posta su Stack Overflow. Ho finito usando un approccio T-SQL dalla seconda risposta di Bob Albright per ripulire un casino causato da un consulente di conversione dei dati.

Ha funzionato per quasi tutti i nostri dati, ma poi ho capito che il suo algoritmo funziona solo per date risalenti al 5 aprile 1987 , e abbiamo avuto alcune date degli anni '40 che ancora non si convertivano correttamente. Alla fine avevamo bisogno delle UTCdate nel nostro database di SQL Server per allinearci con un algoritmo in un programma di terze parti che utilizzava l'API Java per la conversione daUTC dall'ora locale.

mi piace il CLR esempio nella risposta di Kevin Feasel sopra usando l'esempio di Harsh Chawla, e mi piacerebbe anche confrontarlo con una soluzione che utilizza Java, poiché il nostro front-end usa Java per fare la UTCconversione in ora locale.

Wikipedia menziona 8 diversi emendamenti costituzionali che comportano aggiustamenti del fuso orario prima del 1987, e molti di questi sono molto localizzati in stati diversi, quindi c'è una possibilità che CLR e Java possano interpretarli in modo diverso. Il codice dell'applicazione front-end utilizza dotnet o Java o le date precedenti al 1987 sono un problema per te?


2

Puoi farlo facilmente con una stored procedure 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);
}

È possibile memorizzare i fusi orari disponibili in una tabella:

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

E questa procedura memorizzata riempirà la tabella con i possibili fusi orari sul tuo 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();
                }
            }
        }
    }
}

CLR diventa malvagio quando deve essere aggiunto WITH PERMISSION_SET = UNSAFE. Alcuni ambienti non lo consentono come AWS RDS. Ed è, beh, non sicuro. Sfortunatamente, non esiste un'implementazione completa del fuso orario .Net utilizzabile senza unsafeautorizzazione. Vedi qui e qui .
Frédéric,

2

SQL Server versione 2016 risolverà questo problema una volta per tutte . Per le versioni precedenti una soluzione CLR è probabilmente la più semplice. O per una specifica ora legale (solo negli Stati Uniti), una funzione T-SQL può essere relativamente semplice.

Tuttavia, penso che potrebbe essere possibile una soluzione T-SQL generica. Finché xp_regreadfunziona, prova questo:

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

Una funzione T-SQL (complessa) potrebbe utilizzare questi dati per determinare l'offset esatto per tutte le date durante l'attuale regola dell'ora legale.


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
Ciao Joost! Grazie per la pubblicazione. Se aggiungi qualche spiegazione alla tua risposta, potrebbe rivelarsi molto più facile da capire.
LowlyDBA,

2

Ecco una risposta scritta per una specifica applicazione del Regno Unito e basata esclusivamente su SELECT.

  1. Nessun offset del fuso orario (ad es. Regno Unito)
  2. Scritto per l'ora legale a partire dall'ultima domenica di marzo e terminando l'ultima domenica di ottobre (regole del Regno Unito)
  3. Non applicabile tra la mezzanotte e l'01 del giorno in cui inizia l'ora legale. Questo potrebbe essere corretto, ma l'applicazione per cui è stato scritto non lo richiede.

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

Si consiglia di utilizzare i nomi delle parti con data estesa anziché quelli brevi. Solo per chiarezza. Vedi l'eccellente articolo di Aaron Bertrand su diverse "cattive abitudini"
Max Vernon,

Inoltre, benvenuto agli amministratori del database : per favore fai il tour se non l'hai già fatto!
Max Vernon,

1
Grazie a tutti, commenti utili e utili suggerimenti di modifica, sono un principiante totale qui, in qualche modo sono riuscito ad accumulare 1 punto che è favoloso :-).
colinp_1,

ora ne hai 11.
Max Vernon,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.