時刻とタイムゾーン — 保存した「0時」は、どこの0時か
毎日0時から9時までの注文が、前日の売上に計上されていた。時刻には「瞬間」「壁の時計の時刻」「日付」の3種類があり、混ぜた瞬間に9時間ずれる。
先に読んでおくとよい記事
この記事の進み方
What — 時刻には3つの種類がある
「時刻」とひとまとめに呼んでいる値には、性質の違う3つの種類があります。混ぜると、ずれます。
世界中で同じ1点を指す
2026-11-30T15:00:00Z = 2026-12-01T00:00:00+09:00- 例
- 注文した時刻、ログの時刻、決済の時刻
- タイムゾーン
- **値に含まれる(または UTC で表す)**
- 比較・計算
- そのままできる
- 表示
- 見る人のタイムゾーンに変換して出す
- 注意
- —
- 保存
- —
- –上の2つは、書き方が違うだけで同じ瞬間
- –UTC で保存しておけば、どこから見ても同じ1点を指す
- –冒頭の注文の時刻はこれ。値は正しく保存されていた
どれも「日時っぽい値」に見えるが、意味が違う。保存する前に、その値がどれなのかを決める。
- UTC
- 協定世界時。世界の時刻の基準。日本時間は UTC より9時間進んでいる(+09:00)。
- オフセット
- UTC からの差。+09:00 など。ある瞬間の差を表すだけで、夏時間の決まりは含まない。
- タイムゾーン
- Asia/Tokyo のような地域の名前。その地域の時差と夏時間の決まり、その変更の歴史を含む。
- 夏時間
- 季節によって時計を1時間進める制度。切り替わる日には、存在しない時刻と2回来る時刻がある。
冒頭の事故で、何が失われたか
- 1利用者が注文する日本時間 12月1日 8:00軽い
- 2サーバーが時刻を記録するUTC 11月30日 23:00軽い
- 3DB に保存するタイムゾーンの情報が無い型中
- 4日付を取り出すDATE(created_at) = 11月30日重い
- 5日次の売上に数える11月30日の売上重い
集計の段で「日本の暦で日付を取り出す」と決めていれば、ほかの段はそのままでずれない。
どの段も、受け取った値を正しく扱っている。「どこの時刻か」という情報だけが、どこかで落ちて、最後の段で UTC の暦が使われた。
注文時刻 created_at は UTC で保存しています。日本時間の11月30日の売上を集計するつもりで WHERE DATE(created_at) = '2026-11-30' と書きました。集計に入るのはどの注文ですか?
Why — なぜ時刻はずれるのか
時刻の値がずれる原因は、ほとんどの場合、計算の誤りではなく、「どこの時刻か」という情報が、途中のどこかで落ちることです。
値は受け渡しのたびに、文字列になり、型が変わり、別の言語に渡ります。その途中で、
- タイムゾーンを持たない文字列に変換される(
"2026-11-30 23:00") - タイムゾーンを持たない DB の型に保存される
- 受け取った側が、自分のタイムゾーン(サーバーの設定、ブラウザの設定)で解釈する
と、同じ文字列が、受け取った場所によって別の瞬間を指すようになります。
同じ文字列が、書き方しだいで9時間ずれる
JavaScript の Date は、この「どこの時刻か」の扱いが、文字列の書き方によって変わります。
new Date("2026-11-30") // 日付だけ → UTC の 0時 として解釈
new Date("2026-11-30T00:00") // 時刻付き → 実行環境のタイムゾーンの 0時 として解釈
日本時間の環境では、この2つは9時間違う瞬間になります。仕様どおりの動きですが、見た目がほとんど同じなので、画面の入力をそのまま渡すと気づきません。
日本時間に設定されたブラウザで、new Date('2026-11-30') と new Date('2026-11-30T00:00') を作りました。2つの瞬間の差はどれですか?
夏時間のある地域では、存在しない時刻がある
日本には夏時間がありませんが、利用者や取引先が海外にいると避けられません。
ニューヨークでは、2027年3月14日の午前2時に時計が3時に進みます。その日の 2:30 は存在しません。 秋に戻る日には、1:30 が2回来ます。
- 「毎日 2:30 に実行」は、その日は実行されないか、3:30 にずれる
- 「24時間後」と「翌日の同じ時刻」は、その日だけ一致しない
- 先の日付の予約を、いまの決まりで UTC に変換して保存すると、国が夏時間の制度を変えたときに、予約の時刻が1時間ずれる
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
「11月30日 23時59分まで」のキャンペーンの終了を、どう保存し、どう判定しますか?
- キャンペーンは日本の利用者向け。終了は日本時間で決まっている
- 運営者は管理画面から、日付と時刻を入力して終了を設定する
- サーバーは UTC で動いている。複数の言語のサービスが同じ値を読む
- 利用者は海外から日本のサイトを使うこともある
- 「23時59分まで」の最後の1分に注文した人にも割引を適用したい
- · 終了は3つの種類(瞬間・壁の時計・日付)のどれか
- · 入力された「23:59」が、どこの時計の時刻かを、どこで確定させるか
- · 終わりの境界を「以下」で持つか「未満」で持つか
経理の担当者に、日次売上が毎日ずれていた理由と、過去の数字をどう読めばよいかを説明してください
- 相手は会計に詳しいが、システムの時刻の仕組みは知らない
- ずれは毎日0時から8時59分の注文が、前日に入っていたこと
- 修正は来週入り、過去の数字も作り直せる
- 今月の締めの作業は今週中に行う必要がある
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。