多くのタイムゾーンのデータに対するレポートをサポートするデータウェアハウスの設計を最適化しようとしています。たとえば、1日の時間ごとにグループ化されたアクティビティを示す必要がある、1か月分のアクティビティ(数百万行)のレポートがあるとします。そしてもちろん、その日の時間は、指定されたタイムゾーンの「ローカル」時間である必要があります。
UTCと1つの現地時間をサポートしたときにうまく機能するデザインがありました。 UTCおよび現地時間の日付と時刻のディメンションの標準設計、ファクトテーブルのID。ただし、100以上のタイムゾーンのレポートをサポートする必要がある場合、そのアプローチは拡張されないようです。
ファクトテーブルは非常に広くなります。また、レポートの特定の実行でグループ化に使用する日付と時刻のIDを指定するというSQLの構文の問題を解決する必要があります。おそらく非常に大きなCASEステートメントですか?
カバーしているUTC時間範囲ごとにすべてのデータを取得し、それをプレゼンテーションレイヤーに戻してローカルに変換してそこで集計するという提案をいくつか見ましたが、SSRSを使用した限定的なテストでは、非常に遅くなることが示唆されています。
私はこの主題についてもいくつかの本を参考にしてきましたが、それらはすべてUTCがあり、ディスプレイ上で変換するか、UTCと1つのローカルがあると言っているようです。任意の考えや提案をいただければ幸いです。
注:この質問は次のようなものです。 データマート/ウェアハウスのタイムゾーンの処理 ですが、その質問についてコメントすることはできないため、これは独自の質問に値するものだと感じました。
更新:アーロンが重要な更新を行い、サンプルコードと図を投稿した後、私はアーロンの回答を選択しました。彼の回答に対する以前のコメントは、回答の元の編集を参照しているため、あまり意味がありません。必要に応じて、戻ってこれをもう一度更新しようとします
非常に単純なカレンダーテーブルを使用してこれを解決しました-毎年1行サポートされているタイムゾーンごとで、DSTの標準のオフセットと開始日時/終了日時、およびそのオフセット(その時間の場合)ゾーンはそれをサポートしています)。次に、インラインスキーマバインドのテーブル値関数で、ソース時間(もちろんUTC)を取得し、オフセットを加算または減算します。
データの大部分に対してレポートを作成している場合、これは明らかに非常にうまく機能しません。パーティション化は役立つように見えるかもしれませんが、特定のタイムゾーンに変換すると、1年の最後の数時間または翌年の最初の数時間が実際には別の年に属している場合があるため、実際のパーティションを取得することはできません分離。ただし、レポート範囲に12月31日または1月1日が含まれない場合は除きます。
考慮する必要がある奇妙なEdgeのケースがいくつかあります。
たとえば、2014年11月2日05:30 UTCおよび2014年11月2日06:30 UTCはどちらも東部タイムゾーンで01:30 AMに変換されます(最初にローカルで01:30がヒットし、次に1つ時計が午前2時から午前1時にロールバックし、さらに30分経過した2回目)。そのため、その1時間のレポートをどのように処理するかを決定する必要があります。UTCによれば、DSTを監視するタイムゾーンで1時間に2時間がマッピングされると、測定しているトラフィックまたはボリュームが2倍になるはずです。これは、イベントのシーケンスで楽しいゲームをプレイすることもできます。それは、論理的に他の何かの後に発生しなければならなかった何かappearが、タイミングが2時間ではなく1時間に調整されると、その前に発生する可能性があるためです。極端な例は、UTC 05:59に発生したページビューと、UTC 06:00に発生したクリックです。 UTC時間ではこれらは1分間隔で発生しましたが、東部時間に変換すると、ビューは午前1:59に発生し、クリックは1時間早く発生しました。
2014-03-09 02:30アメリカでは決して起こりません。これは、午前2時にクロックを午前3時に進めるためです。そのため、ユーザーがそのような時間を入力し、それをUTCに変換するように求められた場合、またはユーザーがそのような時間を選択できないようにフォームをデザインする場合は、エラーを発生させると考えられます。
これらのEdgeケースを念頭に置いても、私はあなたが正しいアプローチを持っていると思います:データをUTCに保存します。特に、異なるタイムゾーンが異なる日付にDSTを開始/終了する場合、および同じタイムゾーンでさえ、異なる年に異なるルールを使用して切り替えることができる場合、あるタイムゾーンから他のタイムゾーンに移動するよりも、UTCから他のタイムゾーンにデータをマップする方がはるかに簡単です(たとえば、米国は6年ほど前にルールを変更しました)。
あなたはこれすべてのためにカレンダーテーブルを使用したいでしょう、いくつかの巨大なCASE
式ではありません(--ステートメントではありません)。これについて MSSQLTips.com の3部構成のシリーズを書いたところです。 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のUTC午前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月にback、10月にforwardになります。 (上記の表では名前が逆になっていますが、南半球のタイムゾーンでこれをわかりやすくする方法がわかりません。)
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])
);
そして、それを使用する手順(edit: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の回答のオフカフ例です。私よりもずっと賢い人が私の論理と効率を改善できると確信しています。
データを調べて、私が話しているEdgeのケースを確認することができます-クロックがロールフォワードする1時間の出力の行はありません。悪い値で遊ぶこともできます。たとえば、20140309 02:30東部標準時を過ぎると、うまくいきません。
レポートがどのように機能するかについて、すべての想定が正しいとは限らないため、いくつかの調整が必要になる場合があります。しかし、これは基本をカバーしていると思います。
プレゼンテーションレイヤーの代わりにストアドプロシージャまたはパラメーター化されたビューで変換を実行できますか?もう1つのオプションは、キューブを作成し、計算をキューブに入れることです。
コメントからの説明:
OPは、プレゼンテーションレイヤーで計算を行うことにより、限られたテストでパフォーマンスの問題に遭遇しました。私の提案は、それをデータベースに移動することです。 SQLでは、テーブル値関数を使用してパラメーター化ビューを実行できます。この関数に渡されるタイムゾーンに基づいて、データを計算し、UTCテーブルから返すことができます。これで私の元の答えが明確になることを願っています。