日時/タイムスタンプとタイムゾーンに関わるデータ型
タイムゾーン情報の有無に関わる、日時またはタイムスタンプを扱うデータ型について、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からの秒数)
- 表示例1:
- 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)の記事を書きました。
RubyのTime
は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.
- 表示上の日時の数値を表す値であって、タイムゾーンの情報はない
- 型名の例
- Javaの
LocalDateTime
型 - Pythonの
datetime
クラスのNaiveタイプ - PostgreSQLの
timestamp
型- timestampという名前が直感に反する
- MySQLの
DATETIME
型
- Javaの
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からの秒数を表す単一の数値で保存していることが多い
- 型名の例
- Javaの
Instant
型、Date
型 - Rubyの
Time
型はBとCが混ざっている- ロンドン0時と東京9時で
==
が成立する点はBっぽい
- ロンドン0時と東京9時で
- PostgreSQLの
timestamp with time zone
型- with time zoneという名前が直感に反する
- 別名
timestamptz
- MySQLの
TIMESTAMP
型 - Unixtime
- Javaの
C.
- 特定の瞬間を表す値(B)にUTCとのオフセット値の情報を加えたもの
- 型名の例
D.
- 表示上の日時の数値(A)に地域の情報を加えたもの
Asia/Seoul
の2020-07-23 09:00:00
という値はAsia/Tokyo
の2020-07-23 09:00:00
と同じ瞬間を表すが、地域が違うので、異なる値として扱う
- UTCとのオフセット値は地域と日時からタイムゾーンのデータベースを見ないとわからない。データベースには以下のようなややこしい事情が全部必要
America/Los_Angeles
は夏時間のシーズンと冬時間のシーズンがあるので、2019-11-03 23:00:00
は -08:00 だが、前日の2019-11-02 23:00:00
は -07:00Asia/Seoul
は日本と同じで通年で +09:00 だが、ソウルオリンピックによる臨時夏時間があったため1988-07-23 09:00:00
は +10:00Europe/Moscow
の2015-01-01 00:00:00
は +03:00 だが、2014年にロシアがオフセット値を変更しているため前年の2014-01-01 00:00:00
は +04:00
America/Los_Angeles
の2019-11-03 01:00:00
は夏時間から冬時間に変わるタイミングでありこの日の深夜1時というのは2回ある。夏時間(-07:00)の深夜1時の1時間後に冬時間(-08:00)の深夜1時になるので、この日の深夜1時という情報から特定の瞬間を表すことはできない- 内部実装にはタイムゾーンのデータベースが必要
- 型名の例
参考
- timestamp with time zone型はタイムゾーン情報を持っていない - Qiita
- Java8の日時APIはとりあえずこれだけ覚えとけ - Qiita
- タイムゾーン呪いの書 - Qiita