PostgreSQL の timestamp with time zone 型にまつわる誤解
HQ ではメインのリレーショナルデータベースとして PostgreSQL を利用しており、
ほぼすべてのテーブルにはcreated_at
やupdated_at
のような作成日時や更新日時などを記録するカラムを持たせています。
今回は PostgreSQL のタイムスタンプ型について誤解しがちな点について説明します。
型名から想像するタイムスタンプ型の挙動
PostgreSQL には、タイムスタンプのデータ型として以下の 2 つがあります。
timestamp with time zone
(省略形:timestamptz
)timestamp without time zone
(省略形:timestamp
)
一見とてもわかりやすい名前に見えますね。しかし実際の仕様・挙動は、多くの人がこれらの名前から想像するものとは異なるかもしれません。
私は初めて HQ の DB スキーマを見て timestamp with time zone
が多く使われているのを見たとき、こんな風に考えました。
timestamp with time zone
はおそらく時間情報とタイムゾーン情報を一緒に保存できる型なんだろうな(間違い)- 各レコード・各カラムごとに異なるタイムゾーンを持たせることができるのかな(間違い)
- Go の
time.Time
や Rails のActiveSupport::TimeWithZone
もタイムゾーン情報を持った時間型なので、DB からの読み込み・書き込み時にこれらとマッピングできる感じだろうか(間違い) - HQ では基本的にこっちを使っているっぽい
- じゃあ、
timestamp without time zone
は時間情報だけを保存できる型なんだろうな。こっちはほとんど使われてなさそう - でもタイムゾーンが必要になるのって、フロントエンドの UI でユーザーのローカルタイムゾーンで時間・日付を表示するときくらいじゃないだろうか
- ユーザーのローカルタイムゾーンはブラウザと JavaScript が面倒を見てくれるし、「このユーザーのタイムゾーンはこれ」という情報をサーバー側に保存する必要もなさそう
- サーバー側のビジネスロジックでタイムゾーンが必要な処理は今のところないし、 DB に保存する必要はあるんだろうか?
実際に保存されているtimestamp with time zone
なデータを見てみても以下のようになっており、
この+00
を見て「なるほど、このレコードのこのカラムは UTC で保存されているっぽいな」と思い込んでいました。
core=> SELECT created_at_with_timezone FROM users LIMIT 1;
created_at_with_timezone
-------------------------------
2023-04-13 07:09:05.804634+00
(1 row)
ここまでの推測が仮に正しいとすると、タイムゾーンに関心がないサーバー側で timestamp without time zone
を使うのは無駄ではないかと思い、
仕様について調べたところ、実際には想像した挙動とまったく異なっていることがわかりました。
実際の timestamp with time zone
の挙動
実はtimestamp with time zone
はその名前に反してタイムゾーン情報を保持していません。
内部的には常に UTC 時間として保存されています。
PostgreSQL: Documentation: 17: 8.5.1.3. Time Stamps
For
timestamp with time zone
, the internally stored value is always in UTC (Universal Coordinated Time, traditionally known as Greenwich Mean Time, GMT).
つまり、INSERT
文で保存する値としてtimestamp with time zone '2024-01-01 00:00:00+09'
を指定しようが、
timestamp with time zone '2023-12-31 15:00:00+00'
を指定しようが内部的には全く同じ扱いになります。
カラム、レコード、テーブル単位で「このカラム、このレコード、このテーブルではこのタイムゾーン」のような挙動もなく、内部的には常に一貫して UTC で保存されています。
では先ほどのSELECT
した結果の2023-04-13 07:09:05.804634+00
の+00
はなんだったのでしょうか?
これは「timestamp with time zone
の値を PostgreSQL クライアントにどのタイムゾーンで見せるか」というTimeZone
設定
(サーバーの設定でデフォルト値が決まっているが、クライアントとのコネクションごとに変更可能)によって変換して出力されたものです。
つまり単なるプレゼンテーションの話であり、この出力は「timestamp with time zone
がどう保存されているか」とは関係ありません。
PostgreSQL: Documentation: 17: 8.5.1.3. Time Stamps
When a
timestamp with time zone
value is output, it is always converted from UTC to the currenttimezone
zone, and displayed as local time in that zone. To see the time in another time zone, either changetimezone
or use theAT TIME ZONE
construct (see Section 9.9.4).
PostgreSQL: Documentation: 17: 8.5.3. Time Zones
All timezone-aware dates and times are stored internally in UTC. They are converted to local time in the zone specified by the TimeZone configuration parameter before being displayed to the client.
以下のようにコネクションのタイムゾーン設定を変えることで、絶対時間は同一のまま異なるタイムゾーンで表示できることがわかります。
core=> SHOW TimeZone;
TimeZone
----------
UTC
(1 row)
core=> SELECT created_at_with_timezone FROM users LIMIT 1;
created_at_with_timezone
-------------------------------
2023-04-13 07:09:05.804634+00
(1 row)
core=> SET SESSION TIME ZONE 'Asia/Tokyo';
SET
core=> SELECT created_at_with_timezone FROM users LIMIT 1;
created_at_with_timezone
-------------------------------
2023-04-13 16:09:05.804634+09
(1 row)
timestamp without time zone
はなんなのか
timestamp with time zone
の仕様はわかりました。
ではtimestamp without time zone
はどんなもので、なんのために存在するのでしょうか?
timestamp without time zone
はその名前が示す通りタイムゾーン情報を持たないタイムスタンプ型であり、
なおかつ PostgreSQL クライアントとのやり取りにおいてもタイムゾーンに関する変換を行いません。
core=> SHOW TimeZone;
TimeZone
----------
UTC
(1 row)
core=> SELECT created_at_without_timezone FROM users LIMIT 1;
created_at_without_timezone
----------------------------
2024-10-03 10:42:50.639281
(1 row)
core=> SET SESSION TIME ZONE 'Asia/Tokyo';
SET
core=> SELECT created_at_without_timezone FROM users LIMIT 1;
created_at_without_timezone
----------------------------
2024-10-03 10:42:50.639281
(1 row)
これの用途としては、おそらくそのシステムのローカル時間で保存したい場合に使うものと思われます。
しかしその場合「そのローカル時間はどのタイムゾーンのものなのか」という設定情報が
PostgreSQL の外側(大抵の場合 PostgreSQL クライアントとして動くアプリケーションプロセス)で管理されることになり、
データの整合性の面で大きな不安があります。
アプリケーションプロセスやシステムのタイムゾーン設定が変わると、データの解釈が変わってしまうということになりかねません。
そのため、レガシーシステムとの連携などの事情がない限り基本的にtimestamp with time zone
を使う方が良いでしょう。
timestamp with time zone
を採用し、これを適切に扱える PostgreSQL ドライバーや ORM を使っていれば、
PostgreSQL のTimeZone
設定やアプリケーションプロセスのタイムゾーンが変わろうが絶対的な時間が誤って解釈されることはないはずです。
まとめ
- なにか特殊な事情がない限り
timestamp with time zone
を使うのが良い - DB ドライバーや ORM を介してデータに触れる場合はこのあたりの事情は普段あまり意識しないが、「PostgreSQL はどのタイムスタンプ型でもタイムゾーン情報を保持しない」ということは覚えておきたい
- 直接 SQL を書く場合、PostgreSQL サーバーに接続後まずは
SET SESSION TIME ZONE 'Asia/Tokyo';
を実行しておくと、以後のクエリでのタイムスタンプ表示がわかりやすくなる