Як я можу отримати правильне зміщення між UTC та місцевим часом для дати, що передує до або після DST?


29

Наразі я використовую наступне, щоб отримати локальний час дати з UTC дати:

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

Моя проблема полягає в тому, що якщо літній час перебуває між GetUTCDate()і @utcDateTime, це @localDateTimeзакінчується годиною.

Чи є простий спосіб перетворити з utc в місцевий час для дати, яка не є поточною датою?

Я використовую SQL Server 2005

Відповіді:


18

Найкращий спосіб конвертувати непоточну дату UTC у місцевий час - це використовувати CLR. Сам код легкий; важка частина зазвичай переконує людей, що CLR не є чистим злом чи страшним ...

Одним із численних прикладів, ознайомтеся з публікацією блогу Harsh Chawla .

На жаль, немає нічого вбудованого, який би міг обробляти цей тип перетворення, за винятком CLR-рішень. Ви можете написати T-SQL функцію, яка робить щось подібне, але тоді вам доведеться самостійно реалізувати логіку зміни дати, і я б закликав це не просто.


Враховуючи фактичну складність регіональних варіацій у часі, мовляв, це зробити "чистий T-SQL", мабуть, непросто, ймовірно, це розуміє ;-). Так що так, SQLCLR - єдиний надійний та ефективний засіб виконання цієї операції. +1 для цього. FYI: пов’язана публікація блогу є функціонально правильною, але не дотримується кращих практик, тому, на жаль, неефективна. Функції для перетворення між UTC та локальним часом сервера доступні у бібліотеці SQL # (автор якої я), але не у вільній версії.
Соломон Руцький

1
CLR стає злим, коли його потрібно додати WITH PERMISSION_SET = UNSAFE. У деяких середовищах це не дозволяє, як AWS RDS. І це, ну, небезпечно. На жаль, не існує повного впровадження часового поясу .Net, яке можна використовувати без unsafeдозволу. Дивіться тут і тут .
Фредерік

15

Я розробив і опублікував проект T-SQL Toolbox на кодоплексі, щоб допомогти всім, хто бореться з обробкою датою та часовим поясом в Microsoft SQL Server. Це відкритий код і абсолютно безкоштовний у використанні.

Він пропонує простий UDF для перетворення дати, використовуючи звичайний T-SQL (без CLR), а також заздалегідь заповнені таблиці конфігурації. І має повну підтримку літнього часу (DST).

Список усіх підтримуваних часових поясів можна знайти в таблиці "DateTimeUtil.Timezone" (надається в базі даних T-SQL Toolbox).

У вашому прикладі ви можете використовувати такий зразок:

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
)

Це поверне перетворене місцеве значення часу.

На жаль, він підтримується для SQL Server 2008 або новішої версії лише через новіші типи даних (DATE, TIME, DATETIME2). Але якщо надається повний вихідний код, ви можете легко налаштувати таблиці та UDF, замінивши їх на DATETIME. У мене немає MSSQL 2005 для тестування, але він також повинен працювати з MSSQL 2005. У випадку питань, просто дайте мені знати.


12

Я завжди використовую цю команду TSQL.

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

-- the local time

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

Це дуже просто і це робить свою роботу.


2
Існують часові пояси, які не компенсують повну годину від UTC, тому використання цього DATEPART може спричинити проблеми.
Майкл Грін

4
Щодо коментаря Майкла Гріна, ви можете вирішити цю проблему, змінивши її на ВИБІР DATEADD (MINUTE, DATEDIFF (MINUTE, GETUTCDATE (), GETDATE ()), @utc).
Зареєстрований користувач

4
Це не працює, оскільки ви визначаєте лише те, чи поточний час DST чи ні, тоді порівнюєте час, який може бути DST чи ні. Використовуючи наведений вище приклад код та час у Великобританії, в даний час мені кажуть, що це повинно бути 6:14 ранку, проте листопад знаходиться поза межами DST, тому він повинен бути 5:14 ранку, оскільки GMT та UTC збігаються.
Метт

Хоча я погоджуюсь, що це не стосується фактичного питання, що стосується цієї відповіді, я вважаю, що краще: ВИБІР ДАТАТА (МИНУТА, ДАТЕПАРТ (TZoffset, SYSDATETIMEOFFSET ()), @utc)
Еймон

@Ludo Bernaerts: Перше використання мілісекунд, друге: це не працює, оскільки зміщення UTC сьогодні може бути іншим, ніж зміщення UTC в певний час (літній час - літній та зимовий час) ...
Четверте

11

Цю відповідь я знайшов у StackOverflow, який забезпечує визначену користувачем функцію, яка, схоже, точно переводить дати

Єдине, що вам потрібно змінити - це @offset змінна вгорі, щоб встановити її на зміщення часової зони на сервері SQL, який виконує цю функцію. У моєму випадку наш SQL-сервер використовує EST, а це GMT ​​- 5

Це не ідеально і, ймовірно, не спрацює для багатьох випадків, наприклад, компенсація TZ за півгодини або 15 хвилин (для тих, хто рекомендував би функцію CLR, як рекомендує Кевін ), однак вона працює досить добре для більшості загальних часових поясів на півночі Америка.

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

Є кілька хороших відповідей на подібне питання, яке задають у Stack Overflow. Я завершився використанням T-SQL підходу з другої відповіді Боба Олбрайта щоб очистити безлад, викликаний консультантом із перетворення даних.

Він працював майже за всіма нашими даними, але згодом я зрозумів, що його алгоритм працює лише для дат, починаючи з 5 квітня 1987 року , і у нас були деякі дати 1940-х років, які досі не конвертували належним чином. Зрештою, нам потрібні UTCдати в нашій базі даних SQL Server, щоб вирівнятися з алгоритмом в сторонній програмі, яка використовувала API Java для перетворення UTCв місцевий час.

Мені подобається CLRприклад у відповіді Кевіна Фізеля вище, використовуючи приклад Харса Чаула, і я також хотів би порівняти його з рішенням, яке використовує Java, оскільки наш передній кінець використовує Java для UTCперетворення в локальний час.

Вікіпедія згадує 8 різних конституційних поправок, які передбачають коригування часового поясу до 1987 року, і багато з них дуже локалізовані в різних штатах, тому існує ймовірність, що CLR та Java можуть їх інтерпретувати по-різному. Чи використовується у вашому коді додаткового додатка dotnet або Java, чи дати до 1987 року є проблемою для вас?


2

Ви можете легко зробити це за допомогою процедури зберігання 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);
}

Ви можете зберігати доступні TimeZones у таблиці:

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

І ця збережена процедура заповнить таблицю можливими часовими поясами на вашому сервері.

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 стає злим, коли його потрібно додати WITH PERMISSION_SET = UNSAFE. У деяких середовищах це не дозволяє, як AWS RDS. І це, ну, небезпечно. На жаль, не існує повного впровадження часового поясу .Net, яке можна використовувати без unsafeдозволу. Дивіться тут і тут .
Фредерік

2

Версія SQL Server 2016 вирішить цю проблему раз і назавжди . Для більш ранніх версій рішення CLR, мабуть, найпростіше. Або для конкретного правила DST (як тільки у США) функція T-SQL може бути відносно простою.

Однак я думаю, що загальне рішення T-SQL може бути можливим. Поки xp_regreadпрацює, спробуйте це:

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

(Складна) функція T-SQL може використовувати ці дані для визначення точного зміщення для всіх дат під час поточного правила 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
Привіт Джоост! Дякуємо за публікацію Якщо ви додасте пояснення до своєї відповіді, це може виявитись набагато простіше зрозуміти.
LowlyDBA

2

Ось відповідь, написана для конкретної заявки у Великобританії та заснована виключно на SELECT.

  1. Не зміщено часовий пояс (наприклад, Великобританія)
  2. Призначено для літнього часу, починаючи з останньої неділі березня і закінчуючи в останню неділю жовтня (правила Великобританії)
  3. Не застосовується між півночі та 1 годиною ранку в день, коли починається літній день. Це можна виправити, але додаток, для якого було написано, цього не потребує.

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

Можливо, ви захочете скористатися назви частин із довгими датами замість коротких. Просто для наочності. Дивіться чудову статтю Аарона Бертран про кілька «шкідливих звичок»
Макс Вернон

Також, ласкаво просимо до адміністраторів баз даних - будь ласка, вирушайте в тур, якщо ви ще цього не зробили!
Макс Вернон

1
Дякую всім, корисні коментарі та корисні пропозиції щодо редагування, я тут абсолютно новачок, мені якось вдалося накопичити 1 бал, який fab :-).
colinp_1

тепер у вас є 11.
Макс Вернон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.