Как я могу получить правильное смещение между UTC и местным временем для даты до или после летнего времени?

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 не является чистым злом или страшным ...

Один из множества примеров можно найти в блоге Харш Чавла на эту тему .

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

Кевин Физель
источник
Учитывая реальную сложность региональных различий с течением времени, сказать, что «это непросто» сделать это в чистом T-SQL, вероятно, недооценивает это ;-). Так что да, SQLCLR является единственным надежным и эффективным средством выполнения этой операции. +1 за это. К вашему сведению: ссылка на блоге функционально верна, но не соответствует передовым методам, поэтому, к сожалению, неэффективна. Функции для преобразования между UTC и локальным временем сервера доступны в библиотеке SQL # (автором которой я являюсь), но не в бесплатной версии.
Соломон Руцкий,
1
CLR становится злым, когда его нужно добавить WITH PERMISSION_SET = UNSAFE. Некоторые среды не позволяют это, как AWS RDS. И это, ну, небезопасно. К сожалению, нет полной реализации часового пояса .Net, которую можно использовать без unsafeразрешения. Смотрите здесь и здесь .
Фредерик
15

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

Он предлагает простые функции преобразования даты и времени с использованием простого 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). Но так как предоставляется полный исходный код, вы можете легко настроить таблицы и пользовательские функции, заменив их на 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
Что касается комментария Майкла Грина, вы можете решить проблему, изменив ее на SELECT DATEADD (MINUTE, DATEDIFF (MINUTE, GETUTCDATE (), GETDATE ()), @utc).
Зарегистрированный пользователь
4
Это не работает, так как вы только определяете, является ли текущее время летним или нет, затем сравниваете время, которое может быть летним или нет. Использование приведенного выше примера кода и даты и времени в Великобритании в настоящее время говорит мне, что это должно быть 6:14 утра, однако ноябрь - вне летнего времени, поэтому это должно быть 5:14 утра, поскольку GMT и UTC совпадают.
Мэтт
Хотя я согласен, что это не относится к актуальному вопросу, поскольку этот ответ касается, я думаю, что лучше следующее: ВЫБЕРИТЕ DATEADD (MINUTE, DATEPART (TZoffset, SYSDATETIMEOFFSET ()), @utc)
Eamon
@ Людо Бернаертс: во-первых, используйте миллисекунды, во-вторых: это не работает, потому что смещение 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

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

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

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

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

kkarns
источник
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);
}

Вы можете хранить доступные часовые пояса в таблице:

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
colinp_1
источник
Вы можете рассмотреть возможность использования длинных имен деталей вместо коротких. Просто для наглядности. Посмотрите отличную статью Аарона Бертрана о нескольких «вредных привычках»
Макс Вернон,
Кроме того, добро пожаловать к администраторам баз данных - пожалуйста, возьмите тур, если вы еще этого не сделали!
Макс Вернон
1
Спасибо всем, полезные комментарии и полезные предложения по редактированию, я здесь новичок, каким-то образом мне удалось набрать 1 очко, что просто потрясающе :-).
colinp_1
теперь у вас 11
Макс Вернон