2026年1月21日·1分で読めます

TIMESTAMPTZ と TIMESTAMP:PostgreSQL のダッシュボードと API

PostgreSQL の TIMESTAMPTZ と TIMESTAMP:どちらを選ぶかがダッシュボード、API レスポンス、タイムゾーン変換、夏時間(DST)バグにどう影響するか。

TIMESTAMPTZ と TIMESTAMP:PostgreSQL のダッシュボードと API

本当の問題:一つの出来事、複数の解釈

出来事は一度だけ起きますが、それが報告される方法は多数あります。データベースは値を保存し、API はそれをシリアライズし、ダッシュボードは集計し、各ユーザーは自分のタイムゾーンで表示します。どれかの層で異なる前提が使われると、同じ行が異なる瞬間に見えてしまいます。

だから TIMESTAMPTZTIMESTAMP の違いは単なる型の好みではありません。保存された値が特定の瞬間(instant)を表すのか、特定の場所でしか意味を持たない壁時計(wall-clock)時間を表すのかを決めます。

最初に壊れやすいのは次のようなケースです。販売ダッシュボードがニューヨークとベルリンで日次合計を異なって表示する、時間ごとのチャートで夏時間(DST)切替時に1時間が抜けたり重複したりする、監査ログが順序通りに見えない(同じ日付だが実際の瞬間が違う)など。

単純なモデルでトラブルを避けられます。

  • 保存(Storage): PostgreSQL に保存するものとそれが何を表すか。
  • 表示(Display): UI、エクスポート、レポートでどのようにフォーマットするか。
  • ユーザーのロケール(User locale): 見る人のタイムゾーンやカレンダー規則(DST を含む)。

これらを混同すると静かなレポーティングのバグになります。サポートチームがダッシュボードから「昨日作成されたチケット」をエクスポートし、API レポートと比較すると、両方とも合理的に見えるが、片方は閲覧者のローカルな真夜中境界を使い、もう片方は UTC を使っていた、ということが起きます。

目的は簡単です:各時刻値について、保存するものと表示するものの二つの選択を明確にすること。データモデル、API レスポンス、ダッシュボード全体で同じ明確さを保ち、誰もが同じタイムラインを見るようにします。

TIMESTAMPTIMESTAMPTZ が実際に意味するもの

PostgreSQL では名前が誤解を招きます。名前は保存されるものを説明しているように見えますが、実際は PostgreSQL が入力をどう解釈し出力をどう整形するかを示しています。

TIMESTAMP(別名 timestamp without time zone)はカレンダー日付と時計時刻だけで、例えば 2026-01-29 09:00:00 のようなものです。タイムゾーンは付いていません。PostgreSQL は自動で変換しません。異なるタイムゾーンにいる二人が同じ TIMESTAMP を見て異なる実世界の瞬間だと解釈する可能性があります。

TIMESTAMPTZ(別名 timestamp with time zone)は実際の瞬間を表します。インスタントだと考えてください。PostgreSQL は内部的に正規化(事実上 UTC に)してから、セッションのタイムゾーンに合わせて表示します。

驚きの多くは次の振る舞いに由来します。

  • 入力時(On input): PostgreSQL は TIMESTAMPTZ 値を単一の比較可能な瞬間に変換します。
  • 出力時(On output): PostgreSQL はその瞬間を現在のセッションタイムゾーンを使って整形して表示します。
  • TIMESTAMP の場合: 入力・出力の両方で自動変換は行われません。

小さな例で違いを示します。アプリがユーザーから 2026-03-08 02:30 を受け取ったとします。TIMESTAMP 列に挿入すると、PostgreSQL は正確にその壁時計の値を保存します。そのローカル時間が DST ジャンプで存在しない場合、報告で壊れるまで気づかないかもしれません。

TIMESTAMPTZ に挿入する場合、PostgreSQL はその値を解釈するためのタイムゾーンが必要です。例えば 2026-03-08 02:30 America/New_York を与えると、PostgreSQL はそれを瞬間に変換します(ルールや正確な値によってはエラーになることもあります)。後でロンドンのダッシュボードが違うローカル時刻を表示しても、それは同じ瞬間です。

よくある誤解:人々は "with time zone" を見て、PostgreSQL が元のタイムゾーンラベルを保存すると期待しますが、そうではありません。PostgreSQL はラベルではなく瞬間を保存します。ユーザーの元のタイムゾーンラベルを表示に使いたいなら、そのゾーンは別にテキストフィールドとして保存してください。

セッションタイムゾーン:多くの驚きの裏にある隠れた設定

PostgreSQL には表示内容を静かに変える設定があります:セッションタイムゾーンです。同じデータに対して同じクエリを実行しても、セッションが異なるタイムゾーンを使っていると表示される時刻が異なることがあります。

これは主に TIMESTAMPTZ に影響します。PostgreSQL は絶対的な瞬間を保存し、セッションタイムゾーンで表示します。TIMESTAMP(タイムゾーンなし)の場合、値は単なるカレンダー時刻として扱われ表示時にシフトされませんが、TIMESTAMPTIMESTAMPTZ に変換したり、タイムゾーンあり値と比較したりするときにセッションタイムゾーンが問題になることがあります。

セッションタイムゾーンは気づかないうちに設定されることが多いです:アプリ起動時の設定、ドライバのパラメータ、コネクションプールが古いセッションを使い回す、BI ツールのデフォルト、ETL ジョブがサーバのロケール設定を継承する、あるいは手動の SQL コンソールがノートパソコンの設定を使う、などです。

チームが論争になる典型はこうです。あるイベントが TIMESTAMPTZ 列に 2026-03-08 01:30:00+00 として保存されているとします。America/Los_Angeles のダッシュボードセッションはそれを現地時間で前日の夜として表示し、API の UTC セッションは別の時計時刻を表示します。もしチャートがセッションローカルな日でグループ化するなら、日次合計が異なることがあります。

-- Make your output consistent for a reporting job
SET TIME ZONE 'UTC';

SELECT created_at, date_trunc('day', created_at) AS day_bucket
FROM events;

レポートや API レスポンスを生成する何かには、タイムゾーンを明示してください。接続時に設定する(または最初に SET TIME ZONE を実行する)、機械向け出力の標準を決める(多くは UTC)、ローカルなビジネス時間のレポートではジョブ内でビジネスゾーンを設定し、誰かのノートパソコンに依存しないようにします。プールされた接続を使う場合は、接続がチェックアウトされたときにセッション設定をリセットしてください。

ダッシュボードが壊れる仕組み:グルーピング、バケット、DST のギャップ

ダッシュボードは単純に見えます:日ごとの注文数、時間ごとのサインアップ、週次比較。問題は、データベースが一つの“瞬間”を保存していても、チャートがそれを見る人によって複数の“日”に変換してしまうことから始まります。

ユーザーのローカルタイムゾーンで日ごとにグループ化すると、同じイベントが二人で別の日になることがあります。ロサンゼルスで 23:30 に行われた注文はベルリンではすでに「翌日」です。また、プレーンな TIMESTAMP に対して DATE(created_at) でグループ化すると、実際の瞬間ではなくタイムゾーンのない壁時計の読みでグループ化していることになります。

DST 周辺では時間ごとのチャートがさらに複雑になります。春にはあるローカル時間が存在しないためチャートにギャップが出ます。秋にはあるローカル時間が二度現れるため、どの 01:30 を意味するかでバケットが重複し、スパイクや二重カウントが生じます。

実用的な問いはこうです:あなたは「実際の瞬間(real moments)」をチャート化しているのか(安全に変換できる)、それとも「ローカルのスケジュール時間」をチャート化しているのか(変換してはいけないのか)。ダッシュボードはほとんどの場合、実際の瞬間を扱います。

UTC とビジネス用タイムゾーンのどちらでグループ化するべきか

1つのルールを選び、それを全部に適用してください(SQL、API、BI ツール)。そうしないと合計がずれます。

グローバルで一貫した系列が欲しい場合は UTC でグループ化します(システム稼働状況、API トラフィック、世界的なサインアップ)。「営業日」など法的・運用上の意味がある場合はビジネスのタイムゾーンでグループ化します(店舗の日、サポート SLA、会計)。個人用のフィードなどパーソナライズが比較可能性より重要なら閲覧者のタイムゾーンでグループ化します。

ビジネス日で一貫性を保つパターンは次の通りです。

SELECT date_trunc('day', created_at AT TIME ZONE 'America/New_York') AS business_day,
       count(*)
FROM orders
GROUP BY 1
ORDER BY 1;

不信感を防ぐラベル表示

数字が飛んだりジャンプしたりして説明がないと人はチャートを信用しなくなります。UI 上でルールを明確にラベル表示してください:「Daily orders (America/New_York)」や「Hourly events (UTC)」のように。エクスポートや API でも同じルールを使ってください。

レポーティングと API のための簡単なルールセット

Own your generated source code
Get production-ready Go, Vue3, and native mobile code you can deploy or self-host.
Generate Code

その値が「実際に起きた瞬間」を表すのか「正確に書かれたままにしたいローカル時刻」なのかを決めてください。この二つを混ぜるとダッシュボードと API が食い違い始めます。

予測可能なレポーティングを保つためのルールセット:

  • 実世界のイベントは TIMESTAMPTZ にインスタントとして保存し、UTC を事実上の真実源と見なす。
  • 「請求日」などのビジネス概念は DATE(または本当に壁時計時間が必要ならローカル時間フィールド)として別に保存する。
  • API では ISO 8601 でタイムスタンプを返し、一貫性を保つ:常にオフセット(例:+02:00)を含めるか、常に UTC の Z を使う。
  • 変換はエッジで行う(UI とレポート層)。データベース内やバッチ処理で頻繁に往復変換しない。

なぜこれが有効か:ダッシュボードは範囲をバケット化して比較します。インスタントを保存しておけば、PostgreSQL は DST シフトがあってもイベントを確実に並べ替え・フィルタできます。表示やグルーピング方法は後から選べます。ローカルの時計時間(TIMESTAMP)だけを保存すると PostgreSQL はそれが何を意味するか分からないため、セッションタイムゾーンが変わるとグルーピング結果が変わります。

「ローカルのビジネス日」はインスタントではないので別にしておいてください。"Deliver on 2026-03-08" は日付の決定であって瞬間ではありません。タイムスタンプに無理に押し込むと DST の日で時間が抜けたり重複したりし、後でギャップやスパイクとなって現れます。

ステップバイステップ:各時刻値にふさわしい型を選ぶ

Make time handling consistent
Build your backend and API from one place so timestamp rules stay consistent.
Try AppMaster

TIMESTAMPTZTIMESTAMP の選択は一つの質問から始まります:この値は「実際に起きた瞬間」を表すか、それとも「書かれたまま保持したいローカル時刻」か?

1) 実イベントとスケジュールのローカル時間を分ける

列の棚卸を行ってください。

実イベント(クリック、支払い、ログイン、出荷、センサーデータ、サポートメッセージ)は通常 TIMESTAMPTZ に保存すべきです。異なるタイムゾーンから見ても一意の瞬間が欲しいためです。

スケジュールされたローカル時間は別です:「店舗は 09:00 に開店」「ピックアップ窓口は 16:00–18:00」「請求は現地時間で毎月 1 日 10:00 に実行」など。これらは場所の壁時計に紐づく意図なので、TIMESTAMP と別のタイムゾーンまたはロケーションフィールドを組み合わせて保存する方が良いことが多いです。

2) 基準を決めて文書化する

ほとんどのプロダクトに対する良いデフォルトは:イベント時刻は UTC に保存し、表示はユーザーのタイムゾーンで行う、と記載することです。スキーマノート、API ドキュメント、ダッシュボード説明など、実際に人が読む場所に書いてください。また「ビジネス日」が何を意味するか(UTC 日なのかビジネスゾーン日なのか閲覧者ローカル日なのか)を定義してください。これは日次レポートに直接影響します。

現場で使えるチェックリスト:

  • 各時刻カラムに「event instant」か「local schedule」かをタグ付けする。
  • イベントインスタントはデフォルトで UTC に保存する TIMESTAMPTZ にする。
  • スキーマ変更時はバックフィルを慎重に行い、サンプル行を手で検証する。
  • API フォーマットを標準化する(インスタントは常に Z かオフセットを含める)。
  • ETL ジョブ、BI コネクタ、バックグラウンドワーカーでセッションタイムゾーンを明示的に設定する。

「変換してバックフィルする」作業は注意してください。カラム型を変更すると、以前の値が異なるセッションタイムゾーンの下で解釈されていた場合に意味が静かに変わることがあります。

日付のずれや DST バグを引き起こすよくあるミス

多くの時間のバグは「PostgreSQL が変だ」というより、見た目は正しい値を間違った意味で保存し、異なる層が欠けている文脈を推測することから生じます。

ミス 1: 壁時計時間を絶対時刻のように保存する

典型的な罠は、ローカルの壁時計時間(例:「ベルリンの 2026-03-29 09:00」)を TIMESTAMPTZ に保存してしまうことです。PostgreSQL はそれを瞬間として扱い、現在のセッションタイムゾーンに基づいて変換します。もし意図が「常に現地の 9 時」を意味していたなら、意味を失ってしまいます。閲覧時に別のセッションタイムゾーンで表示すると時刻がシフトします。

予約や予定の場合は、ローカル時間を TIMESTAMP として保存し、別にタイムゾーン(または場所)フィールドを持たせてください。起こった出来事(支払い、ログインなど)は TIMESTAMPTZ に保存します。

ミス 2: 環境ごとに異なる前提を持つ

ローカルのノートパソコン、ステージング、本番で同じタイムゾーン設定を共有していないことがあります。一方は UTC、他方はローカルタイムで動いていると、日次集計が食い違います。データは変わっていないのに、セッション設定が変わっただけです。

ミス 3: タイム関数が何を保証するかを知らずに使う

now()current_timestamp はトランザクション内で安定です。clock_timestamp() は呼び出すたびに変わります。トランザクション内で複数箇所でタイムスタンプを生成し、これらを混ぜると順序や期間が奇妙に見えることがあります。

ミス 4: 二重変換(または未変換)をしてしまう

よくある API バグ:アプリがローカル時間を UTC に変換して送信し、データベース側のセッションが入力をローカルと解釈してさらに変換してしまう。逆も起きます:アプリがローカル時間を Z(UTC) とラベルして送ってしまい、表示時にシフトする。

ミス 5: 意図したタイムゾーンを述べずに日付でグループ化する

「日次合計」はどの境界のことかで変わります。TIMESTAMPTZ に対して date(created_at) でグループ化すると、結果はセッションタイムゾーンに従います。深夜近くのイベントは前日や翌日に移動します。

ダッシュボードや API を出荷する前に基本をサニティチェックしてください:チャートごとに報告用タイムゾーンを一つ選んで一貫して適用する、API ペイロードにオフセット(または Z)を含める、ステージングと本番でタイムゾーンポリシーを揃える、グルーピング時にどのタイムゾーンを意味するか明記する、などです。

ダッシュボードや API を出荷する前の簡単チェック

Show the right local time
Build web and mobile interfaces that display times in the user’s locale correctly.
Create App

時間のバグはほとんど単一の間違ったクエリから起きるわけではありません。保存、報告、API の各層がそれぞれ少しずつ異なる前提を持つことで起きます。

出荷前チェックリスト:

  • 実世界のイベント(サインアップ、支払い、センサーピングなど)は TIMESTAMPTZ にインスタントとして保存する。
  • ビジネスローカルの概念(請求日、報告日など)は DATETIME として保存し、そのままにしておく。
  • 定期ジョブやレポートランナーではセッションタイムゾーンを明示的に設定する。
  • API レスポンスにはオフセットか Z を含め、クライアントがタイムゾーンありとして解析することを確認する。
  • ターゲットタイムゾーンの DST 切替週をテストしてエッジケースを検出する。

短時間でのエンドツーエンド検証:既知のエッジケース(例:DST を観測するゾーンの 2026-03-08 01:30)を一つ選び、保存、クエリ出力、API JSON、最終チャートのラベルまで追跡してください。チャートが正しい日を示すがツールチップが時間を誤っている(またはその逆)なら、変換の不一致があることが分かります。

例:同じ日の数字で二つのチームが意見を異にする理由

Launch with secure access
Start with built-in authentication and focus on your data and reporting rules.
Add Auth

サポートチーム(ニューヨーク)とファイナンスチーム(ベルリン)が同じダッシュボードを見ています。データベースサーバは UTC で動いています。誰もが自分の数字が正しいと主張しますが、「昨日」が人によって違います。

あるイベントの例:顧客のチケットがニューヨークの 3 月 10 日 23:30 に作成されました。これは UTC で 3 月 11 日 04:30、ベルリンでは 05:30 です。一つの実際の瞬間に対して、三つの異なるカレンダー日があります。

もし作成時刻が TIMESTAMP(タイムゾーンなし)で保存され、アプリがそれを「ローカル」と仮定して扱っていると、静かに履歴を書き換えてしまうことがあります。ニューヨークは 2026-03-10 23:30 をニューヨーク時間だと解釈し、ベルリンは同じ保存値をベルリン時間だと解釈するかもしれません。同じ行が異なる日付に入ります。

TIMESTAMPTZ に保存されていれば、PostgreSQL は瞬間を一貫して保存し、見る人がフォーマットするときにのみ変換します。だから TIMESTAMPTZTIMESTAMP の違いはレポートでの「一日」の意味を変えます。

対処パターン:

  1. イベント時刻を TIMESTAMPTZ として保存する。
  2. レポートのルールを決める:閲覧者ローカル(個人ダッシュボード)か、会社全体のビジネスゾーンか。
  3. 選んだルールでクエリ時に報告日を計算する:インスタントをそのゾーンに変換してから日付を取る。

次のステップ:スタック全体で時間処理を標準化する

時間処理が書面化されていないと、新しいレポートごとに推測ゲームになります。データベース、API、ダッシュボード全体で平凡で予測可能な時間の振る舞いを目標にしてください。

短い「時間に関する契約(time contract)」を作り、次の三つに答えさせてください。

  • イベント時刻の標準: イベントは原則 TIMESTAMPTZ(通常は UTC)で保存する。特別な理由がない限りはこれを採用する。
  • ビジネス用タイムゾーン: レポートで「日」「週」「月」を定義する際に使う一つのゾーンを選び、一貫して使う。
  • API フォーマット: タイムスタンプは常にオフセット(ISO 8601 の Z または +/-HH:MM)を含め、各フィールドが「瞬間(instant)」か「ローカル壁時計時間」かをドキュメント化する。

DST 開始と終了の周りに小さなテストを追加してください。高価なバグを早期に検出できます。例えば「日次合計」クエリがビジネスゾーンを固定した場合に DST 変化の間でも安定しているか、2026-11-01T01:30:00-04:002026-11-01T01:30:00-05:00 が別々のインスタントとして扱われるかを検証します。

マイグレーションは計画的に行ってください。型や前提をその場で変更すると、チャートの履歴が静かに書き換えられることがあります。より安全なアプローチは新しいカラムを追加することです(例:created_at_utc TIMESTAMPTZ)、レビューした変換でバックフィルし、読み取りを新しいカラムに移し、書き込みを更新する前に旧カラムと併存させて差分を確認します。

時刻の扱いに関する「time contract」をデータモデル、API、画面間で強制したければ、統一されたビルドセットアップが役立ちます。AppMaster (appmaster.io) は単一プロジェクトからバックエンド、ウェブアプリ、API を生成するため、タイムスタンプ保存と表示ルールをアプリ成長に合わせて一貫して維持しやすくなります。

よくある質問

When should I use TIMESTAMPTZ instead of TIMESTAMP?

Use TIMESTAMPTZ for anything that happened at a real moment (signups, payments, logins, messages, sensor pings). It stores one unambiguous instant and can be safely sorted, filtered, and compared across systems. Use plain TIMESTAMP only when the value is meant to be a wall-clock time that should stay exactly as written, usually paired with a separate time zone or location field.

What’s the real difference between TIMESTAMP and TIMESTAMPTZ in PostgreSQL?

TIMESTAMPTZ represents a real instant in time; PostgreSQL normalizes it internally and then displays it in your session time zone. TIMESTAMP is just a date and clock time with no zone attached, so PostgreSQL won’t shift it automatically. The key difference is meaning: instant versus local wall time.

Why do I see different times for the same row depending on who runs the query?

Because the session time zone controls how TIMESTAMPTZ is formatted on output and how some inputs are interpreted. Two tools can query the same row and show different clock times if one session is set to UTC and another to America/Los_Angeles. For reports and APIs, set the session time zone explicitly so results don’t depend on hidden defaults.

Why do daily totals change between New York and Berlin?

Because “a day” depends on a time zone boundary. If one dashboard groups by viewer-local time while another groups by UTC (or a business zone), late-night events can fall on different dates and change daily totals. Fix it by picking one grouping rule per chart (UTC or a specific business zone) and using it consistently in SQL, BI, and exports.

How do I avoid DST bugs like missing or duplicated hours in hourly charts?

DST creates missing or duplicated local hours, which can produce gaps or double-counted buckets when grouping by local time. If your data represents real moments, store it as TIMESTAMPTZ and choose a clear chart time zone for bucketing. Also test the DST transition week for your target zones to catch surprises early.

Does TIMESTAMPTZ store the user’s time zone?

No, PostgreSQL does not preserve the original time zone label with TIMESTAMPTZ; it stores the instant. When you query it, PostgreSQL displays it in the session time zone, which may differ from the user’s original zone. If you need “show it in the customer’s time zone,” store that zone separately in another column.

What should my API return for timestamps to avoid confusion?

Return ISO 8601 timestamps that include an offset, and be consistent. A simple default is to always return UTC with Z for event instants, then let clients convert for display. Avoid sending “naive” strings like 2026-03-10 23:30:00 because clients will guess the zone differently.

Where should time zone conversion happen: database, API, or UI?

Convert at the edges: store event instants as TIMESTAMPTZ, then convert to the desired zone when you display or bucket for reporting. Avoid converting back and forth inside triggers, background jobs, and ETL unless you have a clear contract. Most reporting problems come from double conversion or from mixing naive and time-zone-aware values.

How should I store business days and schedules like “run at 10:00 local time”?

Use DATE for business concepts that are truly dates, like “billing day,” “reporting date,” or “delivery date.” Use TIME (or TIMESTAMP plus a separate time zone) for schedules like “opens at 09:00 local time.” Don’t force these into TIMESTAMPTZ unless you really mean a single instant, because DST and zone changes can shift the intended meaning.

How can I migrate from TIMESTAMP to TIMESTAMPTZ without breaking reports?

First, decide whether it’s an instant (TIMESTAMPTZ) or a local wall time (TIMESTAMP plus zone), then add a new column instead of rewriting in place. Backfill with a reviewed conversion under a known session time zone, and validate sample rows around midnight and DST boundaries. Run old and new reports side by side briefly so any shifts in totals are obvious before you remove the old column.

始めやすい
何かを作成する 素晴らしい

無料プランで AppMaster を試してみてください。
準備が整ったら、適切なサブスクリプションを選択できます。

始める