日時を扱っていて、「+09:00 を付けているのに1時間ずれる」「本番と手元で日付が変わる」といった不具合に当たったことがあると思います。原因の多くは、タイムゾーンとUTCオフセットを同じものとして扱っていることにあります。この2つは別の概念で、保存すべき場面も違います。
+09:00 は「ある瞬間のUTCとのずれ」を表す数値。Asia/Tokyo は「その地域が時刻をどう決めてきたかの履歴を含む規則の集合」。前者からは後者を復元できません。
オフセットは「その瞬間のずれ」でしかない
2026-08-03T10:00:00+09:00 という表記は、「この日時はUTCより9時間進んだ時刻である」と言っています。これはその1点についての情報で、その地域が常に +09:00 であるとは言っていません。
分かりやすいのは夏時間のある地域です。ニューヨークは冬が -05:00、夏が -04:00 です。-05:00 という値だけを保存していると、そこから「これはニューヨークの時刻だ」と決めることはできません。同じ -05:00 を使う地域はほかにもあるうえ、夏になれば同じニューヨークが別の値になるからです。
日本にも該当する期間があります。1948年から1951年まで日本には夏時間がありました。この期間の日本の夏の時刻は +10:00 です。過去の日時を扱うシステムで +09:00 を固定値として持っていると、この期間の変換を間違えます。
IANAタイムゾーン名は何を持っているか
Asia/Tokyo や America/New_York は、IANA タイムゾーンデータベース(tz database)で定義されている識別子です。これは単なるラベルではなく、その地域でオフセットがいつ何に変わったかの履歴を持っています。
| UTCオフセット(+09:00 等) | IANAタイムゾーン名(Asia/Tokyo 等) | |
|---|---|---|
| 表すもの | ある瞬間のずれ | 地域の時刻規則の履歴 |
| 夏時間 | 表現できない | 切り替え日時を含めて表現できる |
| 過去の日時 | 当時の規則を復元できない | 当時の規則で変換できる |
| 将来の規則変更 | 追随できない | データベース更新で追随する |
| 用途 | 「起きた瞬間」の記録 | 「どの地域の時刻か」の記録 |
保存するときの判断
過去に起きたことを記録する場合
UTCで保存します。ログイン時刻、注文日時、更新日時などがこれに当たります。起きた瞬間は1つに決まっており、後から規則が変わっても、その瞬間そのものは動きません。表示するときに利用者のタイムゾーンへ変換します。
-- 例(PostgreSQL)
created_at timestamptz NOT NULL DEFAULT now()
将来の予定を保存する場合
UTCに変換して保存してはいけません。「2027年10月1日の朝9時に会議」という予定をUTCに変換して保存すると、その地域の時刻規則が変わったときに、予定の時刻がずれます。実際に各国で夏時間の廃止や切り替え日の変更は起きています。
将来の予定は、現地の日時とIANAタイムゾーン名を分けて保存してください。
-- 例
starts_at_local timestamp -- 2027-10-01 09:00:00(タイムゾーンなし)
time_zone text -- 'Asia/Tokyo'
表示や通知のたびに、その時点の tz database を使って実際の瞬間を計算します。
ISO 8601 の書き方
| 表記 | 意味 |
|---|---|
2026-08-03T01:00:00Z | UTCの1時。Z は +00:00 と同じ意味 |
2026-08-03T10:00:00+09:00 | 上と同じ瞬間を、+09:00 の地域の時刻で表したもの |
2026-08-03T10:00:00 | どのタイムゾーンかが決まっていない。受け取る側の解釈次第で意味が変わる |
2026-08-03 | 日付だけ。時刻の情報を持たない |
APIで日時をやり取りするときは、末尾のオフセットを必ず付けてください。付いていない文字列は、送る側と受け取る側で解釈が食い違う典型的な原因です。
言語ごとの扱いを確認する
// JavaScript: タイムゾーンを指定して表示する
new Intl.DateTimeFormat('ja-JP', {
timeZone: 'Asia/Tokyo',
dateStyle: 'full',
timeStyle: 'long',
}).format(new Date('2026-08-03T01:00:00Z'));
// 実行環境のタイムゾーンを確認する
Intl.DateTimeFormat().resolvedOptions().timeZone; // 例: "Asia/Tokyo"
サーバのタイムゾーン設定に依存したコードは、本番と手元で結果が変わります。実行環境の設定に頼らず、明示的にタイムゾーンを指定するのが、この種の不具合を防ぐ一番確実な方法です。
よくある質問(FAQ)
Asia/Tokyo と +09:00 は同じではないのですか?
現在の日本ではどちらもUTCより9時間進んだ時刻を指しますが、意味が違います。+09:00 はある瞬間のずれを表す固定値で、Asia/Tokyo は「日本という地域の時刻の決まりの集合」です。日本には1948年から1951年まで夏時間があり、その期間の Asia/Tokyo は +10:00 でした。過去や未来の日時を扱うなら、地域名のほうが情報量が多くなります。
データベースには何を保存すべきですか?
起きた瞬間を記録するならUTCで保存し、表示するときに利用者のタイムゾーンへ変換するのが基本です。ただし「将来の予定」を保存する場合は別で、UTCに変換して保存すると、その地域の時刻の決まりが後から変わったときに予定がずれます。将来の予定は現地の日時とIANAタイムゾーン名を分けて保存してください。
ISO 8601 の Z は何を意味しますか?
UTCそのものを表す記号で、+00:00 と同じ意味です。2026-08-03T01:00:00Z は 2026-08-03T10:00:00+09:00 と同じ瞬間を指します。末尾に何も付いていない 2026-08-03T10:00:00 は、どのタイムゾーンの時刻なのかが決まっていないため、受け取る側の解釈によって意味が変わります。