日時/タイムスタンプとタイムゾーンに関わるデータ型

タイムゾーン情報の有無に関わる、日時またはタイムスタンプを扱うデータ型について、4つに分類して整理しました。

  • A: 「0時」
  • B: @0時
  • C: @0時 +09:00
  • D: @0時 日本時間

Aは、ロンドンの0時も東京の0時も同じ「0時」として表現します。ロンドンの0時と東京の9時は異なる値になります。

Bは逆に、ロンドンの0時と東京の9時を同じ値で表現します。ただし値からはロンドンの0時なのか東京の9時なのかはわかりません。

AとBは、表現している意味が異なります。しかし、どちらもタイムゾーンの情報はありません。

CとDは、Bにタイムゾーンの情報を付与したものです。

環境によってBを「with time zone」などと表現していて、個人的には混乱しやすいのが、本記事を書いた理由です。AもBもタイムゾーン情報がなく、CとDがタイムゾーン情報付きです。

4つの型の概要

  • A: 年月日時分秒の6個の表示数値を持つデータ。どの瞬間かは不明。タイムゾーン情報もない
    • 表示例: 2020-07-23 00:00:00
  • B: ある特定の瞬間を表す数値。どう表示すべきかは不明。タイムゾーン情報もない。1970年1月1日0時UTCからの秒数で実装していることが多い
    • 表示例1: 2020-07-23 00:00:00 +00:00
    • 表示例2: 2020-07-23 09:00:00 +09:00
    • 表示例3: 1595430000 (1970年1月1日0時UTCからの秒数)
  • C: BにUTCとのオフセット値を加えたもの。どの瞬間かは明確。年月日時分秒の6個の数値も明確
    • 表示例: 2020-07-23 09:00:00 +09:00
  • D: Aに地域の情報を加えたもの。どの瞬間かはだいたい(後述)わかる。年月日時分秒の6個の数値も明確
    • 表示例: 2020-07-23 09:00:00 Asia/Tokyo

AとBの内容はまったく異なるものですが、環境の都合でAとBを相互に変換しないといけない状況もあり、よっぽど注意して扱わないと不整合が起きてしまいます。AとBの違いを理解しておけば、AとBを変換したときにどういうことが起こり得るかは予想ができるはずです。変換せざるを得ない場合にも対処しやすいです。

CはAとBのいいとこ取りのように見えて、悪いところ取りでもあります。

Dは入出力時や時間計算では必要な場面がありそうです。

各環境での扱い

各環境での型名

A B C D
2020-07-23 00:00:00 2020-07-23 00:00:00 2020-07-23 00:00:00+09:00 2020-07-23 00:00:00 Asia/Tokyo
Java LocalDateTime Instant OffsetDateTime ZonedDateTime
Python datetimeのNaive datetimeのAware datetimeのAwareでpytz利用
Ruby Time Time
PostgreSQL timestamp timestamp with time zone
MySQL DATETIME TIMESTAMP
Unixtime

4つのデータ型に区別して理解している立場としては、Javaのクラスはとってもわかりやすいです。

Pythonは場当たり的な機能追加のように見えてしまいます。過去にPythonでのタイムゾーンの扱い(datetime, pytz)の記事を書きました。

RubyTimeはBとCが混ざっているように見えます。タイムゾーンの情報を保持できるのでCかと思いきや、2つの値が同じ瞬間を表すのであれば異なるタイムゾーンであっても == 演算子true になります。

AとBはどちらもタイムゾーンの情報がないはずですが、PostgreSQLの型名は非常に紛らわしいです。timestamp with time zone型はタイムゾーン情報を持っていないの記事の著者も同じことを主張されています。

PostgreSQLの型

timestamp (without time zone)はA。年月日時分秒の6個の数値を保持するデータ型です。

timestamp with time zone はB。with time zoneという名前に反してタイムゾーンの情報は含まれていません。ある特定の瞬間を表す値で、表示するときにタイムゾーンの設定があれば、それに応じて日時を表示しているだけです。

-- with time zoneではロンドン0時と東京9時は同じ値
select '2020-07-29 00:00:00+00' :: timestamp with time zone = '2020-07-29 09:00:00+09' :: timestamp with time zone;
=> t

-- with time zoneではロンドン0時と東京0時は別の値
select '2020-07-29 00:00:00+00' :: timestamp with time zone = '2020-07-29 00:00:00+09' :: timestamp with time zone;
=> f

-- without time zoneではロンドン0時と東京9時は別の値
select '2020-07-29 00:00:00+00' :: timestamp without time zone = '2020-07-29 09:00:00+09' :: timestamp without time zone;
=> f

-- without time zoneではロンドン0時と東京0時は同じ値
select '2020-07-29 00:00:00+00' :: timestamp without time zone = '2020-07-29 00:00:00+09' :: timestamp without time zone;
=> t

with time zone は瞬間が同じかどうかに意味があって、年月日時分秒もタイムゾーンも関係ないです。

without time zone は年月日時分秒の数値だけに意味があって、瞬間もタイムゾーンも関係ないです。

何度もいいますが、with time zoneもwithout time zoneもタイムゾーン情報は含まれていません。

4つの型の詳細

A.

  • 表示上の日時の数値を表す値であって、タイムゾーンの情報はない
    • 2020-07-22 00:00:00 という値はUTCの0時かもしれないし、台湾時間の0時かもしれない
    • どの0時のことかを決めないといけない場合は、運用側が決めるのであって、値自身はそれを表現していない
    • UTCや日本時間などとタイムゾーンを固定して、Bの目的でAを使うこともあるかもしれないが、混乱しやすい
  • 型名の例
    • JavaLocalDateTime
    • PythondatetimeクラスのNaiveタイプ
    • PostgreSQLtimestamp
      • timestampという名前が直感に反する
    • MySQLDATETIME

B.

  • 特定の瞬間を表す値であって、タイムゾーンの情報はない
    • 2020-07-23 09:00:00+09:00 という値はUTCで0時の瞬間を表し、 2020-07-23 00:00:00+00:00 と同じ値で区別がつかない
    • 2020-07-23 09:00:00+09:00 と表記するか 2020-07-23 00:00:00 と表記するのかは値を表示する側が決めることであって、値自身はそれを表現していない
    • ログデータなど、イベント発生の瞬間を特定する目的にかなっている
  • 内部実装は1970年1月1日0時UTCからの秒数を表す単一の数値で保存していることが多い
  • 型名の例
    • JavaInstant型、Date
    • RubyTime型はBとCが混ざっている
      • ロンドン0時と東京9時で == が成立する点はBっぽい
    • PostgreSQLtimestamp with time zone
      • with time zoneという名前が直感に反する
      • 別名 timestamptz
    • MySQLTIMESTAMP
    • Unixtime

C.

  • 特定の瞬間を表す値(B)にUTCとのオフセット値の情報を加えたもの
    • 2020-07-23 09:00:00+09:00 という値はUTCで0時の瞬間を表すが、 +09:00 というタイムゾーンの情報も含まれ、 2020-07-23 00:00:00+00:00 とは異なる値として扱う
  • 型名の例
    • JavaOffsetDateTime
    • PythondatetimeクラスのAwareタイプでタイムゾーンがオフセット値のみの場合
    • RubyTime型はBとCが混ざっている

D.

  • 表示上の日時の数値(A)に地域の情報を加えたもの
    • Asia/Seoul2020-07-23 09:00:00 という値は Asia/Tokyo2020-07-23 09:00:00 と同じ瞬間を表すが、地域が違うので、異なる値として扱う
  • UTCとのオフセット値は地域と日時からタイムゾーンのデータベースを見ないとわからない。データベースには以下のようなややこしい事情が全部必要
    • America/Los_Angeles は夏時間のシーズンと冬時間のシーズンがあるので、 2019-11-03 23:00:00 は -08:00 だが、前日の 2019-11-02 23:00:00 は -07:00
    • Asia/Seoul は日本と同じで通年で +09:00 だが、ソウルオリンピックによる臨時夏時間があったため 1988-07-23 09:00:00 は +10:00
    • Europe/Moscow2015-01-01 00:00:00 は +03:00 だが、2014年にロシアがオフセット値を変更しているため前年の 2014-01-01 00:00:00 は +04:00
  • America/Los_Angeles2019-11-03 01:00:00 は夏時間から冬時間に変わるタイミングでありこの日の深夜1時というのは2回ある。夏時間(-07:00)の深夜1時の1時間後に冬時間(-08:00)の深夜1時になるので、この日の深夜1時という情報から特定の瞬間を表すことはできない
  • 内部実装にはタイムゾーンのデータベースが必要
  • 型名の例
    • JavaZonedDateTime
      • オフセット値が変わるタイミングで2つの瞬間があることも表現できている
    • PythondatetimeクラスのAwareタイプでタイムゾーンにpytzパッケージを利用する場合

参考

備考

  • 地球の自転速度が一定でないためにうるう秒というものもあるが、この記事では無視してる
  • 年月日の数値は採用している暦によって変わってくるが、この記事では扱わない。JavaのCalendarクラスはこのあたりを真面目に扱っている
  • 時の流れは相対性理論によれば相対的なもの
    • 重力が違うと時間の流れ方が違う
    • 重力の違いを考え始めると、TT(地球時)、TCG(地心座標時)、TCB(太陽系座標時)、TDB(太陽系力学時)などが登場する