ฉันจะได้รับการชดเชยที่ถูกต้องระหว่างเวลา 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 ไม่ใช่ความชั่วร้ายที่น่ากลัวหรือน่ากลัว ...

สำหรับหนึ่งในหลายตัวอย่างให้ตรวจสอบโพสต์บล็อกรุนแรง Chawla ในหัวข้อ

น่าเสียดายที่ไม่มีสิ่งใดในตัวที่สามารถรองรับการแปลงประเภทนี้ได้บันทึกสำหรับโซลูชันที่ใช้ CLR คุณสามารถเขียนฟังก์ชัน T-SQL ซึ่งทำสิ่งนี้ แต่คุณต้องใช้ตรรกะการเปลี่ยนวันด้วยตัวเองและฉันจะเรียกมันว่าไม่ใช่เรื่องง่าย


เมื่อพิจารณาถึงความซับซ้อนที่แท้จริงของความผันแปรในภูมิภาคเมื่อเวลาผ่านไปการพูดว่า "ไม่ง่ายอย่างแน่นอน" ในการลองใช้ T-SQL ล้วนๆอาจเป็นไปได้ ;-) ใช่ SQLCLR เป็นวิธีการที่เชื่อถือได้และมีประสิทธิภาพเพียงอย่างเดียวในการดำเนินการนี้ +1 สำหรับสิ่งนั้น FYI: การโพสต์บล็อกที่เชื่อมโยงนั้นถูกต้องตามหน้าที่ แต่ไม่เป็นไปตามแนวปฏิบัติที่ดีที่สุดดังนั้นจึงไม่มีประสิทธิภาพ ฟังก์ชั่นสำหรับการแปลงระหว่างเวลา UTC กับเวลาท้องถิ่นของเซิร์ฟเวอร์มีอยู่ในไลบรารีSQL # (ซึ่งฉันเป็นผู้เขียน) แต่ไม่ใช่ในรุ่นฟรี
โซโลมอน Rutzky

1
CLR WITH PERMISSION_SET = UNSAFEได้รับความชั่วร้ายที่จะต้องมีการเพิ่ม บางสภาพแวดล้อมไม่อนุญาตเช่น AWS RDS และมันก็เป็นอย่างดีไม่ปลอดภัย น่าเสียดายที่ไม่มีการใช้งาน. โซนเวลาแบบสมบูรณ์ซึ่งสามารถใช้งานได้โดยไม่unsafeได้รับอนุญาต ดูที่นี่และที่นี่
Frédéric

15

ฉันได้พัฒนาและเผยแพร่โครงการT-SQL Toolboxบน codeplex เพื่อช่วยให้ทุกคนที่ต่อสู้กับ datetime และ timezone handling ใน Microsoft SQL Server เป็นโอเพ่นซอร์สและใช้งานได้อย่างสมบูรณ์ฟรี

มันมี UDF การแปลงวันที่และเวลาที่ง่ายโดยใช้ T-SQL ธรรมดา (ไม่มี CLRs) นอกเหนือจากตารางการกำหนดค่าที่กรอกไว้ล่วงหน้า และรองรับการปรับเวลาตามฤดูกาล (เต็มเวลา)

รายการของเขตเวลาที่สนับสนุนทั้งหมดสามารถพบได้ในตาราง "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 นี้อาจทำให้คุณมีปัญหา
Michael Green

4
เกี่ยวกับความคิดเห็นของ Michael Green คุณสามารถแก้ไขปัญหานี้ได้โดยเปลี่ยนเป็น SELECT DATEADD (MINUTE, DATEDIFF (MINUTE, GETUTCDATE (), GETDATE (), @utc)
สมาชิกที่ลงทะเบียน

4
สิ่งนี้ไม่ทำงานตามที่คุณกำลังพิจารณาว่าเวลาปัจจุบันเป็น DST หรือไม่จากนั้นเปรียบเทียบเวลาที่อาจเป็น DST หรือไม่ การใช้โค้ดตัวอย่างข้างต้นและวันที่และเวลาในสหราชอาณาจักรในปัจจุบันบอกฉันว่าควรเป็น 6:14 น. อย่างไรก็ตามพฤศจิกายนอยู่นอก DST ดังนั้นควรเป็น 5:14 น. ตามเวลา GMT และ UTC ตรง
แมตต์

ขณะที่ฉัน aggree นี้ไม่ได้อยู่ที่คำถามที่เกิดขึ้นจริงเท่าที่คำตอบนี้เป็นห่วงผมคิดว่าต่อไปนี้จะดีกว่า: เลือก DATEADD (นาที DATEPART (TZoffset, SYSDATETIMEOFFSET ()), @utc)
เอมอน

@Ludo Bernaerts: การใช้งานครั้งแรกมิลลิวินาทีที่สอง: สิ่งนี้ไม่ทำงานเนื่องจาก UTC offset ในวันนี้อาจแตกต่างจาก UTC-offset ในช่วงเวลาหนึ่ง (การปรับเวลาตามฤดูกาล - ฤดูร้อนและฤดูหนาว)
Quandary

11

ฉันพบคำตอบนี้ใน StackOverflow ที่มีฟังก์ชั่นที่ผู้ใช้กำหนดซึ่งปรากฏขึ้นเพื่อแปลชุดข้อมูลอย่างถูกต้อง

สิ่งเดียวที่คุณต้องแก้ไขคือ@offsetตัวแปรที่ด้านบนเพื่อตั้งค่าให้เป็นเขตเวลาชดเชยของเซิร์ฟเวอร์ SQL ที่ใช้ฟังก์ชันนี้ ในกรณีของฉันเซิร์ฟเวอร์ SQL ของเราใช้ EST ซึ่งเป็น GMT - 5

มันไม่สมบูรณ์แบบและอาจไม่สามารถใช้ได้กับหลาย ๆ กรณีเช่นมีการชดเชย TZ ครึ่งชั่วโมงหรือ 15 นาที (สำหรับผู้ที่ฉันแนะนำให้ใช้ฟังก์ชัน CLR เหมือนที่Kevin แนะนำ ) แต่มันทำงานได้ดีพอสำหรับเขตเวลาทั่วไปส่วนใหญ่ในภาคเหนือ สหรัฐอเมริกา

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 จากคำตอบที่สองโดย Bob Albrightเพื่อทำความสะอาดระเบียบที่เกิดจากที่ปรึกษาด้านการแปลงข้อมูล

มันใช้งานได้กับข้อมูลเกือบทั้งหมดของเรา แต่หลังจากนั้นฉันก็รู้ว่าอัลกอริทึมของเขาใช้ได้เฉพาะวันที่จนถึงวันที่ 5 เมษายน 1987และเรามีบางวันจากปี 1940 ที่ยังแปลงไม่ถูกต้อง ในที่สุดเราก็ต้องการUTCวันที่ในฐานข้อมูล SQL Server ของเราเพื่อให้สอดคล้องกับอัลกอริทึมในโปรแกรมของบุคคลที่สามที่ใช้ Java API เพื่อแปลงจากUTCเวลาท้องถิ่น

ฉันชอบCLRตัวอย่างในคำตอบของ Kevin Feasel ด้านบนโดยใช้ตัวอย่างของ Harsh Chawla และฉันยังต้องการเปรียบเทียบกับโซลูชันที่ใช้ Java เนื่องจากส่วนหน้าของเราใช้ Java เพื่อUTCแปลงเวลาท้องถิ่นเป็นภาษาท้องถิ่น

วิกิพีเดียกล่าวถึงการแก้ไขรัฐธรรมนูญ 8 แบบที่เกี่ยวข้องกับการปรับเปลี่ยนเขตเวลาก่อนปี 2530 และหลาย ๆ ส่วนได้รับการแปลเป็นภาษาท้องถิ่นในรัฐที่แตกต่างกันมากดังนั้นจึงมีโอกาสที่ CLR และ Java อาจตีความแตกต่างกัน รหัสแอปพลิเคชันส่วนหน้าของคุณใช้ dotnet หรือ Java หรือเป็นวันที่ก่อนปี 1987 ที่เป็นปัญหาสำหรับคุณหรือไม่


2

คุณสามารถทำได้โดยใช้ CLR Stored Procedure

[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 และมันก็เป็นอย่างดีไม่ปลอดภัย น่าเสียดายที่ไม่มีการใช้งาน. โซนเวลาแบบสมบูรณ์ซึ่งสามารถใช้งานได้โดยไม่unsafeได้รับอนุญาต ดูที่นี่และที่นี่
Frédéric

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
สวัสดี Joost! ขอบคุณสำหรับการโพสต์ หากคุณเพิ่มคำอธิบายลงในคำตอบอาจเป็นเรื่องง่ายที่จะเข้าใจ
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

คุณอาจต้องการพิจารณาใช้ชื่อส่วนวันที่แบบยาวแทนชื่อแบบสั้น เพียงเพื่อความชัดเจน ดูบทความที่ยอดเยี่ยมของ Aaron Bertrand ในเรื่อง "นิสัยที่ไม่ดี"
Max Vernon

นอกจากนี้ยินดีต้อนรับสู่ผู้ดูแลฐานข้อมูล - โปรดทัวร์ถ้าคุณยังไม่ได้!
Max Vernon

1
ขอบคุณทุกความคิดเห็นที่เป็นประโยชน์และคำแนะนำการแก้ไขที่เป็นประโยชน์ฉันเป็นมือใหม่ทั้งหมดที่นี่อย่างใดฉันได้พยายามที่จะสะสม 1 จุดซึ่งเป็น fab :-)
colinp_1

ตอนนี้คุณมี 11.
Max Vernon
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.