Bunu çok basit bir takvim tablosuna sahip olarak çözdüm - her yıl desteklenen zaman dilimi başına bir satır vardır, standart ofset ve DST ve başlangıç tarih / bitiş tarih saatiyle (bu saat dilimi destekliyorsa). Ardından kaynak zamanını alan (elbette UTC'de) ve ofseti toplayan / çıkaran satır içi, şemaya bağlı, tablo değerli bir işlev.
Verilerin büyük bir kısmına karşı bildirimde bulunuyorsanız, bu kesinlikle hiçbir zaman iyi bir performans göstermeyecektir; bölümleme yardımcı gibi görünebilir, ancak yine de bir yılda son birkaç saatin veya gelecek yılın ilk birkaç saatinin belirli bir saat dilimine dönüştürüldüğünde aslında farklı bir yıla ait olduğu durumlara sahip olacaksınız - böylece asla gerçek bölüm elde edemezsiniz raporlama aralığınızın 31 Aralık veya 1 Ocak içermediği durumlar dışında, yalıtım.
Dikkate almanız gereken birkaç garip kenar durumu var:
2014-11-02 05:30 UTC ve 2014-11-02 06:30 UTC her ikisi de Doğu saat diliminde 01:30 AM, örneğin (ilk kez 01:30 yerel olarak vuruldu ve sonra bir saatlerin 2:00 AM - 1:00 AM arasında geri döndüğü ikinci kez ve yarım saat daha geçtiği). Bu nedenle, bu saat raporunun nasıl ele alınacağına karar vermelisiniz - UTC'ye göre, bu iki saat DST'yi gözlemleyen bir saat diliminde tek bir saate eşlendikten sonra ölçtüğünüz şeyin trafiğini veya hacmini iki katına çıkarmalısınız. Bu aynı zamanda mantıksal başka bir şey olabilir sonra ne zorunda şey beri, olayların dizileme ile eğlenceli oyunlar oynayabilirsiniz görünürzamanlama iki yerine tek bir saate ayarlandıktan sonra gerçekleşir. Aşırı bir örnek, 05:59 UTC'de gerçekleşen bir sayfa görünümüdür, ardından 06:00 UTC'de gerçekleşen bir tıklamadır. UTC zamanında bunlar bir dakika arayla gerçekleşti, ancak Doğu zamanına dönüştürüldüğünde, görünüm 1:59 AM'de gerçekleşti ve tıklama bir saat önce gerçekleşti.
2014-03-09 02:30 ABD'de asla olmaz. Bunun nedeni, 2: 00'de saatleri 3: 00'a doğru yuvarlamamızdır. Bu nedenle, kullanıcı böyle bir zaman girerse ve bunu UTC'ye dönüştürmenizi isterse veya formunuzu, kullanıcıların böyle bir zaman seçememesi için tasarlamanız istenirse, bir hata oluşturmak isteyeceksiniz.
Bu uç durumlar göz önünde bulundurulduğunda bile, hala doğru yaklaşıma sahip olduğunuzu düşünüyorum: verileri UTC'de saklayın. Verileri UTC'den diğer saat dilimlerine eşleştirmek, özellikle farklı saat dilimleri farklı tarihlerde DST'yi başlattığında / sonlandırdığında ve hatta aynı saat dilimi farklı yıllarda farklı kurallar kullanarak geçiş yapabiliyorsa, bir saat diliminden diğer bir saat dilimine eşlemek çok daha kolaydır. örneğin ABD kuralları 6 yıl önce değiştirdi).
Buna, bazı devasa tümü için takvim tablo kullanmak isteyeceksiniz CASE
ifadesi (değil bildirimi ). Bu konuda MSSQLTips.com için üç bölümlük bir seri yazdım ; Bence 3. bölüm sizin için en faydalı olacak:
http://www.mssqltips.com/sqlservertip/3173/handle-conversion-between-time-zones-in-sql-server--part-1/
http://www.mssqltips.com/sqlservertip/3174/handle-conversion-between-time-zones-in-sql-server--part-2/
http://www.mssqltips.com/sqlservertip/3175/handle-conversion-between-time-zones-in-sql-server--part-3/
Bu arada gerçek bir canlı örnek
Diyelim ki çok basit bir olgu tablonuz var. Bu durumda umursadığım tek gerçek olay zamanıdır, ancak tabloyu önemsemek için yeterince geniş hale getirmek için anlamsız bir GUID ekleyeceğim. Yine açık olmak gerekirse, olgu tablosu olayları yalnızca UTC ve UTC saatlerinde saklar. Sütunu bile ekledim, _UTC
bu yüzden karışıklık yok.
CREATE TABLE dbo.Fact
(
EventTime_UTC DATETIME NOT NULL,
Filler UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID()
);
GO
CREATE CLUSTERED INDEX x ON dbo.Fact(EventTime_UTC);
GO
Şimdi, gerçek tablonuzu 10.000.000 satırla yükleyelim - 2013-12-30 arası gece yarısı UTC'de gece saatlerinde 2014-12-12 arası saat 5'e kadar her 3 saniyede bir (saatte 1.200 satır). Bu, verilerin bir yıl sınırı ile DST'nin birden fazla zaman dilimi için ileri ve geri olmasını sağlar. Bu gerçekten korkutucu görünüyor, ancak sistemimde ~ 9 saniye sürdü. Tablo 325 MB civarında olmalıdır.
;WITH x(c) AS
(
SELECT TOP (10000000) DATEADD(SECOND,
3*(ROW_NUMBER() OVER (ORDER BY s1.[object_id])-1),
'20131230')
FROM sys.all_columns AS s1
CROSS JOIN sys.all_columns AS s2
ORDER BY s1.[object_id]
)
INSERT dbo.Fact WITH (TABLOCKX) (EventTime_UTC)
SELECT c FROM x;
Ve bu sorguyu çalıştırırsam, tipik bir arama sorgusunun bu 10MM satır tablosuna nasıl görüneceğini göstermek için:
SELECT DATEADD(HOUR, DATEDIFF(HOUR, 0, EventTime_UTC), 0),
COUNT(*)
FROM dbo.Fact
WHERE EventTime_UTC >= '20140308'
AND EventTime_UTC < '20140311'
GROUP BY DATEADD(HOUR, DATEDIFF(HOUR, 0, EventTime_UTC), 0);
Bu planı alıyorum ve 72 saatlik toplamları döndürmek için 358 okuma yaparak 25 milisaniye * içinde dönüyor:
* Sonuçları atar ücretsiz SQL Sentry Plan Explorer tarafından ölçülen süre , bu nedenle veri, render, vb ağ aktarım süresini içermez. Ek bir feragatname olarak, SQL Sentry için çalışıyorum.
Açıkçası, aralığımı çok fazla büyütürsem biraz daha uzun sürer - bir aylık veri 258 ms sürer, iki ay 500 ms'yi alır, vb. Paralellik başlayabilir:
Bu, raporlama sorgularını karşılamak için diğer daha iyi çözümleri düşünmeye başladığınız yerdir ve çıktınızın hangi saat diliminde görüntüleneceği ile ilgisi yoktur. Buna girmeyeceğim, sadece zaman dilimi dönüşümünün raporlama sorgularınızı çok daha fazla emmesini sağlamayacağını göstermek istiyorum ve uygun şekilde desteklenmeyen büyük aralıklar alıyorsanız zaten emebilirler. indeksleri. Mantığın doğru olduğunu göstermek için küçük tarih aralıklarına bağlı kalacağım ve aralık tabanlı raporlama sorgularınızın saat dilimi dönüşümleriyle veya saat dilimi dönüşümleri olmadan yeterli bir şekilde çalıştığından emin olmanızı sağlayacağım.
Tamam, şimdi saat dilimlerimizi (ofsetlerle, dakikalar içinde, herkes UTC saati dışında bile olmadığından) ve desteklenen her yıl için DST değişiklik tarihlerini saklamak için tablolara ihtiyacımız var. Basit olması için, yukarıdaki verilerle eşleşmesi için yalnızca birkaç saat dilimi ve bir yıl gireceğim.
CREATE TABLE dbo.TimeZones
(
TimeZoneID TINYINT NOT NULL PRIMARY KEY,
Name VARCHAR(9) NOT NULL,
Offset SMALLINT NOT NULL, -- minutes
DSTName VARCHAR(9) NOT NULL,
DSTOffset SMALLINT NOT NULL -- minutes
);
Bazıları yarım saat ofsetleri olan, bazıları DST'yi gözlemlemeyen çeşitlilik için birkaç zaman dilimi içeriyordu. Onların saatler gitmek böylece Avustralya, güney yarımkürede, bizim kış aylarında DST gözlemler Not geri Nisan ayında ve ileriye Ekim ayında. (Yukarıdaki tablo isimleri ters çevirir, ancak bunu güney yarımküre saat dilimleri için nasıl daha az kafa karıştırıcı hale getireceğinden emin değilim.)
INSERT dbo.TimeZones VALUES
(1, 'UTC', 0, 'UTC', 0),
(2, 'GMT', 0, 'BST', 60),
-- London = UTC in winter, +1 in summer
(3, 'EST', -300, 'EDT', -240),
-- East coast US (-5 h in winter, -4 in summer)
(4, 'ACDT', 630, 'ACST', 570),
-- Adelaide (Australia) +10.5 h Oct - Apr, +9.5 Apr - Oct
(5, 'ACST', 570, 'ACST', 570);
-- Darwin (Australia) +9.5 h year round
Şimdi, TZ'lerin ne zaman değiştiğini bilmek için bir takvim tablosu. Sadece ilgilenilen satırları ekleyeceğim (yukarıdaki her saat dilimi ve 2014 için yalnızca DST değişiklikleri). Hesaplamaları kolaylaştırmak için, hem saat diliminin değiştiği anı UTC'de hem de yerel saatte aynı anı saklıyorum. DST'yi gözlemlemeyen saat dilimleri için, yıl boyunca standarttır ve DST 1 Ocak'ta "başlar".
CREATE TABLE dbo.Calendar
(
TimeZoneID TINYINT NOT NULL FOREIGN KEY
REFERENCES dbo.TimeZones(TimeZoneID),
[Year] SMALLDATETIME NOT NULL,
UTCDSTStart SMALLDATETIME NOT NULL,
UTCDSTEnd SMALLDATETIME NOT NULL,
LocalDSTStart SMALLDATETIME NOT NULL,
LocalDSTEnd SMALLDATETIME NOT NULL,
PRIMARY KEY (TimeZoneID, [Year])
);
Bunu kesinlikle algoritmalarla doldurabilirsiniz (ve yaklaşan ipucu serisi, kendim söylüyorsam, akıllıca set tabanlı teknikler kullanır), döngü yerine, elle doldurun, ne var. Bu cevap için beş zaman dilimi için bir yılı elle doldurmaya karar verdim ve herhangi bir fantezi hileyi rahatsız etmeyeceğim.
INSERT dbo.Calendar VALUES
(1, '20140101', '20140101 00:00','20150101 00:00','20140101 00:00','20150101 00:00'),
(2, '20140101', '20140330 01:00','20141026 00:00','20140330 02:00','20141026 01:00'),
(3, '20140101', '20140309 07:00','20141102 06:00','20140309 03:00','20141102 01:00'),
(4, '20140101', '20140405 16:30','20141004 16:30','20140406 03:00','20141005 02:00'),
(5, '20140101', '20140101 00:00','20150101 00:00','20140101 00:00','20150101 00:00');
Tamam, bu yüzden gerçek verilerimiz ve "boyut" tablolarımız var (bunu söylediğimde kandırırım), o zaman mantık nedir? Kullanıcıların saat dilimlerini seçmelerini ve sorgu için tarih aralığını girmelerini sağlayacağınızı tahmin ediyorum. Ayrıca tarih aralığının kendi saat diliminde tam gün olacağını varsayacağım; kısmi gün yok, kısmi saatleri boş verin. Bu yüzden bir başlangıç tarihi, bir bitiş tarihi ve bir TimeZoneID iletecekler. Oradan, başlangıç / bitiş tarihini o zaman diliminden UTC'ye dönüştürmek için bir skaler işlev kullanacağız; bu da verileri UTC aralığına göre filtrelememizi sağlayacaktır. Bunu yaptıktan ve üzerinde toplamalarımızı yaptıktan sonra, kullanıcıya göstermeden önce gruplanmış zamanların kaynak saat dilimine dönüştürülmesini uygulayabiliriz.
Skaler UDF:
CREATE FUNCTION dbo.ConvertToUTC
(
@Source SMALLDATETIME,
@SourceTZ TINYINT
)
RETURNS SMALLDATETIME
WITH SCHEMABINDING
AS
BEGIN
RETURN
(
SELECT DATEADD(MINUTE, -CASE
WHEN @Source >= src.LocalDSTStart
AND @Source < src.LocalDSTEnd THEN t.DSTOffset
WHEN @Source >= DATEADD(HOUR,-1,src.LocalDSTStart)
AND @Source < src.LocalDSTStart THEN NULL
ELSE t.Offset END, @Source)
FROM dbo.Calendar AS src
INNER JOIN dbo.TimeZones AS t
ON src.TimeZoneID = t.TimeZoneID
WHERE src.TimeZoneID = @SourceTZ
AND t.TimeZoneID = @SourceTZ
AND DATEADD(MINUTE,t.Offset,@Source) >= src.[Year]
AND DATEADD(MINUTE,t.Offset,@Source) < DATEADD(YEAR, 1, src.[Year])
);
END
GO
Ve tablo değerli fonksiyon:
CREATE FUNCTION dbo.ConvertFromUTC
(
@Source SMALLDATETIME,
@SourceTZ TINYINT
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT
[Target] = DATEADD(MINUTE, CASE
WHEN @Source >= trg.UTCDSTStart
AND @Source < trg.UTCDSTEnd THEN tz.DSTOffset
ELSE tz.Offset END, @Source)
FROM dbo.Calendar AS trg
INNER JOIN dbo.TimeZones AS tz
ON trg.TimeZoneID = tz.TimeZoneID
WHERE trg.TimeZoneID = @SourceTZ
AND tz.TimeZoneID = @SourceTZ
AND @Source >= trg.[Year]
AND @Source < DATEADD(YEAR, 1, trg.[Year])
);
Ve bunu kullanan bir prosedür ( değiştir : 30 dakikalık ofset gruplamasını işleyecek şekilde güncellendi):
CREATE PROCEDURE dbo.ReportOnDateRange
@Start SMALLDATETIME, -- whole dates only please!
@End SMALLDATETIME, -- whole dates only please!
@TimeZoneID TINYINT
AS
BEGIN
SET NOCOUNT ON;
SELECT @Start = dbo.ConvertToUTC(@Start, @TimeZoneID),
@End = dbo.ConvertToUTC(@End, @TimeZoneID);
;WITH x(t,c) AS
(
SELECT DATEDIFF(MINUTE, @Start, EventTime_UTC)/60,
COUNT(*)
FROM dbo.Fact
WHERE EventTime_UTC >= @Start
AND EventTime_UTC < DATEADD(DAY, 1, @End)
GROUP BY DATEDIFF(MINUTE, @Start, EventTime_UTC)/60
)
SELECT
UTC = DATEADD(MINUTE, x.t*60, @Start),
[Local] = y.[Target],
[RowCount] = x.c
FROM x OUTER APPLY
dbo.ConvertFromUTC(DATEADD(MINUTE, x.t*60, @Start), @TimeZoneID) AS y
ORDER BY UTC;
END
GO
(Kullanıcının UTC'de raporlama yapmak istemesi durumunda, orada kısa devre yapmak veya ayrı bir saklı yordam geçirmek isteyebilirsiniz - açıkçası UTC'ye ve UTC'den çeviri yapmak boşa harcanan yoğun bir çalışma olacaktır.)
Örnek çağrı:
EXEC dbo.ReportOnDateRange
@Start = '20140308',
@End = '20140311',
@TimeZoneID = 3;
41ms * içinde döner ve bu planı oluşturur:
* Yine, atılan sonuçlarla.
2 ay boyunca, 507 ms içinde döner ve plan satır sayıları dışında aynıdır:
Biraz daha karmaşık ve artan çalışma süresi biraz olsa da, bu tür bir yaklaşımın köprü tablosu yaklaşımından çok daha iyi çalışacağından oldukça eminim. Ve bu bir dba.se cevabı için manşet dışı bir örnektir; Eminim mantığım ve verimliliğim benden çok daha akıllı insanlar tarafından geliştirilebilir.
Veriler hakkında konuştuğum kenar durumlarını görmek için verileri inceleyebilirsiniz - saatlerin ileri doğru döndüğü saat için çıkış satırı yok, geri döndükleri saat için iki sıra (ve bu saat iki kez oldu). Ayrıca kötü değerlerle oynayabilirsiniz; 20140309 02:30 'da geçerseniz, örneğin Doğu zamanı, çok iyi çalışmayacak.
Raporlamanızın nasıl çalışacağına ilişkin tüm varsayımlara sahip olamayabilirim, bu nedenle bazı ayarlamalar yapmanız gerekebilir. Ama bunun temelleri kapsadığını düşünüyorum.