¿Cómo puedo obtener el desplazamiento correcto entre las horas UTC y locales para una fecha anterior o posterior al horario de verano?


29

Actualmente utilizo lo siguiente para obtener una fecha y hora local de una fecha y hora UTC:

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

Mi problema es que si el horario de verano se produce entre GetUTCDate()y @utcDateTime, @localDateTimetermina siendo una hora libre.

¿Hay una manera fácil de convertir de utc a hora local para una fecha que no es la fecha actual?

Estoy usando SQL Server 2005

Respuestas:


18

La mejor manera de convertir una fecha UTC no actual en hora local es usar el CLR. El código en sí es fácil; La parte difícil suele ser convencer a la gente de que el CLR no es pura maldad ni miedo ...

Para uno de los muchos ejemplos, consulte la publicación de blog de Harsh Chawla sobre el tema .

Desafortunadamente, no hay nada incorporado que pueda manejar este tipo de conversión, salvo las soluciones basadas en CLR. Podrías escribir una función T-SQL que haga algo como esto, pero luego tendrías que implementar la lógica de cambio de fecha tú mismo, y yo diría que eso definitivamente no es fácil.


Dada la complejidad real de las variaciones regionales a lo largo del tiempo, decir que "definitivamente no es fácil" intentar esto en T-SQL puro probablemente lo está entendiendo ;-). Entonces, sí, SQLCLR es el único medio confiable y eficiente para realizar esta operación. +1 por eso. FYI: la publicación de blog vinculada es funcionalmente correcta pero no sigue las mejores prácticas, por lo que desafortunadamente es ineficiente. Las funciones para convertir entre UTC y la hora local del servidor están disponibles en la biblioteca SQL # (de la que soy autor), pero no en la versión gratuita.
Solomon Rutzky

1
CLR se vuelve malvado cuando debe agregarse WITH PERMISSION_SET = UNSAFE. Algunos entornos no lo permiten como AWS RDS. Y es, bueno, inseguro. Desafortunadamente, no existe una implementación completa de zona horaria .Net que se pueda usar sin unsafepermiso. Mira aquí y aquí .
Frédéric

15

Desarrollé y publiqué el proyecto T-SQL Toolbox en codeplex para ayudar a cualquiera que tenga problemas con el manejo de fecha y hora en Microsoft SQL Server. Es de código abierto y completamente gratuito.

Ofrece UDF de conversión de fecha y hora fáciles utilizando T-SQL simple (sin CLR) además de tablas de configuración precargadas listas para usar. Y tiene soporte completo DST (horario de verano).

Puede encontrar una lista de todas las zonas horarias admitidas en la tabla "DateTimeUtil.Timezone" (incluida en la base de datos de T-SQL Toolbox).

En su ejemplo, puede usar la siguiente muestra:

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
)

Esto devolverá el valor de fecha y hora local convertido.

Desafortunadamente, es compatible con SQL Server 2008 o posterior solo debido a los nuevos tipos de datos (DATE, TIME, DATETIME2). Pero como se proporciona el código fuente completo, puede ajustar fácilmente las tablas y las UDF reemplazándolas por DATETIME. No tengo un MSSQL 2005 disponible para probar, pero también debería funcionar con MSSQL 2005. En caso de preguntas, solo házmelo saber.


12

Siempre uso este comando TSQL.

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

-- the local time

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

Es muy simple y hace el trabajo.


2
Hay zonas horarias que no son una hora completa de UTC, por lo que usar este DATEPART puede causarle problemas.
Michael Green

44
Con respecto al comentario de Michael Green, puede abordar el problema cambiándolo a SELECT DATEADD (MINUTE, DATEDIFF (MINUTE, GETUTCDATE (), GETDATE ()), @utc).
Usuario registrado

44
Esto no funciona, ya que solo está determinando si la hora actual es DST o no, y luego compara una hora que podría ser DST o no. El uso de su código de ejemplo anterior y la fecha y hora en el Reino Unido actualmente me dice que deberían ser las 6:14 am, sin embargo, noviembre está fuera del horario de verano, por lo que deberían ser las 5:14 am ya que GMT y UTC coinciden.
Matt

Si bien estoy de acuerdo con que esto no aborda la pregunta real, en lo que respecta a esta respuesta, creo que lo siguiente es mejor: SELECT DATEADD (MINUTE, DATEPART (TZoffset, SYSDATETIMEOFFSET ()), @utc)
Eamon

@Ludo Bernaerts: primer uso de milisegundos, segundo: esto no funciona porque el desplazamiento UTC de hoy podría ser diferente del desplazamiento UTC en un momento determinado (horario de verano - horario de verano vs invierno) ...
Dilema

11

Encontré esta respuesta en StackOverflow que proporciona una función definida por el usuario que parece traducir con precisión las fechas y horas

Lo único que necesita modificar es la @offsetvariable en la parte superior para establecerlo en el desplazamiento de la zona horaria del servidor SQL que ejecuta esta función. En mi caso, nuestro servidor SQL usa EST, que es GMT - 5

No es perfecto y probablemente no funcionará en muchos casos, ya que tiene compensaciones de TZ de media hora o 15 minutos (para aquellos que recomendaría una función CLR como la recomendada por Kevin ), sin embargo, funciona lo suficientemente bien para la mayoría de las zonas horarias genéricas en el norte 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

Hay un par de buenas respuestas a una pregunta similar en Stack Overflow. Terminé usando un enfoque T-SQL de la segunda respuesta de Bob Albright para limpiar un desastre causado por un consultor de conversión de datos.

Funcionó para casi todos nuestros datos, pero luego me di cuenta de que su algoritmo solo funciona para fechas tan lejanas como el 5 de abril de 1987 , y teníamos algunas fechas de la década de 1940 que aún no se convertían correctamente. En última instancia, necesitábamos las UTCfechas en nuestra base de datos de SQL Server para alinearnos con un algoritmo en un programa de terceros que utilizaba la API de Java para convertir UTCa la hora local.

Me gusta el CLRejemplo en la respuesta anterior de Kevin Feasel usando el ejemplo de Harsh Chawla, y también me gustaría compararlo con una solución que usa Java, ya que nuestro front end usa Java para hacer la UTCconversión a la hora local.

Wikipedia menciona 8 enmiendas constitucionales diferentes que involucran ajustes de zona horaria antes de 1987, y muchas de ellas están muy localizadas en diferentes estados, por lo que existe la posibilidad de que el CLR y Java puedan interpretarlas de manera diferente. ¿Su código de aplicación front-end utiliza dotnet o Java, o las fechas anteriores a 1987 son un problema para usted?


2

Puede hacerlo fácilmente con un procedimiento almacenado 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);
}

Puede almacenar las zonas horarias disponibles en una tabla:

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

Y este procedimiento almacenado llenará la tabla con las zonas horarias posibles en su servidor.

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 se vuelve malvado cuando debe agregarse WITH PERMISSION_SET = UNSAFE. Algunos entornos no lo permiten como AWS RDS. Y es, bueno, inseguro. Desafortunadamente, no existe una implementación completa de zona horaria .Net que se pueda usar sin unsafepermiso. Mira aquí y aquí .
Frédéric

2

SQL Server versión 2016 resolverá este problema de una vez por todas . Para versiones anteriores, una solución CLR es probablemente la más fácil. O para una regla DST específica (como solo EE. UU.), Una función T-SQL puede ser relativamente simple.

Sin embargo, creo que una solución genérica de T-SQL podría ser posible. Mientras xp_regreadfuncione, intente esto:

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 función T-SQL (compleja) podría usar estos datos para determinar el desplazamiento exacto de todas las fechas durante la regla DST actual.


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
Hola joost Gracias por publicar. Si agrega alguna explicación a su respuesta, puede resultar mucho más fácil de entender.
LowlyDBA

2

Aquí hay una respuesta escrita para una aplicación específica del Reino Unido y basada exclusivamente en SELECT.

  1. Sin compensación de zona horaria (por ejemplo, Reino Unido)
  2. Escrito para el horario de verano que comienza el último domingo de marzo y termina el último domingo de octubre (reglas del Reino Unido)
  3. No aplicable entre la medianoche y la 1 a.m. del día en que comienza el horario de verano. Esto podría corregirse, pero la aplicación para la que fue escrita no lo requiere.

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

Es posible que desee considerar el uso de nombres de partes de fecha larga, en lugar de los cortos. Solo por claridad. Vea el excelente artículo de Aaron Bertrand sobre varios "malos hábitos"
Max Vernon

Además, bienvenido a los Administradores de la base de datos . ¡Haga el recorrido si aún no lo hizo!
Max Vernon el

1
Gracias a todos, comentarios útiles y sugerencias de edición útiles, soy un novato aquí, de alguna manera he logrado acumular 1 punto que es fabuloso :-).
colinp_1

ahora tienes 11.
Max Vernon
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.