PostgreSQLのtimestampカラムには一体何が格納されているのか?
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
すべてのツールはブラウザ上で完全に動作します。データがあなたのマシンを離れることはありません。
まとめ
- PostgreSQLの2つの時間型はどちらもマイクロ秒カウンター。ラベルの有無だけが違い
- 型を間違えると、混乱するタイムスタンプと誤った計算の原因になる
- 適切なツールでテスト・変換・検証すれば、何時間ものデバッグ時間を節約できる