Laravel × MySQL のタイムゾーン地獄 ― 9時間ズレたDBと戦った日々
ある日、Carbonで日付操作をしようとしたら…
最近、Laravelのコードを改修していて、ある日時カラムでちょっとした条件検索を書いた。
「ここはスマートに Carbon でいけるでしょ」――そう思って Carbon::now() や addDays() をサクサク使い始めた。
…のに、実際にSQLを流してみるとなぜか9時間ズレる。
サーバは Asia/Tokyo、PHPもJST。なのに合わない。なんで。
掘ってみると出てきた“初期構築の罪”
調べていくうちに、原因はシンプルで、でも根深かった。
初期構築時、MySQLのtime_zoneはUTCのままだった ※これは問題ない
なのに、アプリはJSTの文字列をそのままDATETIMEに格納していた これはひどい。。
その状態で何年も運用を続けているようだ
結果、DBに保存されている日時は「実際より9時間進んだJST文字列(ただしUTC扱い)」という、ややこしい爆弾状態。
こうなると何が起きるか(地味に致命傷)
例えば本来JSTで「2025-08-08 15:00:00」に行われたイベントは、
DB上では「2025-08-08 15:00:00 (UTC)」と記録される。
DBの値をJSTに直すと「2025-08-09 00:00:00」と9時間未来に飛ぶ。
Carbon はタイムゾーンをきちんと意識してくれるが、
この“実体はJST文字列なのにUTCとして解釈されてる”データと向き合うと、計算結果がすぐに破綻する。
修正しようにも手遅れ感がすごい
「じゃあDBのtime_zoneをJSTに変えればいいじゃん」と思うかもしれない。
でも、それをやると既存の全レコードが“二重にズレる”地獄が待っている。
また、日時のデータを保持しているカラムは大量に存在する。
もう数年分のデータがこの状態で積み上がっているため、
アプリ側で吸収するしかない。
対応方針
新規の保存処理は正しくする
MySQLのtime_zoneをUTCにしたまま、UTCの日時でINSERTするよう修正。理想だが、既存のデータは変更できないので無理。既存データの読み取り時に補正
Carbonで扱うときに ->subHours(9) などでズレを補正するラッパを作成。時間比較のSQLは注意
DB内の値が“JST文字列(UTC扱い)”なので、WHERE句での比較条件はUTCに寄せるか、全件取得後にアプリで絞り込み。
正直、かなり面倒くさい
日時比較のたびに「これ、補正必要だっけ?」と考えないといけない。
しかも、開発メンバー全員がこの背景を理解してないと、またバグが生まれる。
これ、完全に“初期構築の罪”なんだけど、
運用が長くなるほど修正コストが指数関数的に膨れ上がるのを肌で感じた。
おわりに:タイムゾーンは最初に意識して決めよう!
今回の件で痛感したのはこれ。
DBとアプリのタイムゾーンを 最初にきっちり揃える
運用開始後に変えようとすると地獄を見る
「一旦動けばいい」は後で倍返しで返ってくる
タイムゾーン設定は軽く見られがちだけど、
日時データを扱うシステムでは生命線だ。
未来の自分を泣かせないためにも、最初の一手で間違えないようにしたい。
…って、私は立ち上げに関わってないので文句しか言えないんですけどね。
