web-dev-qa-db-ja.com

データが日時に依存している場合、データベースに日時およびタイムゾーン情報を保存するベストプラクティス

DBに日時とタイムゾーンの情報を保存することについてはかなり多くの質問がありましたが、全体的なレベルではさらに多くの質問がありました。ここでは、特定のケースに対処したいと思います。

システム仕様

  • Ordersシステムデータベースがあります
  • テナントが任意のタイムゾーンを使用できるマルチテナントシステムです(テナントごとに任意ですが単一のタイムゾーンであり、テナントテーブルに一度保存され、変更されることはありません)

DBでカバーする必要があるビジネスルール

  • テナントがシステムに注文を出すと、注文番号はローカルの日時に基づいて計算されます(文字通り数字ではなく、ORDR-13432-Year-Month-Dayのような何らかの識別子)。現時点では正確な計算は重要ではありませんが、テナントのローカル日時に依存していることが重要です
  • また、システムレベルで、テナントに関係なく、いくつかのUTC日時の間に配置されたすべての注文を選択できるようにします(一般的なシステム統計/レポート用)

最初のアイデア

  • 私たちの最初のアイデアは、DB全体でUTC日​​時を保存し、もちろん、テナントのタイムゾーンをUTCに対してオフセットし、DBを消費するアプリケーションが常に日時をUTCに変換し、DB自体が常にUTCで動作するようにすることでした。

アプローチ1

  • ローカルテナントの日付時刻を保存すると、テナントごとにナイスになりますが、次のようなクエリで問題が発生します。

    SELECT * FROM ORDERS WHERE OrderDateTime BETWEEN UTCDateTime1 AND UTCDateTime2
    

このクエリのOrderDateTimeは、テナントに基づいて異なる瞬間を意味するため、問題があります。もちろん、このクエリにはTenantsテーブルへの結合が含まれてローカルの日付/時刻オフセットを取得し、_OrderDateTimeをその場で計算して調整します。それは可能ですが、それを行うのに良い方法であるかどうかはわかりませんか?

アプローチ2

  • 一方、UTC日時を保存する場合、UTCの日/月/年はローカル日時の日/月/年と異なる可能性があるため、OrderNumberの計算を行うとき

極端な例を見てみましょう。テナントがUTCより6時間進んでおり、ローカルの日時が2017-01-01 02:00であるとします。 UTCは2016-12-31 20:00になります。その時点で発注された注文はOrderNumber 'ORDR-13432-2017-1-1'を取得するはずですが、UTCを保存するとORDR-13432-2016-12-31を取得します。

この場合、DBでOrderを作成する時点で、UTC日時、テナントオフセットを取得し、再計算されたテナントのローカル時間に基づいてOrderNumberをコンパイルしますが、DateTime列はUTCで保存します。

質問

  1. この種の状況を処理する好ましい方法は何ですか?
  2. UTC日時を保存するニースソリューションはありますか?システムレベルのレポートのために、それは私たちにとってかなりナイスだからです。
  3. UTCを保存する場合、アプローチ2)はそれらのケースを処理するのに良い方法ですか、それともより良い/推奨される方法がありますか?

[UPDATE]

Gerard AshtonとHugoからのコメントに基づきます:

テナントがタイムゾーンを変更できるかどうか、および政治当局がタイムゾーンのプロパティまたはテリトリーのタイムゾーンを変更するとどうなるかについて、最初の質問は詳細に関して明確ではありませんでした。もちろんそれは非常に重要ですが、この問題の中心にはありません。別の質問でそれを取り上げるかもしれません。

この質問のために、テナントが場所を変更しないと仮定しましょう。その場所のタイムゾーンプロパティまたはタイムゾーン自体が変更される場合があり、これらの変更はこの質問とは別にシステムで処理されます。

27
daneejela

Hugoの答えはほとんど正しいですが、いくつかの重要なポイントを追加します。

  • 顧客のタイムゾーンを保存するときは、数値のオフセットを保存しないでください。他の人が指摘したように、UTCからのオフセットは1つの時点のみであり、DSTやその他の理由で簡単に変更できます。代わりに、タイムゾーン識別子、できればIANAタイムゾーン識別子を"America/Los_Angeles"などの文字列として保存する必要があります。詳細は タイムゾーンタグwiki をご覧ください。

  • OrderDateTimeフィールドは、UTCで時刻を絶対的に表す必要があります。ただし、データベースプラットフォームに応じて、これを保存する方法にはいくつかの選択肢があります。

    • たとえば、Microsoft SQL Serverを使用する場合、UTCからのオフセットを保持するdatetimeoffset列にローカル時間を格納するのが適切なアプローチです。その列に作成するインデックスはUTC同等に基づいているため、範囲クエリを実行すると、クエリのパフォーマンスが向上することに注意してください。

    • 他のデータベースプラットフォームを使用している場合は、代わりにtimestampフィールドにUTC値を格納することもできます。一部のデータベースにはtimestamp with time zoneもありますが、タイムゾーンまたはオフセットstoresを意味するわけではないことを理解してください。値を保存および取得するときに暗黙的に。常にUTCを表す場合は、多くの場合、timestamp(タイムゾーンなし)またはdatetimeのみが適切です。

  • 上記のメソッドのいずれかがUTC時間を保存するため、ローカル時間値のインデックスを必要とする操作の実行方法も考慮する必要があります。たとえば、ユーザーのタイムゾーンの日に基づいて、日次レポートを作成する必要がある場合があります。そのためには、現地の日付でグループ化する必要があります。 UTC値からクエリ時にそれを計算しようとすると、テーブル全体をスキャンすることになります。

    これに対処するための良い方法は、ローカルdate(または、場合によってはローカルdatetimeにも個別の列を作成することですが、nota datetimeoffsetまたはtimestamp)。これは、個別に入力する完全に分離された列でも、他の列に基づいて計算/計算された列でもかまいません。この列をインデックスで使用して、ローカル日付でフィルタリングまたはグループ化できます。

  • 計算列アプローチを使用する場合は、データベース内のタイムゾーンを変換する方法を知る必要があります。一部のデータベースには、IANAタイムゾーン識別子を理解するconvert_tz関数が組み込まれています。

    Microsoft SQL Serverを使用している場合、SQL 2016およびAzure SQL DBで新しい AT TIME ZONE 関数を使用できますが、これはMicrosoftタイムゾーン識別子でのみ機能します。 IANAタイムゾーン識別子を使用するには、my SQL Serverタイムゾーンサポート プロジェクトなどのサードパーティソリューションが必要です。

  • クエリ時には、BETWEENステートメントの使用を避けます。完全に包括的です。日付全体で問題なく動作しますが、時間がかかる場合は、次のような半開範囲のクエリを実行することをお勧めします。

    ... WHERE OrderDateTime >= @t1 AND OrderDateTime < @t2
    

    たとえば、@t1が今日の始まりであれば、@t2は明日の始まりになります。

ユーザーのタイムゾーンが変更されたコメントで説明されているシナリオに関して:

  • データベースのローカル日付を計算することを選択した場合、心配する必要がある唯一のシナリオは、ロケーションまたはビジネスが「ゾーン分割」が発生することなくタイムゾーンを切り替える場合です。ゾーン分割は、古いルールと新しいルールを含む、変更されたエリアをカバーする新しいタイムゾーン識別子が導入される場合です。

    たとえば、これを書いている時点でIANA tzdbに追加された最新のゾーンはAmerica/Punta_Arenasです。これは、チリ南部がチリの残り(America/Santiago)がUTCに戻ったときにUTC-3にとどまることにしたゾーン分割でしたDSTの終わりに-4。

    ただし、2つのタイムゾーンの境界の小さな地域が、どちらの側に従うかを変更することを決定し、ゾーン分割が保証されなかった場合、古いデータに対して新しいタイムゾーンのルールを使用する可能性があります。

  • ローカル日付を個​​別に保存する場合(DBではなくアプリケーションで計算されます)、問題はありません。ユーザーはタイムゾーンを新しいタイムゾーンに変更し、古いデータはすべてそのまま残り、新しいデータは新しいタイムゾーンで保存されます。

25

内部的には常にUTCを使用し、ユーザーに日付を表​​示する場合にのみタイムゾーンに変換することをお勧めします。したがって、アプローチ2を好む傾向があります。

テナントの現地の日付/時刻が識別子の一部でなければならないというビジネスルールがある場合は、そうします。ただし、内部的には、注文日をUTCで保持します。

あなたの例を使用して:タイムゾーンがUTC+06:00にあるテナント。したがって、テナントのローカル時間は2017-01-01 02:00であり、これはUTCの2016-12-31 20:00と同等です。

順序識別子ORDR-13432-2017-1-1になり、順序日付はUTC 2016-12-31 20:00Zになります。

2つの日付の間のすべての注文を取得するには、このクエリは簡単です。

SELECT * FROM ORDERS WHERE OrderDateTime BETWEEN UTCDateTime1 AND UTCDateTime2

OrderDateTimeはUTCであるためです。

特定のテナントを探している場合は、対応するタイムゾーンを取得し、それに応じて日付を変換して検索できます。上記と同じ例を使用して(テナントのタイムゾーンはUTC+06:00にあります)、2017-01-01で行われたすべての注文を取得するには(テナントの現地時間に):

--get tenant timezone
--startUTC=tenant's local 2017-01-01 00:00 converted to UTC (2016-12-31T18:00Z)
--endUTC=tenant's local 2017-01-01 23:59:59.999 converted to UTC (2017-01-01T17:59:59.999)
SELECT * FROM ORDERS WHERE OrderDateTime between startUTC and endUTC

これにより、ORDR-13432-2017-1-1が正しく取得されます。


異なるタイムゾーンの複数のテナントに対してクエリを実行するには、両方のアプローチで結合が必要になるため、この場合に「より良い」ものはありません。

テナントのローカル日付/時刻で追加の列を作成しない限り(UTCOrderDateTimeはテナントのタイムゾーンに変換されます)。これは冗長になりますが、複数のタイムゾーンで検索するクエリに役立ちます。それが合理的なトレードオフである場合、それらのクエリがどの程度頻繁に行われるかによって異なります。

14
user7605325