DST'den önceki veya sonraki bir tarih için UTC ile yerel saatler arasındaki doğru dengeyi nasıl alabilirim?


29

Şu anda bir UTC tarih saatinden yerel bir tarih saatini almak için aşağıdakileri kullanıyorum:

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

Benim sorunum şu ki, gün ışığından yararlanma süresi GetUTCDate()ve arasında biterse @utcDateTime, @localDateTimesona erme süresi bir saat.

Geçerli tarih olmayan bir tarih için utc'dan yerel saate dönüştürmenin kolay bir yolu var mı?

SQL Server 2005 kullanıyorum

Yanıtlar:


18

Mevcut olmayan bir UTC tarihini yerel saate dönüştürmenin en iyi yolu CLR kullanmaktır. Kodun kendisi kolaydır; Zor kısım genellikle insanları CLR'nin saf şeytan ya da korkutucu olmadığı konusunda ikna etmek ...

Pek çok örnek için, Harsh Chawla'nın konuyla ilgili blog yazısına göz atın .

Ne yazık ki, bu tür bir dönüşümle başa çıkabilen, CLR tabanlı çözümler için tasarruf sağlayacak hiçbir şey yoktur. Böyle bir şey yapan bir T-SQL işlevi yazabilirsiniz, ancak o zaman tarih değişikliği mantığını kendiniz uygulamak zorunda kalırsınız ve buna kesinlikle kolay değil derim.


Zaman içinde bölgesel değişikliklerin gerçek karmaşıklığı göz önüne alındığında, saf T-SQL'de bunu denemenin "kesinlikle kolay olmadığını" söyleyerek büyük olasılıkla altını çiziyor ;-). Yani evet, SQLCLR bu işlemi gerçekleştirmenin tek güvenilir ve verimli aracıdır. Bunun için +1. Bilginize: Bağlantılı blog yazısı işlevsel olarak doğrudur, ancak en iyi uygulamaları izlememektedir, bu nedenle maalesef verimsizdir. UTC ve sunucu yerel saati arasında dönüştürme işlevleri SQL # kütüphanesinde (yazarı olduğum) var, ancak Ücretsiz sürümde bulunmuyor .
Solomon Rutzky,

1
Eklenmesi gerektiğinde CLR kötülük kazanır WITH PERMISSION_SET = UNSAFE. Bazı ortamlar AWS RDS gibi izin vermiyor. Ve bu güvenli değil. Ne yazık ki, unsafeizinsiz kullanılabilecek hiçbir Net zaman dilimi uygulaması yoktur . Buraya ve buraya bakın .
Frédéric

15

Microsoft SQL Server'da tarih ve saat dilimi ile mücadele eden herkese yardımcı olmak için codeplex üzerinde T-SQL Toolbox projesini geliştirdim ve yayınladım . Açık kaynak ve kullanımı tamamen ücretsizdir.

Kutudan önceden doldurulmuş konfigürasyon tablolarının yanı sıra düz T-SQL (CLR yok) kullanarak kolay tarih / saat dönüşümü UDF'leri sunar. Ve tam DST (gün ışığından yararlanma saati) desteğine sahiptir.

Desteklenen tüm zaman dilimlerinin bir listesi "DateTimeUtil.Timezone" tablosunda bulunabilir (T-SQL Araç Kutusu veritabanında bulunur).

Örnekte, aşağıdaki örneği kullanabilirsiniz:

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
)

Bu dönüştürülen yerel tarih saat değerini döndürür.

Ne yazık ki, yalnızca daha yeni veri türleri (DATE, TIME, DATETIME2) nedeniyle SQL Server 2008 veya sonraki sürümlerinde desteklenir. Ancak tam kaynak kodu verildiğinde, tabloları ve UDF'leri DATETIME ile değiştirerek kolayca ayarlayabilirsiniz. Test için bir MSSQL 2005'im yok, ancak MSSQL 2005 ile de çalışması gerekiyor. Sorularınız olursa, sadece bana bildirin.


12

Ben her zaman bu TSQL komutunu kullanıyorum.

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

-- the local time

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

Çok basit ve işi yapıyor.


2
UTC'den tam bir saat uzaklığı olmayan zaman dilimleri vardır, bu nedenle bu DATEPART'ın kullanılması sorunlara neden olabilir.
Michael Green

4
Michael Green'in yorumu ile ilgili olarak, sorunu SELECT DATEADD (DAKİKA, DATEDIFF (DAKİKA, GETUTCDATE (), GETDATE ()), @utc) olarak değiştirerek çözebilirsiniz.
Kayıtlı Kullanıcı

4
Bu, yalnızca geçerli zamanın DST olup olmadığını belirleyen, ardından DST olabilecek bir zamanı karşılaştırarak belirlediğiniz için çalışmaz. İngiltere'deki yukarıdaki örnek kodunuzu ve tarih saatinizi kullanmak şu anda bana sabah 6:14, ancak Kasım'ın DST dışında olması dolayısıyla GMT ve UTC'nin çakıştığı 5:14 olması gerektiğini söylüyor.
Matt

Bunu kabul etmeme rağmen, bu asıl soruyu ele almıyor, bu cevaba gelince, aşağıdakilerin daha iyi olduğunu düşünüyorum: SELECT DATEADD (DAKİKA, DATEPART (TZoffset, SYSDATETIMEOFFSET ()), @utc)
Eamon

@Ludo Bernaerts: İlk milisaniyeyi kullanın, ikincisi: bu çalışmaz çünkü UTC bugünkü UTC ofsetinden belirli bir zamanda farklı olabilir (gün ışığından yararlanma - yaza karşı kış saati) ...
Quandary

11

buldum Bu cevabı , tarihlerini doğru şekilde çeviriyor gibi görünen Kullanıcı Tanımlı bir İşlev sağlayan StackOverflow'ta

Değiştirmeniz gereken tek şey, @offsetbu işlevi çalıştıran SQL sunucusunun Saat Dilimi ofsetine ayarlamak için üstteki değişkendir. Benim durumumda, SQL sunucumuz GMT olan EST kullanıyor - 5

Mükemmel değil ve muhtemelen yarım saat veya 15 dakikalık TZ ofsetleri olan birçok durumda işe yaramayacak ( Kevin'in tavsiye ettiği bir CLR işlevini önerenler için ), ancak Kuzey'deki çoğu jenerik zaman dilimi için yeterince iyi çalışıyor 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


3

Stack Overflow'ta sorulan benzer bir soruya birkaç iyi cevap var . Veri dönüştürme danışmanının yol açtığı karmaşayı temizlemek için Bob Albright'ın ikinci cevabındaki T-SQL yaklaşımını kullanarak yaralandım .

Verilerimizin neredeyse tamamı için işe yaradı, ancak daha sonra algoritmasının sadece 5 Nisan 1987'ye kadar olan tarihler için çalıştığını ve 1940'lardan bu yana hala düzgün bir şekilde dönüşmeyen bazı tarihler geçirdiğimizi fark ettim . Sonunda UTC, dönüştürmek için Java API'sini kullanan bir üçüncü taraf programdaki bir algoritmayla aynı hizada olmak için SQL Server veritabanımızdaki tarihlere ihtiyacımız vardı.UTC yerel saate .

Gibi CLRörneğin yukarıda Kevin Feasel yanıtında Sert Chawla örneğini kullanarak ve bizim ön uç yapmak için Java kullandığından, Java kullanan bir çözüme karşılaştırmak için benzeri de ediyorumUTC yerel saat dönüşümü.

Vikipedi, 1987'den önce saat dilimi düzenlemelerini içeren 8 farklı anayasa değişikliğinden bahseder ve bunların çoğu farklı eyaletlere çok özgüdür, bu yüzden CLR ve Java'nın bunları farklı şekilde yorumlama şansı vardır. Ön uç uygulama kodunuz dotnet veya Java kullanıyor mu veya 1987'den önceki tarihler sizin için sorun oluyor mu?


2

Bunu bir CLR Saklı Prosedürü ile kolayca yapabilirsiniz.

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

Kullanılabilir TimeZones'ı bir tabloda saklayabilirsiniz:

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

Ve bu saklı yordam, tabloyu sunucunuzdaki olası zaman dilimleriyle doldurur.

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

Eklenmesi gerektiğinde CLR kötülük kazanır WITH PERMISSION_SET = UNSAFE. Bazı ortamlar AWS RDS gibi izin vermiyor. Ve bu güvenli değil. Ne yazık ki, unsafeizinsiz kullanılabilecek hiçbir Net zaman dilimi uygulaması yoktur . Buraya ve buraya bakın .
Frédéric

2

SQL Server 2016 sürümü bu sorunu bir kez ve herkes için çözecektir . Daha önceki sürümler için bir CLR çözümü muhtemelen en kolay olanıdır. Veya belirli bir DST kuralı için (yalnızca ABD'de olduğu gibi), bir T-SQL işlevi nispeten basit olabilir.

Ancak, genel bir T-SQL çözümü mümkün olabileceğini düşünüyorum. Uzun Olarak xp_regreadeserleri, şunu deneyin:

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

Bir (karmaşık) T-SQL işlevi bu verileri , geçerli DST kuralı sırasındaki tüm tarihler için tam kaymayı belirlemek üzere kullanabilir .


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
Selam Joost! Gönderdiğiniz için teşekkür ederiz. Cevabınıza bir açıklama eklerseniz, anlaşılması daha kolay olabilir.
LowlyDBA

2

İşte belirli bir İngiltere uygulaması için yazılmış ve tamamen SELECT'e dayalı bir cevap.

  1. Saat dilimi dengesi yok (örneğin İngiltere)
  2. Mart ayının son pazar gününden başlayarak ve ekim ayının son pazar gününü bitiren gün ışığından yararlanma için yazılmıştır (İngiltere kuralları).
  3. Gündüzleri gece yarısı ve 01:00 arasında uygulanamaz. Gün ışığından yararlanma başlar. Bu düzeltilebilir, ancak yazıldığı uygulama gerektirmez.

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

Kısa tarih yerine uzun tarih bölüm adlarını kullanmayı düşünebilirsiniz. Sadece netlik için. Bkz birkaç "kötü alışkanlıkları" konulu Aaron Bertrand'ın mükemmel makale
Max Vernon

Ayrıca, Veritabanı Yöneticilerine hoş geldiniz - henüz yapmadıysanız , lütfen tura katılın!
Max Vernon

1
Teşekkürler, yararlı yorumlar ve faydalı düzenleme önerileri, burada tamamen acemiyim, bir şekilde fab :-) olan 1 puan toplamayı başardım.
colinp_1

şimdi 11 var
Max Vernon
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.