Comment puis-je obtenir le décalage correct entre l'heure UTC et l'heure locale pour une date antérieure ou postérieure à l'heure d'été?


29

J'utilise actuellement ce qui suit pour obtenir un datetime local à partir d'un datetime UTC:

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

Mon problème est que si l'heure d'été se produit entre GetUTCDate()et @utcDateTime, cela @localDateTimese termine par une heure de repos.

Existe-t-il un moyen simple de convertir l'utc en heure locale pour une date qui n'est pas la date actuelle?

J'utilise SQL Server 2005

Réponses:


18

La meilleure façon de convertir une date UTC non actuelle en heure locale est d'utiliser le CLR. Le code lui-même est facile; la partie difficile est généralement de convaincre les gens que le CLR n'est pas du mal pur ou effrayant ...

Pour l'un des nombreux exemples, consultez le blog de Harsh Chawla sur le sujet .

Malheureusement, rien de intégré ne peut gérer ce type de conversion, à l'exception des solutions basées sur CLR. Vous pouvez écrire une fonction T-SQL qui fait quelque chose comme ça, mais alors vous devrez implémenter vous-même la logique de changement de date, et j'appellerais cela décidément pas facile.


Compte tenu de la complexité réelle des variations régionales au fil du temps, dire qu'il n'est "décidément pas facile" de tenter cela en T-SQL pur est probablement le sous-estimer ;-). Alors oui, SQLCLR est le seul moyen fiable et efficace d'effectuer cette opération. +1 pour cela. Pour info: le billet de blog lié est fonctionnellement correct mais ne suit pas les meilleures pratiques et est donc malheureusement inefficace. Les fonctions de conversion entre UTC et l'heure locale du serveur sont disponibles dans la bibliothèque SQL # (dont je suis l'auteur), mais pas dans la version gratuite.
Solomon Rutzky

1
CLR devient mauvais quand il faut l'ajouter WITH PERMISSION_SET = UNSAFE. Certains environnements ne le permettent pas comme AWS RDS. Et c'est, eh bien, dangereux. Malheureusement, il n'y a pas d'implémentation complète du fuseau horaire .Net utilisable sans unsafeautorisation. Voir ici et ici .
Frédéric

15

J'ai développé et publié le projet T-SQL Toolbox sur codeplex pour aider quiconque éprouve des difficultés avec la gestion du datetime et du fuseau horaire dans Microsoft SQL Server. C'est open source et totalement gratuit à utiliser.

Il offre des UDF de conversion datetime faciles en utilisant du T-SQL simple (pas de CLR) en plus des tableaux de configuration préremplis prêts à l'emploi. Et il a une prise en charge complète de l'heure d'été (DST).

Une liste de tous les fuseaux horaires pris en charge se trouve dans le tableau "DateTimeUtil.Timezone" (fourni dans la base de données T-SQL Toolbox).

Dans votre exemple, vous pouvez utiliser l'exemple suivant:

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
)

Cela renverra la valeur datetime locale convertie.

Malheureusement, il est pris en charge pour SQL Server 2008 ou version ultérieure uniquement en raison de nouveaux types de données (DATE, TIME, DATETIME2). Mais comme le code source complet est fourni, vous pouvez facilement ajuster les tables et les FDU en les remplaçant par DATETIME. Je n'ai pas de MSSQL 2005 disponible pour les tests, mais il devrait également fonctionner avec MSSQL 2005. En cas de questions, faites le moi savoir.


12

J'utilise toujours cette commande TSQL.

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

-- the local time

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

C'est très simple et ça fait l'affaire.


2
Il y a des fuseaux horaires qui ne sont pas décalés d'une heure complète par rapport à l'UTC, donc l'utilisation de ce DATEPART peut vous poser des problèmes.
Michael Green

4
Concernant le commentaire de Michael Green, vous pouvez résoudre le problème en le changeant en SELECT DATEADD (MINUTE, DATEDIFF (MINUTE, GETUTCDATE (), GETDATE ()), @utc).
Utilisateur enregistré le

4
Cela ne fonctionne pas car vous déterminez uniquement si l'heure actuelle est DST ou non, puis comparez une heure qui pourrait être DST ou non. L'utilisation de votre exemple de code et de datetime ci-dessus au Royaume-Uni me dit actuellement qu'il devrait être 6 h 14, mais novembre est en dehors de l'heure d'été, il devrait donc être 5 h 14 car GMT et UTC coïncident.
Matt

Bien que je sois d'accord, cela ne répond pas à la question réelle, en ce qui concerne cette réponse, je pense que la chose suivante est meilleure: SELECT DATEADD (MINUTE, DATEPART (TZoffset, SYSDATETIMEOFFSET ()), @utc)
Eamon

@Ludo Bernaerts: Première utilisation millisecondes, deuxième: cela ne fonctionne pas car le décalage UTC aujourd'hui peut être différent du décalage UTC à un certain moment (heure d'été - heure d'été vs heure d'hiver) ...
Quandary

11

J'ai trouvé cette réponse sur StackOverflow qui fournit une fonction définie par l'utilisateur qui semble traduire avec précision les heures

La seule chose que vous devez modifier est la @offsetvariable en haut pour la définir sur le décalage de fuseau horaire du serveur SQL exécutant cette fonction. Dans mon cas, notre serveur SQL utilise EST, qui est GMT - 5

Ce n'est pas parfait et ne fonctionnera probablement pas dans de nombreux cas, comme les décalages TZ d'une demi-heure ou de 15 minutes (pour ceux que je recommanderais une fonction CLR comme Kevin recommandé ), mais cela fonctionne assez bien pour la plupart des fuseaux horaires génériques du Nord. Amérique.

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

Il y a quelques bonnes réponses à une question similaire posée sur Stack Overflow. J'ai fini par utiliser une approche T-SQL de la deuxième réponse de Bob Albright pour nettoyer un gâchis causé par un consultant en conversion de données.

Cela a fonctionné pour presque toutes nos données, mais j'ai ensuite réalisé que son algorithme ne fonctionnait que pour des dates remontant au 5 avril 1987 et que nous avions des dates des années 40 qui ne se convertissaient toujours pas correctement. Nous avons finalement eu besoin des UTCdates de notre base de données SQL Server pour nous aligner sur un algorithme dans un programme tiers qui utilisait l'API Java pour effectuer une conversion de UTCl'heure locale à l'heure locale.

J'aime l' CLRexemple de la réponse de Kevin Feasel ci-dessus en utilisant l'exemple de Harsh Chawla, et j'aimerais également le comparer à une solution qui utilise Java, car notre frontal utilise Java pour effectuer la UTCconversion en heure locale.

Wikipédia mentionne 8 modifications constitutionnelles différentes qui impliquent des ajustements de fuseau horaire avant 1987, et bon nombre d'entre elles sont très localisées dans différents États, il est donc possible que le CLR et Java les interprètent différemment. Votre code d'application frontale utilise-t-il dotnet ou Java, ou les dates antérieures à 1987 sont-elles un problème pour vous?


2

Vous pouvez facilement le faire avec une procédure stockée 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);
}

Vous pouvez stocker les TimeZones disponibles dans une table:

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

Et cette procédure stockée remplira le tableau avec les fuseaux horaires possibles sur votre serveur.

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 devient mauvais quand il faut l'ajouter WITH PERMISSION_SET = UNSAFE. Certains environnements ne le permettent pas comme AWS RDS. Et c'est, eh bien, dangereux. Malheureusement, il n'y a pas d'implémentation complète du fuseau horaire .Net utilisable sans unsafeautorisation. Voir ici et ici .
Frédéric

2

SQL Server version 2016 résoudra ce problème une fois pour toutes . Pour les versions antérieures, une solution CLR est probablement la plus simple. Ou pour une règle DST spécifique (comme aux États-Unis uniquement), une fonction T-SQL peut être relativement simple.

Cependant, je pense qu'une solution générique T-SQL pourrait être possible. Tant que xp_regreadça marche, essayez ceci:

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

Une fonction T-SQL (complexe) pourrait utiliser ces données pour déterminer le décalage exact pour toutes les dates pendant la règle DST actuelle.


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
Salut Joost! Merci d'avoir posté. Si vous ajoutez des explications à votre réponse, cela peut s'avérer beaucoup plus facile à comprendre.
LowlyDBA

2

Voici une réponse écrite pour une application spécifique au Royaume-Uni et basée uniquement sur SELECT.

  1. Aucun décalage de fuseau horaire (par exemple au Royaume-Uni)
  2. Écrit pour l'heure d'été à partir du dernier dimanche de mars et se terminant le dernier dimanche d'octobre (règles britanniques)
  3. Non applicable entre minuit et 1 h du matin le jour, l'heure d'été commence. Cela pourrait être corrigé mais l'application pour laquelle il a été écrit ne l'exige pas.

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

Vous voudrez peut-être envisager d'utiliser les noms de pièces de date longue, au lieu des noms courts. Juste pour plus de clarté. Voir l'excellent article d'Aaron Bertrand sur plusieurs "mauvaises habitudes"
Max Vernon

Bienvenue également aux administrateurs de bases de données - veuillez faire le tour si vous ne l'avez pas déjà fait!
Max Vernon

1
Merci à tous, commentaires utiles et suggestions de modifications utiles, je suis un débutant total ici, j'ai réussi à accumuler 1 point, ce qui est fabuleux :-).
colinp_1

vous en avez maintenant 11.
Max Vernon
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.