Bagaimana saya bisa mendapatkan offset yang benar antara waktu UTC dan waktu lokal untuk tanggal sebelum atau sesudah DST?

29

Saat ini saya menggunakan yang berikut ini untuk mendapatkan datetime lokal dari datetime UTC:

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

Masalah saya adalah bahwa jika penghematan waktu siang hari terjadi antara GetUTCDate()dan @utcDateTime, @localDateTimeujungnya menjadi satu jam libur.

Apakah ada cara mudah untuk mengkonversi dari utc ke waktu lokal untuk tanggal yang bukan tanggal saat ini?

Saya menggunakan SQL Server 2005

Rachel
sumber

Jawaban:

18

Cara terbaik untuk mengubah tanggal UTC yang tidak terkini menjadi waktu lokal adalah dengan menggunakan CLR. Kode itu sendiri mudah; bagian yang sulit biasanya meyakinkan orang bahwa CLR bukanlah kejahatan murni atau menakutkan ...

Untuk salah satu dari banyak contoh, lihat posting blog Harsh Chawla tentang topik tersebut .

Sayangnya, tidak ada built-in yang dapat menangani konversi jenis ini, kecuali solusi berbasis CLR. Anda bisa menulis fungsi T-SQL yang melakukan sesuatu seperti ini, tetapi kemudian Anda harus mengimplementasikan sendiri logika perubahan-tanggal, dan saya menyebutnya jelas tidak mudah.

Kevin Feasel
sumber
Mengingat kompleksitas aktual variasi regional dari waktu ke waktu, mengatakan itu "jelas tidak mudah" untuk mencoba ini dalam T-SQL murni mungkin mengecilkan itu ;-). Jadi ya, SQLCLR adalah satu-satunya cara yang dapat diandalkan dan efisien untuk melakukan operasi ini. +1 untuk itu. FYI: posting blog yang terhubung secara fungsional benar tetapi tidak mengikuti praktik terbaik sehingga sayangnya tidak efisien. Fungsi untuk mengonversi antara UTC dan waktu lokal server tersedia di pustaka SQL # (yang merupakan penulisnya), tetapi tidak dalam versi Gratis.
Solomon Rutzky
1
CLR menjadi jahat ketika harus ditambahkan WITH PERMISSION_SET = UNSAFE. Beberapa lingkungan tidak mengizinkannya seperti AWS RDS. Dan itu, yah, tidak aman. Sayangnya, tidak ada implementasi zona waktu lengkap. Net yang dapat digunakan tanpa unsafeizin. Lihat di sini dan di sini .
Frédéric
15

Saya telah mengembangkan dan menerbitkan proyek T-SQL Toolbox pada codeplex untuk membantu siapa saja yang berjuang dengan penanganan waktu dan zona waktu di Microsoft SQL Server. Ini open source dan sepenuhnya gratis untuk digunakan.

Ini menawarkan UDFs konversi datetime mudah menggunakan T-SQL polos (tidak ada CLR) di samping dengan tabel konfigurasi pra-diisi di luar kotak. Dan memiliki dukungan DST (daylight saving time) penuh.

Daftar semua zona waktu yang didukung dapat ditemukan dalam tabel "DateTimeUtil.Timezone" (disediakan dalam basis data T-SQL Toolbox).

Dalam contoh Anda, Anda dapat menggunakan sampel berikut:

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
)

Ini akan mengembalikan nilai datetime lokal yang dikonversi.

Sayangnya, ini didukung untuk SQL Server 2008 atau yang lebih baru hanya karena tipe data yang lebih baru (DATE, TIME, DATETIME2). Tetapi karena kode sumber lengkap disediakan, Anda dapat dengan mudah menyesuaikan tabel dan UDF dengan menggantinya dengan DATETIME. Saya tidak memiliki MSSQL 2005 yang tersedia untuk pengujian, tetapi itu harus bekerja dengan MSSQL 2005 juga. Jika ada pertanyaan, beri tahu saya.

iklan
sumber
12

Saya selalu menggunakan perintah TSQL ini.

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

-- the local time

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

Ini sangat sederhana dan berhasil.

Ludo Bernaerts
sumber
2
Ada zona waktu yang bukan offset satu jam penuh dari UTC sehingga penggunaan DATEPART ini dapat menyebabkan masalah bagi Anda.
Michael Green
4
Mengenai komentar Michael Green, Anda dapat mengatasi masalah ini dengan mengubahnya ke SELECT DATEADD (MINUTE, DATEDIFF (MINUTE, GETUTCDATE (), GETDATE (), GETDATE ()), @utc).
Pengguna Terdaftar
4
Ini tidak berfungsi karena Anda hanya menentukan apakah waktu saat ini DST atau tidak, kemudian membandingkan waktu yang bisa DST atau tidak. Dengan menggunakan kode contoh dan waktu lama Anda di Inggris saat ini mengatakan kepada saya bahwa itu harus 6:14 pagi, namun November di luar DST sehingga seharusnya 5:14 pagi karena GMT dan UTC bersamaan.
Mat
Sementara saya setuju ini tidak menjawab pertanyaan yang sebenarnya, sejauh menyangkut jawaban ini, saya pikir yang berikut ini lebih baik: PILIH DATEADD (MENIT, DATEPART (TZoffset, SYSDATETIMEOFFSET ()), @utc)
Eamon
@Ludo Bernaerts: Penggunaan pertama milidetik, kedua: ini tidak berfungsi karena offset UTC hari ini mungkin berbeda dari offset-UTC pada waktu tertentu (hemat siang hari - musim panas vs musim dingin) ...
Quandary
11

Saya menemukan jawaban ini di StackOverflow yang menyediakan Fungsi yang Ditentukan Pengguna yang tampaknya secara akurat menerjemahkan datetimes

Satu-satunya hal yang perlu Anda modifikasi adalah @offsetvariabel di bagian atas untuk mengaturnya ke Timezone ofset dari SQL server yang menjalankan fungsi ini. Dalam kasus saya, server SQL kami menggunakan EST, yaitu GMT - 5

Ini tidak sempurna dan mungkin tidak akan berfungsi untuk banyak kasus seperti memiliki offset TZ setengah jam atau 15 menit (bagi mereka saya akan merekomendasikan fungsi CLR seperti yang disarankan Kevin ), namun itu berfungsi cukup baik untuk sebagian besar zona waktu umum di Utara Amerika.

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
Rachel
sumber
3

Ada beberapa jawaban bagus untuk pertanyaan serupa yang diajukan di Stack Overflow. Saya akhirnya menggunakan pendekatan T-SQL dari jawaban kedua oleh Bob Albright untuk membersihkan kekacauan yang disebabkan oleh konsultan konversi data.

Ini bekerja untuk hampir semua data kami, tetapi kemudian saya menyadari bahwa algoritmanya hanya berfungsi untuk tanggal sejauh 5 April 1987 , dan kami memiliki beberapa tanggal dari tahun 1940-an yang masih belum dikonversi dengan benar. Kami akhirnya membutuhkan UTCtanggal dalam database SQL Server kami untuk sejalan dengan algoritma dalam program pihak ke-3 yang menggunakan Java API untuk mengkonversi dari UTCke waktu lokal.

Saya suka CLRcontoh dalam jawaban Kevin Feasel di atas menggunakan contoh Harsh Chawla, dan saya juga ingin membandingkannya dengan solusi yang menggunakan Java, karena ujung depan kami menggunakan Jawa untuk melakukan UTCkonversi waktu ke waktu lokal.

Wikipedia menyebutkan 8 amandemen konstitusi berbeda yang melibatkan penyesuaian zona waktu sebelum 1987, dan banyak dari mereka sangat terlokalisasi untuk negara yang berbeda, sehingga ada kemungkinan bahwa CLR dan Jawa dapat menafsirkannya secara berbeda. Apakah kode aplikasi front-end Anda menggunakan dotnet atau Java, atau tanggal sebelum 1987 masalah bagi Anda?

kkarns
sumber
2

Anda dapat dengan mudah melakukan ini dengan Prosedur Tersimpan 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);
}

Anda dapat menyimpan Zona Waktu yang tersedia di tabel:

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

Dan prosedur tersimpan ini akan mengisi tabel dengan zona waktu yang memungkinkan pada server Anda.

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();
                }
            }
        }
    }
}
Tim Cooke
sumber
CLR menjadi jahat ketika harus ditambahkan WITH PERMISSION_SET = UNSAFE. Beberapa lingkungan tidak mengizinkannya seperti AWS RDS. Dan itu, yah, tidak aman. Sayangnya, tidak ada implementasi zona waktu lengkap. Net yang dapat digunakan tanpa unsafeizin. Lihat di sini dan di sini .
Frédéric
2

SQL Server versi 2016 akan menyelesaikan masalah ini sekali dan untuk semua . Untuk versi sebelumnya, solusi CLR mungkin paling mudah. Atau untuk aturan DST tertentu (seperti AS saja), fungsi T-SQL bisa relatif sederhana.

Namun, saya pikir solusi T-SQL generik mungkin dilakukan. Selama xp_regreadbekerja, coba ini:

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

Fungsi T-SQL (kompleks) dapat menggunakan data ini untuk menentukan offset yang tepat untuk semua tanggal selama aturan DST saat ini.

Michel de Ruiter
sumber
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)
Joost Versteegen
sumber
2
Hai Joost! Terima kasih sudah memposting. Jika Anda menambahkan beberapa penjelasan pada jawaban Anda, itu mungkin terbukti jauh lebih mudah untuk dipahami.
LowlyDBA
2

Berikut adalah jawaban yang ditulis untuk aplikasi UK tertentu dan berdasarkan murni pada SELECT.

  1. Tidak ada offset zona waktu (mis. Inggris)
  2. Ditulis untuk penghematan siang hari dimulai pada hari Minggu terakhir bulan Maret dan berakhir pada hari Minggu terakhir bulan Oktober (peraturan Inggris)
  3. Tidak berlaku antara tengah malam dan 01:00 pada hari mulai siang hari dimulai. Ini bisa diperbaiki tetapi aplikasi yang ditulis untuk itu tidak memerlukannya.

    -- 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
sumber
Anda mungkin ingin mempertimbangkan untuk menggunakan nama bagian tanggal panjang, bukan yang pendek. Hanya untuk kejelasan. Lihat artikel luar biasa Aaron Bertrand tentang beberapa "kebiasaan buruk"
Max Vernon
Juga, selamat datang ke Administrator Database - silakan ikuti tur jika Anda belum melakukannya!
Max Vernon
1
Terima kasih semua, komentar yang bermanfaat dan saran edit yang bermanfaat, saya seorang pemula total di sini, entah bagaimana saya berhasil mengumpulkan 1 poin yang hebat :-).
colinp_1
sekarang Anda punya 11.
Max Vernon