SQL Server - конвертировать поле даты в UTC - PullRequest
48 голосов
/ 23 апреля 2010

Я недавно обновил свою систему, чтобы записывать дату / время в формате UTC, как ранее они хранили в качестве местного времени.

Теперь мне нужно преобразовать все локальные сохраненные дату / время в UTC. Мне было интересно, есть ли какая-либо встроенная функция, похожая на метод .NET ConvertTime?

Я пытаюсь избежать написания служебного приложения, чтобы сделать это для меня.

Есть предложения?

Ответы [ 13 ]

74 голосов
/ 19 июня 2011

Я не верю, что приведенный выше код будет работать. Причина в том, что это зависит от разницы между текущей датой в местном и UTC времени. Например, здесь, в Калифорнии, мы сейчас находимся в PDT (тихоокеанское летнее время); разница между этим временем и UTC составляет 7 часов. Предоставленный код, если он будет запущен сейчас, добавит 7 часов к каждой дате, которую требуется преобразовать. Но если сохраненная историческая дата или дата в будущем преобразуется, и эта дата не относится к летнему времени, она все равно добавит 7, когда правильное смещение равно 8. Нижняя строка: вы не можете правильно преобразовать дату / время между часовыми поясами (включая UTC, который не подчиняется летнему времени), глядя только на текущую дату. Вы должны учитывать саму дату, которую вы конвертируете, в отношении того, было ли летнее время действовавшим на эту дату. Кроме того, даты, в которые меняется дневное и стандартное время, изменились (Джордж Буш изменил даты во время своей администрации в США!). Другими словами, любое решение, которое даже ссылается на getdate () или getutcdate (), не работает. Он должен анализировать фактическую дату для конвертации.

37 голосов
/ 07 декабря 2015

В SQL Server 2016 теперь есть встроенная поддержка часовых поясов с оператором AT TIME ZONE.Вы можете объединить их в цепочку для выполнения преобразований:

SELECT YourOriginalDateTime AT TIME ZONE 'Pacific Standard Time' AT TIME ZONE 'UTC'

Или это также будет работать:

SELECT SWITCHOFFSET(YourOriginalDateTime AT TIME ZONE 'Pacific Standard Time', '+00:00')

Любой из них будет интерпретировать ввод в тихоокеанском времени, должным образом учтне DST действует, а затем конвертировать в UTC.Результатом будет datetimeoffset со смещением нуля.

Дополнительные примеры в объявлении CTP.

16 голосов
/ 23 января 2013

Как упомянуто здесь ранее, нет встроенного способа выполнять преобразование даты с учетом правил часовых поясов в SQL Server (по крайней мере, начиная с SQL Server 2012).

По сути, у вас есть три варианта сделать это правильно:

  1. Выполнить преобразование за пределами SQL Server и сохранить результаты в базе данных
  2. Ввести правила смещения часового пояса в отдельную таблицу и создать хранимые процедуры или пользовательские функции для ссылки на таблицу правил для выполнения преобразований. Вы можете найти один подход к этому подходу в SQL Server Central (требуется регистрация)
  3. Вы можете создать SQL CLR UDF; Я опишу подход здесь

Хотя SQL Server не предлагает инструментов для выполнения преобразования дат с учетом правил часовых поясов, платформа .NET делает это, и пока вы можете использовать SQL CLR, вы можете воспользоваться этим.

В Visual Studio 2012 убедитесь, что у вас установлены средства обработки данных (в противном случае проект SQL Server не будет отображаться как опция), и создайте новый проект SQL Server.

Затем добавьте новую пользовательскую функцию SQL CLR C #, назовите ее «ConvertToUtc». VS сгенерирует для вас котельную, которая должна выглядеть примерно так:

public partial class UserDefinedFunctions
{
    [Microsoft.SqlServer.Server.SqlFunction]
    public static SqlString ConvertToUtc()
    {
        // Put your code here
        return new SqlString (string.Empty);
    }
}

Мы хотим внести несколько изменений здесь. Например, мы хотим вернуть SqlDateTime, а не SqlString. Во-вторых, мы хотим сделать что-то полезное. :)

Ваш исправленный код должен выглядеть следующим образом:

public partial class UserDefinedFunctions
{
    [Microsoft.SqlServer.Server.SqlFunction]
    public static SqlDateTime ConvertToUtc(SqlDateTime sqlLocalDate)
    {
        // convert to UTC and use explicit conversion
        // to return a SqlDateTime
        return TimeZone.CurrentTimeZone.ToUniversalTime(sqlLocalDate.Value);
    }
}

На данный момент мы готовы попробовать это. Самый простой способ - использовать встроенную функцию публикации в Visual Studio. Щелкните правой кнопкой мыши проект базы данных и выберите «Опубликовать». Установите соединение с базой данных и имя, а затем либо нажмите «Опубликовать», чтобы вставить код в базу данных, либо нажмите «Создать сценарий», если вы хотите сохранить сценарий для потомков (или отправить биты в производство).

Если у вас есть UDF в базе данных, вы можете увидеть его в действии:

declare @dt as datetime
set @dt = '12/1/2013 1:00 pm'
select dbo.ConvertToUtc(@dt)
11 голосов
/ 24 апреля 2010

Если они все локальные для вас, то вот смещение:

SELECT GETDATE() AS CurrentTime, GETUTCDATE() AS UTCTime

и вы сможете обновить все данные, используя:

UPDATE SomeTable
   SET DateTimeStamp = DATEADD(hh, DATEDIFF(hh, GETDATE(), GETUTCDATE()), DateTimeStamp)

Это сработает, или я упускаю другой угол этой проблемы?

7 голосов
/ 01 ноября 2015

Вот проверенная процедура, которая обновила мою базу данных с локального на время UTC. Единственный вход, необходимый для обновления базы данных, - это ввести количество минут, местное время которых смещено от времени utc в @Offset, и если часовой пояс подлежит корректировке перехода на летнее время, путем установки @ApplyDaylightSavings.

Например, центральное время США будет вводить @ Offset = -360 и @ ApplyDaylightSavings = 1 в течение 6 часов и да, применять корректировку перехода на летнее время.

Поддержка функций базы данных


CREATE FUNCTION [dbo].[GetUtcDateTime](@LocalDateTime DATETIME, @Offset smallint, @ApplyDaylightSavings bit) 
RETURNS DATETIME AS BEGIN 

    --====================================================
    --Calculate the Offset Datetime
    --====================================================
    DECLARE @UtcDateTime AS DATETIME
    SET @UtcDateTime = DATEADD(MINUTE, @Offset * -1, @LocalDateTime)

    IF @ApplyDaylightSavings = 0 RETURN @UtcDateTime;

    --====================================================
    --Calculate the DST Offset for the UDT Datetime
    --====================================================
    DECLARE @Year as SMALLINT
    DECLARE @DSTStartDate AS DATETIME
    DECLARE @DSTEndDate AS DATETIME

    --Get Year
    SET @Year = YEAR(@LocalDateTime)

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

    --Finally add the DST Offset if needed 
    RETURN CASE WHEN @LocalDateTime BETWEEN @DSTStartDate AND @DSTEndDate THEN 
        DATEADD(MINUTE, -60, @UtcDateTime) 
    ELSE 
        @UtcDateTime
    END

END
GO

Сценарий обновления


  1. Сделайте резервную копию перед запуском этого скрипта!
  2. Установить @Offset & @ ApplyDaylightSavings
  3. Запускать только один раз!

begin try
    begin transaction;

    declare @sql nvarchar(max), @Offset smallint, @ApplyDaylightSavings bit;

    set @Offset = -360;             --US Central Time, -300 for US Eastern Time, -480 for US West Coast
    set @ApplyDaylightSavings = 1;  --1 for most US time zones except Arizona which doesn't observer daylight savings, 0 for most time zones outside the US

    declare rs cursor for
    select 'update [' + a.TABLE_SCHEMA + '].[' + a.TABLE_NAME + '] set [' + a.COLUMN_NAME + '] = dbo.GetUtcDateTime([' + a.COLUMN_NAME + '], ' + cast(@Offset as nvarchar) + ', ' + cast(@ApplyDaylightSavings as nvarchar) + ') ;'
    from INFORMATION_SCHEMA.COLUMNS a
        inner join INFORMATION_SCHEMA.TABLES b on a.TABLE_SCHEMA = b.TABLE_SCHEMA and a.TABLE_NAME = b.TABLE_NAME
    where a.DATA_TYPE = 'datetime' and b.TABLE_TYPE = 'BASE TABLE' ;

    open rs;
    fetch next from rs into @sql;
    while @@FETCH_STATUS = 0 begin
        exec sp_executesql @sql;
        print @sql;
        fetch next from rs into @sql;
    end
    close rs;
    deallocate rs;

    commit transaction;
end try
begin catch
    close rs;
    deallocate rs;

    declare @ErrorMessage nvarchar(max), @ErrorSeverity int, @ErrorState int;
    select @ErrorMessage = ERROR_MESSAGE() + ' Line ' + cast(ERROR_LINE() as nvarchar(5)), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE();
    rollback transaction;
    raiserror (@ErrorMessage, @ErrorSeverity, @ErrorState);
end catch
5 голосов
/ 06 сентября 2014

Если вам нужно преобразовать даты, отличные от сегодняшних, в разные часовые пояса, вам придется иметь дело с переходом на летнее время.Я хотел найти решение, которое можно было бы сделать, не беспокоясь о версии базы данных, не используя хранимые функции и что-то, что можно было бы легко перенести в Oracle.

Я думаю, что Уоррен находится на правильном пути с получением правильных дат для дневного времени, но чтобы сделать его более полезным для нескольких часовых поясов и различных правил для стран, и даже для правила, которое изменилось в США в период с 2006 по 2007 год, приведем вариант решения выше.Обратите внимание, что здесь есть не только часовые пояса, но и центральная Европа.Центральная Европа следует за последним воскресеньем апреля и последним воскресеньем октября.Вы также заметите, что в 2006 году в США последовало старое первое воскресенье апреля, последнее воскресенье октября.

Этот код SQL может показаться немного уродливым, но просто скопируйте и вставьте его в SQL Server и попробуйте,Обратите внимание, что есть 3 раздела для лет, часовых поясов и правил.Если вы хотите еще один год, просто добавьте его в год союза.То же самое для другого часового пояса или правила.

select yr, zone, standard, daylight, rulename, strule, edrule, yrstart, yrend,
    dateadd(day, (stdowref + stweekadd), stmonthref) dstlow,
    dateadd(day, (eddowref + edweekadd), edmonthref)  dsthigh
from (
  select yrs.yr, z.zone, z.standard, z.daylight, z.rulename, r.strule, r.edrule, 
    yrs.yr + '-01-01 00:00:00' yrstart,
    yrs.yr + '-12-31 23:59:59' yrend,
    yrs.yr + r.stdtpart + ' ' + r.cngtime stmonthref,
    yrs.yr + r.eddtpart + ' ' + r.cngtime edmonthref,
    case when r.strule in ('1', '2', '3') then case when datepart(dw, yrs.yr + r.stdtpart) = '1' then 0 else 8 - datepart(dw, yrs.yr + r.stdtpart) end
    else (datepart(dw, yrs.yr + r.stdtpart) - 1) * -1 end stdowref,
    case when r.edrule in ('1', '2', '3') then case when datepart(dw, yrs.yr + r.eddtpart) = '1' then 0 else 8 - datepart(dw, yrs.yr + r.eddtpart) end
    else (datepart(dw, yrs.yr + r.eddtpart) - 1) * -1 end eddowref,
    datename(dw, yrs.yr + r.stdtpart) stdow,
    datename(dw, yrs.yr + r.eddtpart) eddow,
    case when r.strule in ('1', '2', '3') then (7 * CAST(r.strule AS Integer)) - 7 else 0 end stweekadd,
    case when r.edrule in ('1', '2', '3') then (7 * CAST(r.edrule AS Integer)) - 7 else 0 end edweekadd
from (
    select '2005' yr union select '2006' yr -- old us rules
    UNION select '2007' yr UNION select '2008' yr UNION select '2009' yr UNION select '2010' yr UNION select '2011' yr
    UNION select '2012' yr UNION select '2013' yr UNION select '2014' yr UNION select '2015' yr UNION select '2016' yr
    UNION select '2017' yr UNION select '2018' yr UNION select '2018' yr UNION select '2020' yr UNION select '2021' yr
    UNION select '2022' yr UNION select '2023' yr UNION select '2024' yr UNION select '2025' yr UNION select '2026' yr
) yrs
cross join (
    SELECT 'ET' zone, '-05:00' standard, '-04:00' daylight, 'US' rulename
    UNION SELECT 'CT' zone, '-06:00' standard, '-05:00' daylight, 'US' rulename
    UNION SELECT 'MT' zone, '-07:00' standard, '-06:00' daylight, 'US' rulename
    UNION SELECT 'PT' zone, '-08:00' standard, '-07:00' daylight, 'US' rulename
    UNION SELECT 'CET' zone, '+01:00' standard, '+02:00' daylight, 'EU' rulename
) z
join (
    SELECT 'US' rulename, '2' strule, '-03-01' stdtpart, '1' edrule, '-11-01' eddtpart, 2007 firstyr, 2099 lastyr, '02:00:00' cngtime
    UNION SELECT 'US' rulename, '1' strule, '-04-01' stdtpart, 'L' edrule, '-10-31' eddtpart, 1900 firstyr, 2006 lastyr, '02:00:00' cngtime
    UNION SELECT  'EU' rulename, 'L' strule, '-03-31' stdtpart, 'L' edrule, '-10-31' eddtpart, 1900 firstyr, 2099 lastyr, '01:00:00' cngtime
) r on r.rulename = z.rulename
    and datepart(year, yrs.yr) between firstyr and lastyr
) dstdates

Для правил используйте 1, 2, 3 или L для первого, второго, третьего или последнего воскресенья.Часть date содержит месяц и, в зависимости от правила, первый день месяца или последний день месяца для типа правила L.

Я поместил вышеупомянутый запрос в представление.Теперь, когда мне нужна дата со смещением часового пояса или преобразованная в время UTC, я просто присоединяюсь к этому представлению и выбираю получение даты в формате даты.Вместо datetime я преобразовал их в datetimeoffset.

select createdon, dst.zone
    , case when createdon >= dstlow and createdon < dsthigh then dst.daylight else dst.standard end pacificoffsettime
    , TODATETIMEOFFSET(createdon, case when createdon >= dstlow and createdon < dsthigh then dst.daylight else dst.standard end) pacifictime
    , SWITCHOFFSET(TODATETIMEOFFSET(createdon, case when createdon >= dstlow and createdon < dsthigh then dst.daylight else dst.standard end), '+00:00')  utctime
from (select '2014-01-01 12:00:00' createdon union select '2014-06-01 12:00:00' createdon) photos
left join US_DAYLIGHT_DATES dst on createdon between yrstart and yrend and zone = 'PT'
2 голосов
/ 07 сентября 2016

Вот моя быстрая и грязная версия. Я знаю, что все мои даты были в восточном часовом поясе США. Вы можете изменить смещение или сделать его умнее, чем нужно. Я делал одноразовую миграцию, так что это было достаточно хорошо.

CREATE FUNCTION [dbo].[ConvertToUtc]
(
    @date datetime
)
RETURNS DATETIME
AS
BEGIN
    -- Declare the return variable here
    DECLARE @utcDate datetime;
    DECLARE @offset int;

    SET @offset = (SELECT CASE WHEN 
                                    @date BETWEEN '1987-04-05 02:00 AM' AND '1987-10-25 02:00 AM'
                                 OR @date BETWEEN '1988-04-03 02:00 AM' AND '1988-10-30 02:00 AM'
                                 OR @date BETWEEN '1989-04-02 02:00 AM' AND '1989-10-29 02:00 AM'
                                 OR @date BETWEEN '1990-04-01 02:00 AM' AND '1990-10-28 02:00 AM'
                                 OR @date BETWEEN '1991-04-07 02:00 AM' AND '1991-10-27 02:00 AM'
                                 OR @date BETWEEN '1992-04-05 02:00 AM' AND '1992-10-25 02:00 AM'
                                 OR @date BETWEEN '1993-04-04 02:00 AM' AND '1993-10-31 02:00 AM'
                                 OR @date BETWEEN '1994-04-03 02:00 AM' AND '1994-10-30 02:00 AM'
                                 OR @date BETWEEN '1995-04-02 02:00 AM' AND '1995-10-29 02:00 AM'
                                 OR @date BETWEEN '1996-04-07 02:00 AM' AND '1996-10-27 02:00 AM'
                                 OR @date BETWEEN '1997-04-06 02:00 AM' AND '1997-10-26 02:00 AM'
                                 OR @date BETWEEN '1998-04-05 02:00 AM' AND '1998-10-25 02:00 AM'
                                 OR @date BETWEEN '1999-04-04 02:00 AM' AND '1999-10-31 02:00 AM'
                                 OR @date BETWEEN '2000-04-02 02:00 AM' AND '2000-10-29 02:00 AM'
                                 OR @date BETWEEN '2001-04-01 02:00 AM' AND '2001-10-28 02:00 AM'
                                 OR @date BETWEEN '2002-04-07 02:00 AM' AND '2002-10-27 02:00 AM'
                                 OR @date BETWEEN '2003-04-06 02:00 AM' AND '2003-10-26 02:00 AM'
                                 OR @date BETWEEN '2004-04-04 02:00 AM' AND '2004-10-31 02:00 AM'
                                 OR @date BETWEEN '2005-04-03 02:00 AM' AND '2005-10-30 02:00 AM'
                                 OR @date BETWEEN '2006-04-02 02:00 AM' AND '2006-10-29 02:00 AM'
                                 OR @date BETWEEN '2007-03-11 02:00 AM' AND '2007-11-04 02:00 AM'
                                 OR @date BETWEEN '2008-03-09 02:00 AM' AND '2008-11-02 02:00 AM'
                                 OR @date BETWEEN '2009-03-08 02:00 AM' AND '2009-11-01 02:00 AM'
                                 OR @date BETWEEN '2010-03-14 02:00 AM' AND '2010-11-07 02:00 AM'
                                 OR @date BETWEEN '2011-03-13 02:00 AM' AND '2011-11-06 02:00 AM'
                                 OR @date BETWEEN '2012-03-11 02:00 AM' AND '2012-11-04 02:00 AM'
                                 OR @date BETWEEN '2013-03-10 02:00 AM' AND '2013-11-03 02:00 AM'
                                 OR @date BETWEEN '2014-03-09 02:00 AM' AND '2014-11-02 02:00 AM'
                                 OR @date BETWEEN '2015-03-08 02:00 AM' AND '2015-11-01 02:00 AM'
                                 OR @date BETWEEN '2016-03-13 02:00 AM' AND '2016-11-06 02:00 AM'
                                 OR @date BETWEEN '2017-03-12 02:00 AM' AND '2017-11-05 02:00 AM'
                                 OR @date BETWEEN '2018-03-11 02:00 AM' AND '2018-11-04 02:00 AM'
                                 OR @date BETWEEN '2019-03-10 02:00 AM' AND '2019-11-03 02:00 AM'
                                 OR @date BETWEEN '2020-03-08 02:00 AM' AND '2020-11-01 02:00 AM'
                                 OR @date BETWEEN '2021-03-14 02:00 AM' AND '2021-11-07 02:00 AM'
                               THEN 4
                               ELSE 5 END);

    SELECT @utcDate = DATEADD(hh, @offset, @date)
    RETURN @utcDate;

END
1 голос
/ 20 мая 2016

Если я не пропустил что-то выше (возможно), все вышеперечисленные методы имеют недостатки в том, что они не учитывают перекрытие при переключении с перехода на летнее время (например, EDT) на стандартное время (например, EST). Пример (очень подробный):

[1] EDT 2016-11-06 00:59 - UTC 2016-11-06 04:59
[2] EDT 2016-11-06 01:00 - UTC 2016-11-06 05:00
[3] EDT 2016-11-06 01:30 - UTC 2016-11-06 05:30
[4] EDT 2016-11-06 01:59 - UTC 2016-11-06 05:59
[5] EST 2016-11-06 01:00 - UTC 2016-11-06 06:00
[6] EST 2016-11-06 01:30 - UTC 2016-11-06 06:30
[7] EST 2016-11-06 01:59 - UTC 2016-11-06 06:59
[8] EST 2016-11-06 02:00 - UTC 2016-11-06 07:00

Простые смещения часов, основанные на дате и времени, не уменьшат его. Если вы не знаете, было ли местное время записано в EDT или EST между 01:00 и 01:59, у вас не будет подсказки! Давайте используем 01:30, например: если вы найдете более поздние времена в диапазоне от 01:31 до 01:59 ДО, вы не будете знать, является ли 01:30, на которую вы смотрите, [3 или [6. В этом случае вы можете получить правильное время UTC с небольшим количеством кода, просматривая предыдущие записи (не весело в SQL), и это ЛУЧШИЙ случай ...

Скажем, у вас записано следующее местное время, и вы не посвятили немного времени для обозначения EDT или EST:

                     UTC time         UTC time         UTC time
                     if [2] and [3]   if [2] and [3]   if [2] before
local time           before switch    after switch     and [3] after
[1] 2016-11-06 00:43     04:43         04:43           04:43
[2] 2016-11-06 01:15     05:15         06:15           05:15
[3] 2016-11-06 01:45     05:45         06:45           06:45
[4] 2016-11-06 03:25     07:25         07:25           07:25

Времена [2] и [3] могут быть в 5 часов утра, в 6 часов утра или один в 5 часов утра, а другой - в 6 часов утра. , , Другими словами: вы находитесь в шланге и должны выбросить все показания с 01:00:00 до 01:59:59. В этом случае абсолютно невозможно определить фактическое время UTC!

0 голосов
/ 03 мая 2019

В случае, если кто-то наткнется на это и использует ответ Криса Барлоу, есть опечатка, где 2018 указан в списке дважды, а второй раз должен быть 2019. В частности, эта строка:

   UNION select '2017' yr UNION select '2018' yr UNION select '2018' yr UNION select '2020' yr UNION select '2021' yr

Извините, у меня недостаточно очков, чтобы прокомментировать ответ Криса. Кроме этого, ответ отличный!

0 голосов
/ 08 марта 2019

Ответ Мэтта Джонсона - это абсолютно лучший подход в Microsoft SQL Server 2016+. Однако его предложение использовать AT TIME ZONE имеет одну уязвимость, о которой следует знать. Невозможно определить правильное смещение от UTC для часа, когда заканчивается летнее время. Насколько я знаю, нет никакого способа решить эту проблему. Рассмотрим пример ниже.

--Assume that the value of input_date = '2018-11-04 01:00:00'
--Assume that dates in this field are written in Central Standard Time

select input_date at time zone 'Central Standard Time'

>> '2018-11-04 01:00:00 -05:00'

По умолчанию Microsoft SQL Server предполагает, что смещение значения input_date равно -05: 00. Однако это время может фактически представлять смещение -05: 00 ИЛИ -06: 00 в зависимости от того, закончился ли DST или нет. Нет никакого способа узнать наверняка без сопутствующего смещения, поэтому Microsoft SQL Server делает предположение, которое может быть или не быть правильным.

...