RailsとPostgreSQLでタイムゾーンを完全に無視する


164

RailsとPostgresで日付と時刻を処理していて、この問題が発生しています。

データベースはUTCです。

ユーザーはRailsアプリで選択したタイムゾーンを設定しますが、時間を比較するためにユーザーのローカル時間を取得するときにのみ使用されます。

ユーザーは時間を保存します。たとえば、2012年3月17日午後7時です。タイムゾーン変換やタイムゾーンを保存したくありません。日付と時刻を保存したいだけです。これにより、ユーザーがタイムゾーンを変更した場合でも、2012年3月17日午後7時と表示されます。

ユーザーが指定したタイムゾーンのみを使用して、ユーザーのローカルタイムゾーンの現在時刻の「前」または「後」のレコードを取得します。

私は現在「タイムゾーンなしのタイムスタンプ」を使用していますが、レコードを取得すると、レール(?)がそれらをアプリのタイムゾーンに変換しますが、これは不要です。

Appointment.first.time
 => Fri, 02 Mar 2012 19:00:00 UTC +00:00 

データベースのレコードはUTCとして出力されるように見えるので、私のハックは現在の時刻を取得し、 'Date.strptime(str、 "%m /%d /%Y")'でタイムゾーンを削除してから、それでクエリ:

.where("time >= ?", date_start)

周りのタイムゾーンを無視するより簡単な方法があるはずです。何か案は?

回答:


347

データ型timestampはの短縮名ですtimestamp without time zone
他のオプションtimestamptzはの略ですtimestamp with time zone

timestamptzある好適な、文字通り、日付/時間ファミリのタイプ。にtypispreferred設定されておりpg_type、関連性があります。

内部ストレージとエポック

内部的には、タイムスタンプはディスクおよびRAMの8バイトのストレージを占有します。これは、Postgresエポック、2000-01-01 00:00:00 UTCからのマイクロ秒数を表す整数値です。

Postgresは、UNIXエポック(1970-01-01 00:00:00 UTC)から一般的に使用されるUNIX時間カウント秒の組み込みの知識を持ち、関数to_timestamp(double precision)またはでそれを使用しますEXTRACT(EPOCH FROM timestamptz)

ソースコード:

*タイムスタンプと間隔のh / m / sフィールドは、
*マイクロ秒単位のint64値。(かつて彼らは  
*秒単位の倍精度値。)

そして:

/ * UnixおよびPostgresでの0日目と同等のユリウス日付* /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j(1970、1、1)* /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j(2000、1、1)* /  

マイクロ秒の解像度は、秒の最大6の小数桁に変換されます。

timestamp

として入力された値は、タイムゾーンが明示的に提供されていないことをPostgresに伝えます。現在のタイムゾーンが想定されます。Postgres 誤って追加されたタイムゾーン修飾子を無視します!timestamp [without time zone]

表示のためにシフトされる時間はありません。同じタイムゾーン設定ですべて問題ありません。別のタイムゾーン設定では意味が変わりますが、表示は同じままです。

timestamptz

取り扱いtimestamp with time zoneが微妙に異なります。私はここにマニュアルを引用します

の場合timestamp with time zone、内部的に保存される値は常にUTC(協定世界時...)です。

大胆な強調鉱山。タイムゾーン自体は保存されることはありません。これは、保存されているUTCタイムスタンプを計算するために使用される入力修飾子です。または、表示するローカル時間を計算するために使用される出力修飾子です。タイムゾーンオフセットが追加されています。timestamptz入力時にオフセットを追加しない場合、セッションの現在のタイムゾーン設定が想定されます。すべての計算はUTCタイムスタンプ値で行われます。複数のタイムゾーンを処理する必要がある(または処理する必要がある場合)は、を使用しますtimestamptz

psqlやpgAdminなどのクライアント、またはlibpqを介して通信するすべてのアプリケーション(pg gemを使用したRubyなど)には、現在のタイムゾーンのタイムスタンプとオフセットが、または要求されたタイムゾーンに従って表示されます(以下を参照)。常に同じ時点であり、表示形式のみが異なります。または、マニュアルにあるように

すべてのタイムゾーン対応の日付と時刻は、内部でUTCに格納されます。これらは 、クライアントに表示される前に、TimeZone構成パラメーターで指定されたゾーンの現地時間に変換されます。

次の簡単な例(psql)を考えてみます。

db =#SELECT timestamptz '2012-03-05 20:00 +03 ';
      タイムスタンプ
------------------------
 2012-03-05 18:00:00 +01

大胆な強調鉱山。ここで何が起こったのですか?入力リテラルに
任意のタイムゾーンオフセットを選択しました+3。Postgresにとって、これはUTCタイムスタンプを入力する多くの方法の1つにすぎません2012-03-05 17:00:00。クエリの結果がされて表示された設定を現在のタイムゾーンのためのウィーン/オーストリアを相殺している私のテストでは、+1冬の間および+2:夏の期間中に2012-03-05 18:00:00+01、それは冬時間に落ちるので。

Postgresは、この値の入力方法をすでに忘れています。覚えているのは、値とデータ型だけです。10進数と同じように。numeric '003.4'numeric '3.40'またはnumeric '+3.4'-すべての結果はまったく同じ内部値になります。

AT TIME ZONE

このロジックを理解するとすぐに、好きなことができます。現在欠けているのは、特定のタイムゾーンに従ってタイムスタンプリテラルを解釈または表現するためのツールです。これがAT TIME ZONE構造の出番です。2つの異なるユースケースがあります。timestamptzに変換されtimestamp、その逆も同様です。

UTCに入るにはtimestamptz 2012-03-05 17:00:00+0

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC'

...これは次と同等です:

SELECT timestamptz '2012-03-05 17:00:00 UTC'

EST timestamp(東部標準時)と同じ時点を表示するには:

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST'

そうです、AT TIME ZONE 'UTC' 2回。最初のものは、timestamp値を(指定された)UTCタイムスタンプとして解釈し、タイプを返しtimestamptzます。もう一つは変換timestamptztimestamp時間内に、このユニークな点のタイムゾーンESTディスプレイのどの時計-指定されたタイムゾーンの「EST」。

SELECT ts AT TIME ZONE 'UTC'
FROM  (
   VALUES
      (1, timestamptz '2012-03-05 17:00:00+0')
    , (2, timestamptz '2012-03-05 18:00:00+1')
    , (3, timestamptz '2012-03-05 17:00:00 UTC')
    , (4, timestamp   '2012-03-05 11:00:00'  AT TIME ZONE '+6') 
    , (5, timestamp   '2012-03-05 17:00:00'  AT TIME ZONE 'UTC') 
    , (6, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'US/Hawaii')  -- 
    , (7, timestamptz '2012-03-05 07:00:00 US/Hawaii')                  -- 
    , (8, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'HST')        -- 
    , (9, timestamp   '2012-03-05 18:00:00+1')  --  loaded footgun!
      ) t(id, ts);

同じUTCタイムスタンプを保持するtimestamptz列を持つ8(または9)の同一の行を返します2012-03-05 17:00:00。9行目は私のタイムゾーンで動作しますが、邪悪な罠です。下記参照。

① ハワイ時間のタイムゾーンとタイムゾーンの省略形を含む行6〜8はDST(夏時間)の対象であり、現在ではありませんが異なる場合があります。のようなタイムゾーン名'US/Hawaii'は、DSTルールとすべての履歴シフトを自動的に認識しますが、のような略語HSTは、固定オフセットの単なるダムコードです。夏時間/標準時間には別の略語を追加する必要がある場合があります。名前は正しく解釈する任意の与えられた時間帯にタイムスタンプを。略語は安いですが、与えられたタイムスタンプのために正しいものである必要があります:

夏時間は、人類がこれまで考え出した最も明るいアイデアの1つではありません。

ロードされたフットガンとしてマークされた列9 は私にとっては機能しますが、偶然にのみ機能します。リテラルをに明示的にキャストした場合timestamp [without time zone]タイムゾーンオフセットは無視されます。ベアタイムスタンプのみが使用されます。このtimestamptz例では、値は列のタイプと一致するように自動的に強制されます。このステップでtimezoneは、現在のセッションの設定が想定されます。これは、たまたま+1私の場合(ヨーロッパ/ウィーン)と同じタイムゾーンです。しかし、おそらくあなたの場合はそうではありません-これは異なる値になります。つまり、timestamptzリテラルをキャストしないでください。キャストするとtimestamp、タイムゾーンオフセットが失われます。

あなたの質問

ユーザーは時間を保存します。たとえば、2012年3月17日午後7時です。タイムゾーン変換やタイムゾーンを保存したくありません。

タイムゾーン自体は保存されません。上記のいずれかの方法を使用して、UTCタイムスタンプを入力します。

ユーザーが指定したタイムゾーンのみを使用して、ユーザーのローカルタイムゾーンの現在時刻の「前」または「後」のレコードを取得します。

異なるタイムゾーンのすべてのクライアントに対して1つのクエリを使用できます。
絶対世界時間の場合:

SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time

ローカルクロックによる時間の場合:

SELECT * FROM tbl WHERE time_col > now()::time

まだ背景情報に飽きていませんか?マニュアルにもっとあります。


2
細かい詳細ですが、タイムスタンプは2000-01-01以降のマイクロ秒数として内部的に格納されていると思います。マニュアルの日付/時刻データ型のセクションを参照してください。私自身の出典の調査で確認できたようです。エポックに別の起源を使用するのは奇妙です!
2013年

2
@harmicエポックの違いは…実はそれほど変ではない。このウィキペディアのページには、さまざまなコンピューターシステムで使用される20のエポックがリストされています。一方でUnixエポックが一般的ですが、それだけではないです。
バジルブルク2014

4
@ErwinBrandstetterこれは1つの深刻な欠陥を除いて、素晴らしい答えです。harmicがコメントしたように、PostgresはUnix時間を使用しませ文書によると:(a)エポックはUnixの1970-01-01ではなく2001-01-01であり、(b)Unixの時間は秒単位の精度を持っていますが、Postgresは秒の端数を保持します。小数桁数は、コンパイル時オプションによって異なります。8バイト整数ストレージ(デフォルト)を使用する場合は0〜6、または浮動小数点ストレージ(非推奨)を使用する場合は0〜10です。
バジルブルク14

2
@BasilBourque:私はこの不幸な間違いを知っています。よろしければ編集してください。私は過去にあなたの答えのいくつかを見たことがありますが、あなたはそれが得意です。私からのもう1つの編集は、これをコミュニティーWikiに強制します-時間の経過とともに、私は明確かつ包括的にするために多くの努力(および編集)を行いました。
Erwin Brandstetter、2014

2
修正:以前のコメントで、Postgresのエポックを2001と誤って引用しました。実際には2000です。
バジルブルク14

1

デフォルトでUTCを扱いたい場合:

config/application.rb、次を追加します。

config.time_zone = 'UTC'

次に、現在のユーザーのタイムゾーン名を保存すると、current_user.timezone言うことができます。

post.created_at.in_time_zone(current_user.timezone)

current_user.timezone有効なタイムゾーン名である必要があります。そうでない場合は取得されます。完全なリストをArgumentError: Invalid Timezone参照してください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.