MySQLで日時を入れる列を作るとき、DATETIMEとTIMESTAMPのどちらにするか迷った経験はないでしょうか。見た目はどちらも「2026-10-27 09:00:00」ですが、中身の扱いはかなり違います。大きな差は3つです。TIMESTAMPはタイムゾーンを変換して保存し、扱える範囲は2038年1月までに限られます。DATETIMEは入れた値をそのまま保存し、9999年まで扱えます。この記事では、2つの型の違いを実際に動くSQLで確かめ、用途ごとの選び方を表に整理します。
このシナリオで考える
イベント予約サービスのテーブルを例に考えます。開催日時(start_at)と、レコードを作った日時(created_at)を持つシンプルな構成です。違いを確かめるため、同じ日時をDATETIME列とTIMESTAMP列の両方に入れるテーブルも用意します。
CREATE TABLE events (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
start_at DATETIME NOT NULL, -- 開催日時
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 比較用:同じ値を両方の型に入れる
CREATE TABLE tz_demo (
id INT PRIMARY KEY,
dt DATETIME,
ts TIMESTAMP NULL
);
以降の例は、MySQL 8.0の標準設定(strictモード有効)を前提に進めます。
DATETIMEとTIMESTAMPの違いを表で比べる
まずは仕様の違いを一覧で押さえましょう。数字はMySQL 8.0公式リファレンスの記載にもとづきます。
| 項目 | DATETIME | TIMESTAMP |
|---|---|---|
| 扱える範囲 | 1000-01-01 00:00:00 〜 9999-12-31 23:59:59 | 1970-01-01 00:00:01 〜 2038-01-19 03:14:07(UTC) |
| タイムゾーン変換 | しない(入れた値のまま) | 保存時にUTCへ、取得時に接続のタイムゾーンへ変換 |
| サイズ | 5バイト | 4バイト |
| 小数秒 | 最大6桁(DATETIME(6)) | 最大6桁(TIMESTAMP(6)) |
| DEFAULT CURRENT_TIMESTAMP | 使える | 使える |
小数秒を付けると、桁数に応じて最大3バイト増えます。たとえばDATETIME(3)は7バイトです。サイズの差は1バイトしかないため、選ぶ決め手にはなりません。実務で効いてくるのは「タイムゾーン変換」と「範囲」の2点です。
TIMESTAMPはタイムゾーンで見え方が変わる
TIMESTAMPは、接続ごとのタイムゾーン設定(time_zone変数)にしたがって値を変換します。日本時間で入れた値を、UTCの接続で読むとどうなるか試してみましょう。
SET time_zone = '+09:00'; -- 日本時間で接続している想定
INSERT INTO tz_demo VALUES (1, '2026-10-27 09:00:00', '2026-10-27 09:00:00');
SET time_zone = '+00:00'; -- UTCの接続に切り替える
SELECT dt, ts FROM tz_demo WHERE id = 1;
+---------------------+---------------------+
| dt | ts |
+---------------------+---------------------+
| 2026-10-27 09:00:00 | 2026-10-27 00:00:00 |
+---------------------+---------------------+
DATETIMEは入れた文字どおりの9時のままです。一方、TIMESTAMPは「日本時間の9時=UTCの0時」として読み替えられました。TIMESTAMPは内部でUTCとして持っているため、同じ瞬間を接続先の時刻で表示してくれます。
この性質は、使い方しだいで長所にも短所にもなります。
- 長所:海外拠点など、タイムゾーンが違う利用者にも「同じ瞬間」を正しく見せられる
- 短所:アプリサーバーごとにtime_zoneがずれていると、同じ行でも表示される時刻が変わる
なお、MySQL 8.0.19以降は、値の末尾にオフセットを付けて入れることもできます。
-- 「日本時間の9時」と明示して入れる(MySQL 8.0.19以降)
INSERT INTO tz_demo VALUES (2, '2026-10-27 09:00:00+09:00', '2026-10-27 09:00:00+09:00');
この場合、DATETIME列には接続のタイムゾーンに直した値が入ります。UTCの接続なら「00:00:00」です。DATETIMEがタイムゾーン情報を持てるわけではない点に気をつけてください。
2038年問題:TIMESTAMPは範囲外の値を入れられない
TIMESTAMPの上限は「2038-01-19 03:14:07(UTC)」です。内部でUNIX時間(1970年からの秒数)を32ビットの範囲で持つため、これより先の日時は扱えません。日本時間に直すと、2038年1月19日12時14分7秒が上限になります。
SET time_zone = '+00:00';
INSERT INTO tz_demo VALUES (3, '2040-04-01 00:00:00', '2040-04-01 00:00:00');
ERROR 1292 (22007): Incorrect datetime value: '2040-04-01 00:00:00' for column 'ts' at row 1
strictモードではエラーになり、行は入りません。DATETIME列だけなら問題なく入ります。
「作成日時なら過去の値しか入らないから大丈夫」と考えがちですが、注意したい列はほかにもあります。
- 契約の満了日や保証期限など、未来の日時を入れる列
- 「無期限」を表すために9999-12-31のような遠い日付を入れる列
- 2038年を過ぎても使い続ける長寿命のシステムの列
1と2はTIMESTAMPでは今すぐ壊れます。3も、残り10年ほどで確実に問題になると覚えてください。
用途別の選び方
迷ったときは、次の表を目安にしてください。
| 用途 | おすすめ | 理由 |
|---|---|---|
| イベント開催日時・予約日時(現地時刻) | DATETIME | 「現地の9時」という意味を変換せずに残したい |
| 契約満了日・有効期限 | DATETIME | 2038年以降の値が入る |
| 作成日時・更新日時(拠点が1つ) | どちらでも可 | time_zoneを統一していれば差は出にくい |
| 作成日時・更新日時(複数のタイムゾーン) | TIMESTAMP、またはUTCで入れるDATETIME | 「同じ瞬間」を基準にそろえたい |
| 誕生日・記念日 | DATE | 時刻がそもそも不要 |
新しく設計するなら、DATETIMEに統一し、値はアプリ側でUTCにそろえて入れる方針もよく採られます。2038年問題を避けつつ、タイムゾーンの扱いをアプリのコードで一か所にまとめられるためです。
よくある落とし穴
型そのものより、設定や関数との組み合わせでつまずくケースが多くあります。
- NOW()も接続のタイムゾーンで返る:DATETIME列にDEFAULT CURRENT_TIMESTAMPを付けると、接続ごとのtime_zoneの時刻が入ります。サーバーによって日本時間とUTCが混ざる原因になります。
- ‘Asia/Tokyo’がエラーになる:名前でタイムゾーンを指定するには、タイムゾーン表を読み込んでおく必要があります(mysql_tzinfo_to_sql)。読み込んでいない環境では’+09:00’のようにオフセットで指定しましょう。
- 古い記事の自動更新の説明:MySQL 8.0はexplicit_defaults_for_timestampが既定で有効です。「最初のTIMESTAMP列が勝手に自動更新される」動きは、明示しない限り起きません。
- 小数秒の精度をそろえる:TIMESTAMP(6)にはCURRENT_TIMESTAMP(6)のように、同じ桁数を指定します。
-- 精度をそろえた作成日時・更新日時の定義
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
ON UPDATE CURRENT_TIMESTAMP(6)
PostgreSQLでは、timestamp(タイムゾーンなし)とtimestamptz(タイムゾーンあり)の2種類があります。timestamptzはUTCで保存しつつ範囲も広いため、2038年問題はありません。
まとめ
DATETIMEは入れた値をそのまま9999年まで保存し、TIMESTAMPはUTCに変換して2038年1月までを扱います。現地時刻の意味を残したい列や、未来の日付が入る列はDATETIMEが安全です。どちらを使う場合も、接続のtime_zoneをそろえておくことが、時刻のずれを防ぐいちばんの近道になります。
参考リンク
- MySQL 8.0 リファレンス: The DATE, DATETIME, and TIMESTAMP Types
- MySQL 8.0 リファレンス: Automatic Initialization and Updating for TIMESTAMP and DATETIME
- MySQL 8.0 リファレンス: Data Type Storage Requirements
- MySQL 8.0 リファレンス: MySQL Server Time Zone Support
アイキャッチ画像: Photo by insung yoon on Unsplash
