SQL Server - Convert date field to UTC

前端 未结 12 572
故里飘歌
故里飘歌 2020-12-23 13:39

I have recently updated my system to record date/times as UTC as previously they were storing as local time.

I now need to convert all the local stored date/times to

相关标签:
12条回答
  • 2020-12-23 13:53

    Here is a tested procedure that upgraded my database from local to utc time. The only input required to upgrade a database is to enter the number of minutes local time is offset from utc time into @Offset and if the timezone is subject to daylight savings adjustments by setting @ApplyDaylightSavings.

    For example, US Central Time would enter @Offset=-360 and @ApplyDaylightSavings=1 for 6 hours and yes apply daylight savings adjustment.

    Supporting Database Function


    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
    

    Upgrade Script


    1. Make a backup before running this script!
    2. Set @Offset & @ApplyDaylightSavings
    3. Only run once!

    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
    
    0 讨论(0)
  • 2020-12-23 13:58

    I'm a bit late to the game but I needed to do something like this on SQL 2012, I haven't fully tested it yet but here is what I came up with.

    CREATE FUNCTION SMS.fnConvertUTC
    (
        @DateCST datetime
    )
    RETURNS DATETIME
    AS
    BEGIN
        RETURN 
            CASE 
            WHEN @DateCST 
                BETWEEN 
                    CASE WHEN @DateCST > '2007-01-01' 
                    THEN CONVERT(DATETIME, CONVERT(VARCHAR,YEAR(@DateCST)) + '-MAR-14 02:00') - DATEPART(DW,CONVERT(VARCHAR,YEAR(@DateCST)) + '-MAR-14 02:00' ) + 1
                    ELSE CONVERT(DATETIME, CONVERT(VARCHAR,YEAR(@DateCST)) + '-APR-07 02:00') - DATEPART(DW,CONVERT(VARCHAR,YEAR(@DateCST)) + '-APR-07 02:00' ) + 1 END
                AND
                    CASE WHEN @DateCST > '2007-01-01' 
                    THEN CONVERT(DATETIME, CONVERT(VARCHAR,YEAR(@DateCST)) + '-NOV-07 02:00') - DATEPART(DW,CONVERT(VARCHAR,YEAR(@DateCST)) + '-NOV-07 02:00' ) + 1
                    ELSE CONVERT(DATETIME, CONVERT(VARCHAR,YEAR(@DateCST)) + '-OCT-31 02:00') - DATEPART(DW,CONVERT(VARCHAR,YEAR(@DateCST)) + '-OCT-31 02:00' ) + 1 END
            THEN DATEADD(HOUR,4,@DateCST)
            ELSE DATEADD(HOUR,5,@DateCST) 
            END
    END
    

    Above someone posted a static list DST dates so I wrote the below query to compare this code's output to that list... so far it looks correct.

    ;WITH DT AS 
    ( 
        SELECT MyDate = GETDATE() 
        UNION ALL 
        SELECT MyDate = DATEADD(YEAR,-1,MyDate) FROM DT
        WHERE DATEADD(YEAR,-1,MyDate) > DATEADD(YEAR, -30, GETDATE())
    )
    SELECT 
        SpringForward = CASE 
            WHEN MyDate > '2007-01-01' 
            THEN CONVERT(DATETIME, CONVERT(VARCHAR,YEAR(MyDate)) + '-MAR-14 02:00') - DATEPART(DW,CONVERT(VARCHAR,YEAR(MyDate)) + '-MAR-14 02:00' ) + 1
            ELSE CONVERT(DATETIME, CONVERT(VARCHAR,YEAR(MyDate)) + '-APR-07 02:00') - DATEPART(DW,CONVERT(VARCHAR,YEAR(MyDate)) + '-APR-07 02:00' ) + 1 END
    ,   FallBackward  = CASE 
            WHEN MyDate > '2007-01-01' 
            THEN CONVERT(DATETIME, CONVERT(VARCHAR,YEAR(MyDate)) + '-NOV-07 02:00') - DATEPART(DW,CONVERT(VARCHAR,YEAR(MyDate)) + '-NOV-07 02:00' ) + 1
            ELSE CONVERT(DATETIME, CONVERT(VARCHAR,YEAR(MyDate)) + '-OCT-31 02:00') - DATEPART(DW,CONVERT(VARCHAR,YEAR(MyDate)) + '-OCT-31 02:00' ) + 1 END
    FROM DT
    ORDER BY 1 DESC
    
    SpringForward      FallBackward
    ----------------   ----------------
    2020-03-08 02:00   2020-11-01 02:00
    2019-03-10 02:00   2019-11-03 02:00
    2018-03-11 02:00   2018-11-04 02:00
    2017-03-12 02:00   2017-11-05 02:00
    2016-03-13 02:00   2016-11-06 02:00
    2015-03-08 02:00   2015-11-01 02:00
    2014-03-09 02:00   2014-11-02 02:00
    2013-03-10 02:00   2013-11-03 02:00
    2012-03-11 02:00   2012-11-04 02:00
    2011-03-13 02:00   2011-11-06 02:00
    2010-03-14 02:00   2010-11-07 02:00
    2009-03-08 02:00   2009-11-01 02:00
    2008-03-09 02:00   2008-11-02 02:00
    2007-03-11 02:00   2007-11-04 02:00
    2006-04-02 02:00   2006-10-29 02:00
    2005-04-03 02:00   2005-10-30 02:00
    2004-04-04 02:00   2004-10-31 02:00
    2003-04-06 02:00   2003-10-26 02:00
    2002-04-07 02:00   2002-10-27 02:00
    2001-04-01 02:00   2001-10-28 02:00
    2000-04-02 02:00   2000-10-29 02:00
    1999-04-04 02:00   1999-10-31 02:00
    1998-04-05 02:00   1998-10-25 02:00
    1997-04-06 02:00   1997-10-26 02:00
    1996-04-07 02:00   1996-10-27 02:00
    1995-04-02 02:00   1995-10-29 02:00
    1994-04-03 02:00   1994-10-30 02:00
    1993-04-04 02:00   1993-10-31 02:00
    1992-04-05 02:00   1992-10-25 02:00
    1991-04-07 02:00   1991-10-27 02:00
    
    (30 row(s) affected)
    
    0 讨论(0)
  • 2020-12-23 13:59

    Unless I missed something above (possible), all of the methods above are flawed in that they don't take the overlap when switching from daylight savings (say EDT) to standard time (say EST) into account. A (very verbose) example:

    [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
    

    Simple hour offsets based on date and time won't cut it. If you don't know if the local time was recorded in EDT or EST between 01:00 and 01:59, you won't have a clue! Let's use 01:30 for example: if you find later times in the range 01:31 through 01:59 BEFORE it, you won't know if the 01:30 you're looking at is [3 or [6. In this case, you can get the correct UTC time with a bit of coding be looking at previous entries (not fun in SQL), and this is the BEST case...

    Say you have the following local times recorded, and didn't dedicate a bit to indicate EDT or 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
    

    Times [2] and [3] may be in the 5 AM timeframe, the 6 AM timeframe, or one in the 5 AM and the other in the 6 AM timeframe . . . In other words: you are hosed, and must throw out all readings between 01:00:00 and 01:59:59. In this circumstance, there is absolutely no way to resolve the actual UTC time!

    0 讨论(0)
  • 2020-12-23 14:05

    As mentioned here previously, there is no build-in way to perform time zone rules aware date conversion in SQL Server (at least as of SQL Server 2012).

    You have essentially three choices to do this right:

    1. Perform the conversion outside of SQL Server and store results in the database
    2. Introduce time zone offset rules in a standalone table and create stored procedures or UDFs to reference the rules table to perform conversions. You can find one take on this approach over at SQL Server Central (registration required)
    3. You can create a SQL CLR UDF; I will describe the approach here

    While SQL Server does not offer tools to perform time zone rules aware date conversion, the .NET framework does, and as long as you can use SQL CLR, you can take advantage of that.

    In Visual Studio 2012, make sure you have the data tools installed (otherwise, SQL Server project won't show up as an option), and create a new SQL Server project.

    Then, add a new SQL CLR C# User Defined Function, call it "ConvertToUtc". VS will generate boiler plate for you that should look something like this:

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

    We want to make several changes here. For one, we want to return a SqlDateTime rather than a SqlString. Secondly, we want to do something useful. :)

    Your revised code should look like this:

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

    At this point, we are ready to try it out. The simplest way is to use the built-in Publish facility in Visual Studio. Right-click on the database project and select "Publish". Set up your database connection and name, and then either click "Publish" to push the code into the database or click "Generate Script" if you'd like to store the script for posterity (or to push the bits into production).

    Once you have the UDF in the database, you can see it in action:

    declare @dt as datetime
    set @dt = '12/1/2013 1:00 pm'
    select dbo.ConvertToUtc(@dt)
    
    0 讨论(0)
  • 2020-12-23 14:06

    With SQL Server 2016, there is now built-in support for time zones with the AT TIME ZONE statement. You can chain these to do conversions:

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

    Or, this would work as well:

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

    Either of these will interpret the input in Pacific time, properly account for whether or not DST is in effect, and then convert to UTC. The result will be a datetimeoffset with a zero offset.

    More examples in the CTP announcement.

    0 讨论(0)
  • 2020-12-23 14:06

    Depending on how far back you need to go, you can build a table of daylight savings times and then join the table and do a dst-sensitive conversion. This particular one converts from EST to GMT (i.e. uses offsets of 5 and 4).

    select createdon, dateadd(hour, case when dstlow is null then 5 else 4 end, createdon) as gmt
    from photos
    left outer join (
              SELECT {ts '2009-03-08 02:00:00'} as dstlow, {ts '2009-11-01 02:00:00'} as dsthigh
    UNION ALL SELECT {ts '2010-03-14 02:00:00'} as dstlow, {ts '2010-11-07 02:00:00'} as dsthigh
    UNION ALL SELECT {ts '2011-03-13 02:00:00'} as dstlow, {ts '2011-11-06 02:00:00'} as dsthigh
    UNION ALL SELECT {ts '2012-03-11 02:00:00'} as dstlow, {ts '2012-11-04 02:00:00'} as dsthigh
    UNION ALL SELECT {ts '2013-03-10 02:00:00'} as dstlow, {ts '2013-11-03 02:00:00'} as dsthigh
    UNION ALL SELECT {ts '2014-03-09 02:00:00'} as dstlow, {ts '2014-11-02 02:00:00'} as dsthigh
    UNION ALL SELECT {ts '2015-03-08 02:00:00'} as dstlow, {ts '2015-11-01 02:00:00'} as dsthigh
    UNION ALL SELECT {ts '2016-03-13 02:00:00'} as dstlow, {ts '2016-11-06 02:00:00'} as dsthigh
    UNION ALL SELECT {ts '2017-03-12 02:00:00'} as dstlow, {ts '2017-11-05 02:00:00'} as dsthigh
    UNION ALL SELECT {ts '2018-03-11 02:00:00'} as dstlow, {ts '2018-11-04 02:00:00'} as dsthigh
        ) dst
        on createdon >= dstlow and createdon < dsthigh
    
    0 讨论(0)
提交回复
热议问题