PostgreSQL の timestamp vs timestamptz:内部で実際に何が保存されているのか?
PostgreSQLの timestamp と timestamptz は、どちらも1つの64ビット整数として格納されています。その値は1970-01-01 00:00:00 UTCからのマイクロ秒数です。違いが現れるのは、データを人間が読める形式にフォーマットするときだけです。
なぜハマりやすいのか?
- 2つのカラムに同じ日付を入れたのに、クエリ結果が違う
- あなたのアプリは
2025-07-29 10:00を挿入したのに、別のチームでは02:00と表示される - フロントエンドのISO文字列がバックエンドのログと一致しない
2つの桃缶:ラベルなしとラベル付き
| データ型 | 正式名称 | 格納値 | SELECT時の振る舞い |
|---|---|---|---|
timestamp | timestamp without time zone | 生のマイクロ秒カウント | そのまま返却 ― Postgresはタイムゾーンを推測しない |
timestamptz | timestamp with time zone | 同じマイクロ秒カウント | Postgresがセッションの TimeZone 設定を適用してからテキストを送信 |
たとえ話
timestamp= 産地ラベルのない桃の缶詰。中身がフルーツだとわかっても、どこで缶詰にされたかはわからない。timestamptz= 「原産地 UTC+9(JST)」と印刷された桃の缶詰。開ける人は自分で栄養表示を換算するかどうか決められる。
内部構造:ただの巨大な数値
2000-01-01 00:00:00 UTC → 0
2000-01-01 00:00:01 UTC → 1 000 000
- 単位:マイクロ秒(100万分の1秒)
- 範囲:紀元前4713年〜西暦294276年
timestampとtimestamptzのストレージは完全に同一。解釈の仕方だけが異なる
15秒で試せるデモ
-- クライアントは東京時間(JST)を使用
SET TimeZone = 'Asia/Tokyo';
CREATE TABLE demo (
created_ts timestamp,
created_tz timestamptz
);
INSERT INTO demo VALUES ('2025-07-29 10:00', '2025-07-29 10:00');
| クエリ | 結果 | 理由 |
|---|---|---|
SELECT created_ts FROM demo; | 2025-07-29 10:00:00 | 生の値がそのまま返される。タイムゾーン計算なし |
SELECT created_tz FROM demo; | 2025-07-29 10:00:00+09 | 出力時にタイムゾーンラベルが付与される |
SET TimeZone = 'UTC'; のあとSELECT | 2025-07-29 01:00:00+00 | 同じ瞬間を、別のレンズで見ている |
よくある落とし穴と対処法
1. ユーザーごとに違う時刻が見える
- 原因:クライアントごとに異なる
TimeZone設定でtimestamptzを読んでいる - 対処:すべて
timestampにして1つのタイムゾーンで統一するか、接続初期化時にSET TimeZone = 'UTC'を強制する
2.「壁時計の時刻」を保存するのに型を間違えた
- 業務カレンダー(営業時間、締切日)には
timestampを使う - 国際的なワークフロー(注文、ログ)は UTC で
timestamptzに格納する
3. APIのタイムゾーンがずれる
timestamptzは常にISO-8601形式でオフセット付き(Zまたは+09:00)の文字列として送信する- UIはユーザーのローカルタイムゾーンに合わせてフォーマットする
早見表:どちらを使うべき?
ローカルの予定表だけ → timestamp
グローバルに扱うもの → timestamptz(UTCで格納)
- 会計レポート、授業スケジュール →
timestamp - 監査ログ、ECの注文データ →
timestamptz
Go Toolsで手軽に検証
| やりたいこと | ツール | 使い方 |
|---|---|---|
| SQLのエポック値を確認 | タイムスタンプ変換ツール | 1690622400 を貼り付けて変換 |
| 2つのタイムゾーンをひと目で比較 | タイムゾーン変換ツール | 10:00 Asia/Tokyo と入力 |
| 時間フィールドを含むJSONを整形 | JSONフォーマッター | データを貼り付けて整形・確認 |
すべてのツールはブラウザ上で完全に動作します。データがあなたのマシンを離れることはありません。
よくある質問
PostgreSQL の timestamp と timestamptz の違いは何ですか?
timestamp(タイムゾーンなし)は日時値をそのまま保存し、タイムゾーン情報を持ちません。timestamptz(タイムゾーン付き)は入力を UTC に変換して保存し、取得時にセッションのタイムゾーンに再変換します。ほぼすべてのケースで timestamptz を使いましょう — 分散システムにおけるタイムゾーン関連のバグを防げます。
PostgreSQL は timestamptz にタイムゾーンを実際に保存していますか?
いいえ — 名前に反して、PostgreSQL はタイムゾーン自体を保存しません。入力を UTC に変換し、UTC 値(2000-01-01 からのマイクロ秒カウント)のみを保存します。取得時には、セッションの timezone 設定に基づいて UTC から変換します。元のタイムゾーン情報は破棄されます。
PostgreSQL セッションのタイムゾーンを変更するにはどうすればよいですか?
SET timezone = 'America/New_York'; を実行するとセッションのタイムゾーンが変更されます。これは timestamptz 値の表示と解釈に影響します。サーバー全体のデフォルトを変更するには postgresql.conf で timezone を設定します。曖昧さを避けるため、略称(JST など)ではなく IANA タイムゾーン名(Asia/Tokyo など)を常に使用してください。
イベント時刻の保存には timestamp と timestamptz のどちらを使うべきですか?
ほぼすべてのケースで timestamptz を使いましょう — ユーザー操作、API 呼び出し、監査ログ、スケジュールイベントなどに最適です。timestamp(タイムゾーンなし)は、特定の瞬間に紐づかない抽象的な時刻にのみ使用します。例えば「店舗は 09:00 に開店」はローカルタイムゾーンの午前 9 時を意味し、特定の UTC 時刻ではありません。
PostgreSQL の timestamptz は夏時間(DST)をどう処理しますか?
PostgreSQL は内部的にすべてを UTC で保存するため、timestamptz で夏時間を正しく処理します。値を取得する際、セッションタイムゾーンの現在の夏時間ルールに基づいて UTC から変換します。これにより、同じ UTC 時刻でも夏時間の切り替え前後で正しく異なるローカル時刻が表示されます。
まとめ
- PostgreSQLの2つの時間型はどちらもマイクロ秒カウンター。ラベルの有無だけが違い
- 型を間違えると、混乱するタイムスタンプと誤った計算の原因になる
- 適切なツールでテスト・変換・検証すれば、何時間ものデバッグ時間を節約できる