私は非常にシンプルなカレンダーテーブルを使用してこれを解決しました-毎年、サポートされているタイムゾーンごとに1つの行があり、標準オフセット、DSTの開始日時/終了日時、およびそのオフセット(そのタイムゾーンでサポートされている場合)が含まれています。次に、インラインのスキーマバインドのテーブル値関数で、ソース時間(もちろんUTC)を取得し、オフセットを加算または減算します。
データの大部分に対してレポートを作成している場合、これは明らかに非常にうまく機能しません。パーティショニングは役立つように見えるかもしれませんが、特定のタイムゾーンに変換すると、ある年の最後の数時間または翌年の最初の数時間が実際には別の年に属している場合があるため、実際のパーティションを取得することはできません分離。ただし、レポート範囲に12月31日または1月1日が含まれない場合は除きます。
考慮する必要がある奇妙なエッジケースがいくつかあります。
たとえば、2014年11月2日05:30 UTCおよび2014年11月2日06:30 UTCはどちらも東部時間帯の午前1時30分に変換されます(最初にローカルで01:30がヒットし、次に1つクロックが午前2時から午前1時にロールバックされ、さらに30分経過した2回目)。したがって、その1時間のレポートをどのように処理するかを決定する必要があります。UTCによると、DSTを監視するタイムゾーンで1時間に2時間がマッピングされると、測定しているトラフィックまたはボリュームが2倍になるはずです。これは、イベントのシーケンスで楽しいゲームをプレイすることもできます。なぜなら、何か他のものが現れた後に論理的に起こらなければならないことがあったからです。タイミングが2時間ではなく1時間に調整されると、その前に発生します。極端な例としては、UTC 05:59に発生したページビューと、UTC 06:00に発生したクリックがあります。UTC時間ではこれらは1分間隔で発生しましたが、東部時間に変換すると、ビューは午前1時59分に発生し、クリックは1時間早く発生しました。
2014-03-09 02:30アメリカでは決して起こりません。これは、午前2時に時計を午前3時に進めるためです。そのため、ユーザーがそのような時間を入力し、それをUTCに変換するように求められた場合、またはユーザーがそのような時間を選択できないようにフォームをデザインする場合は、エラーを発生させることになるでしょう。
これらのエッジケースを念頭に置いても、私はあなたが正しいアプローチを持っていると思います:UTCでデータを保存します。特に、異なるタイムゾーンが異なる日付にDSTを開始/終了する場合、および同じタイムゾーンでさえ、異なる年に異なるルールを使用して切り替えることができる場合、特定のタイムゾーンから他のタイムゾーンに移動するよりも、UTCから他のタイムゾーンにデータをマップする方がはるかに簡単です(たとえば、米国は6年ほど前にルールを変更しました)。
あなたはなく、いくつかの巨大な、このすべてのカレンダーテーブルを使用したいと思うでしょうCASE
表現(ない声明)。これについて、MSSQLTips.comの 3部構成のシリーズを書きました。3番目の部分が最も役立つと思います。
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/
その間、実際のライブの例
非常に単純なファクトテーブルがあるとします。この場合に私が気にする唯一の事実はイベント時間ですが、気になるほどテーブルを広くするために、意味のないGUIDを追加します。繰り返しになりますが、ファクトテーブルはイベントをUTC時間とUTC時間のみで保存します。列に接尾辞を付けた_UTC
ので、混乱はありません。
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
次に、ファクトテーブルに10,000,000行をロードします。これは、2013-12-30のUTC午前0時から2014-12-12の午前5時以降までの3秒ごと(1時間あたり1,200行)を表します。これにより、データが年の境界をまたぐようになり、DSTが複数のタイムゾーンで前後に進みます。これは恐ろしいように見えますが、私のシステムでは約9秒かかりました。テーブルは約325 MBになるはずです。
;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;
そして、このクエリを実行した場合に、この10 MM行テーブルに対して典型的なシーククエリがどのように見えるかを示すためだけに:
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);
このプランを取得すると、25ミリ秒*で戻り、358回の読み取りを実行して、72時間ごとの合計を返します。
* 結果を破棄する無料のSQL Sentry Plan Explorerで測定された期間。これには、データのネットワーク転送時間、レンダリングなどは含まれません。追加の免責事項として、私はSQL Sentryで働いています。
明らかに、範囲を大きくしすぎると少し時間がかかります。1か月のデータには258ミリ秒、2か月には500ミリ秒以上かかります。並列処理が有効になる場合があります。
ここから、レポートクエリを満たすためのより優れた他のソリューションについて考え始めます。出力が表示するタイムゾーンとは関係ありません。それについては触れません。タイムゾーン変換によってレポートクエリが実際にそれほど多くのことを実行するわけではなく、適切にサポートされていない大きな範囲を取得している場合は、それらがすでに機能しない可能性があることを示したいだけです。インデックス。日付範囲を短くして、ロジックが正しいことを示し、タイムゾーン変換の有無にかかわらず、範囲ベースのレポートクエリが適切に実行されることを確認できるようにします。
さて、ここで、サポートされている各年のタイムゾーン(すべてのユーザーがUTCから数時間も離れているわけではないので、分単位のオフセット)とDST変更日付を格納するテーブルが必要です。簡単にするために、上記のデータと一致させるために、いくつかのタイムゾーンと1年のみを入力します。
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
);
さまざまなタイムゾーンがいくつか含まれています。オフセットが30分あるものや、DSTを監視しないものもあります。オーストラリアは、南半球で私たちの冬の間DSTを観察するので、そのクロックが行くことに注意してくださいバック 4月にして前方に 10月に。(上記の表では名前が逆になっていますが、南半球のタイムゾーンでこれをわかりやすくする方法がわかりません。)
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
TZがいつ変更されるかを知るためのカレンダーテーブル。関心のある行のみを挿入します(上の各タイムゾーン、および2014年の夏時間の変更のみ)。計算を簡単にするために、タイムゾーンが変化するUTCの瞬間と現地時間の同じ瞬間の両方を保存します。DSTを順守しないタイムゾーンの場合、それは1年中標準であり、DSTは1月1日に「開始」します。
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])
);
ループではなく、アルゴリズムを確実に入力することができます(そして、今後のヒントシリーズでは、自分で言うとしたら、巧妙なセットベースの手法をいくつか使用します)。この答えのために、私は5つのタイムゾーンに1年を手動で入力することにしました。空想的なトリックに煩わされることはありません。
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');
さて、ファクトデータと "ディメンション"テーブル(私が言うとうんざりします)があるので、ロジックは何ですか。ええと、ユーザーにタイムゾーンを選択してクエリの日付範囲を入力してもらうことになると思います。また、日付範囲はそれぞれのタイムゾーンで丸1日と想定します。部分的な日はありません、部分的な時間を気にしないでください。したがって、開始日、終了日、およびTimeZoneIDを渡します。そこから、スカラー関数を使用して、開始日/終了日をそのタイムゾーンからUTCに変換します。これにより、UTC範囲に基づいてデータをフィルター処理できます。これを実行して集計を実行したら、ユーザーに表示する前に、グループ化された時間の変換をソースタイムゾーンに適用し直すことができます。
スカラー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
そして、テーブル値関数:
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])
);
そして、それを使用する手順(編集:30分のオフセットのグループ化を処理するように更新):
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
(ユーザーがUTCでのレポートを希望する場合は、そこで短絡するか、別のストアドプロシージャを使用することをお勧めします。UTCとの間の変換は明らかに無駄な多忙な作業になります。)
呼び出しの例:
EXEC dbo.ReportOnDateRange
@Start = '20140308',
@End = '20140311',
@TimeZoneID = 3;
41ms *で戻り、このプランを生成します。
* 繰り返しますが、結果は破棄されます。
2か月間は507ミリ秒で戻り、計画は行数以外は同じです。
少し複雑で、実行時間は少し長くなりますが、このタイプのアプローチは、ブリッジテーブルのアプローチよりもはるかにうまく機能すると確信しています。そして、これはdba.seの回答のオフカフ例です。私よりもはるかに賢い人が私の論理と効率を改善できると確信しています。
データを熟読して、私が話しているエッジケースを確認できます-クロックがロールフォワードする1時間の出力の行がない、それらがロールバックする1時間の2つの行(およびその時間が2回発生した)。悪い値で遊ぶこともできます。たとえば、20140309 02:30東部標準時を過ぎると、うまく機能しません。
レポートがどのように機能するかについて、すべての仮定が正しいわけではない可能性があるため、調整が必要になる場合があります。しかし、これは基本をカバーしていると思います。