※この記事にはアフィリエイト広告(プロモーション)が含まれます。
DATETIME は書いた日時をそのまま持ち、TIMESTAMP は UTC に直して持ちます。この記事の出力は MySQL 8.4.11(Ubuntu 26.04、time_zone=SYSTEM・system_time_zone=JST)で採りました。
まず見る(結論)
| DATETIME | TIMESTAMP | |
|---|---|---|
| time_zone を変えたとき | 同じ値のまま | そのタイムゾーンに直して表示 |
| 入る範囲 | 1000-01-01 00:00:00〜9999-12-31 23:59:59 |
UTC で 1970-01-01 00:00:01〜2038-01-19 03:14:07 |
| 範囲外を入れたとき | — | ERROR 1292(厳格モード) |
DEFAULT CURRENT_TIMESTAMP / ON UPDATE |
使える | 使える |
- 2038 年より先の日時を入れる列、タイムゾーンをまたがずに使う列は DATETIME
- アプリやサーバーごとにタイムゾーンが違い、同じ時点を指したい列は TIMESTAMP
time_zone を変えると TIMESTAMP だけ値が変わる
DATETIME 列 dt と TIMESTAMP 列 ts に、time_zone = '+09:00' の接続から同じ '2026-11-08 10:00:00' を入れ、time_zone を変えて読みました。
| 読んだときの time_zone | dt | ts | ts = dt |
|---|---|---|---|
+09:00 |
2026-11-08 10:00:00 |
2026-11-08 10:00:00 |
1 |
+00:00 |
2026-11-08 10:00:00 |
2026-11-08 01:00:00 |
0 |
-05:00 |
2026-11-08 10:00:00 |
2026-11-07 20:00:00 |
— |
UNIX_TIMESTAMP() で見ると、ts はどの time_zone でも 1794099600 です。dt は +09:00 で 1794099600、+00:00 で 1794132000 と、読む側のタイムゾーンで解釈されて 9 時間ずれました。WHERE の比較も読む側の time_zone で行われ、+00:00 の接続では WHERE ts = '2026-11-08 01:00:00' で同じ行が返りました。
サーバーの time_zone が SYSTEM のままなら、アプリのドライバーやセッション設定でタイムゾーンを変えない限り差は出ません。接続プールやバッチの片方だけ UTC にしている構成で問題になります。TIMESTAMP 列を使うなら、接続ごとの time_zone をそろえます。
‘Asia/Tokyo’ のような名前は既定では使えない
SET time_zone = 'Asia/Tokyo' は ERROR 1298 (HY000): Unknown or incorrect time zone: 'Asia/Tokyo' でした。mysql.time_zone_name が 0 行で、タイムゾーン表が読み込まれていないためです。'+09:00' の形なら通ります。
オフセット付きのリテラルは DATETIME でも変換される
'2026-11-08 10:00:00+00:00' を +09:00 の接続で入れると、dt も ts も 2026-11-08 19:00:00 になりました。オフセットそのものは残りません。
TIMESTAMP に入る範囲(2038 年問題)
| time_zone | 入れた値 | 結果 |
|---|---|---|
+00:00 |
2038-01-19 03:14:07 |
入る |
+00:00 |
2038-01-19 03:14:08 |
ERROR 1292 (22007): Incorrect datetime value |
+09:00 |
2038-01-19 12:14:07 |
入る |
+09:00 |
2038-01-19 12:14:08 |
ERROR 1292 |
+00:00 |
1970-01-01 00:00:00 |
ERROR 1292 |
+00:00 |
1970-01-01 00:00:01 |
入る |
上限・下限は UTC で決まっています。DATETIME 列には 9999-12-31 23:59:59 と 1000-01-01 00:00:00 がそのまま入りました。
SELECT では範囲外の値を返してもエラーになりません。SELECT DATE_ADD(ts, INTERVAL 20 YEAR) は 2046-11-08 10:00:00 を返しますが、同じ式で UPDATE すると ERROR 1292 です。
sql_mode を空にした接続では、2050-01-01 00:00:00 が警告 1264 Out of range value が出るだけでエラーにならず、0000-00-00 00:00:00 が入りました。
DEFAULT CURRENT_TIMESTAMP と ON UPDATE
CREATE TABLE a2 (
id int PRIMARY KEY, v int,
created_at timestamp DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
c_dt datetime DEFAULT CURRENT_TIMESTAMP,
u_dt datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
挿入から 2 秒後に v を書き換えると、updated_at と u_dt がどちらも 2 秒進みました。同じ値で UPDATE したとき(ROW_COUNT() が 0)は、どちらの列も変わりませんでした。
| 操作 | 結果 |
|---|---|
型だけ書いた ts timestamp |
timestamp NULL DEFAULT NULL になり、自動では入らない |
DEFAULT CURRENT_TIMESTAMP の列に NULL を明示して INSERT |
NULL が入る |
timestamp NOT NULL(既定値なし)に NULL を INSERT |
ERROR 1048: Column 'ts' cannot be null |
| 同じ列を省略して INSERT | ERROR 1364: Field 'ts' doesn't have a default value |
DEFAULT '0000-00-00 00:00:00' |
ERROR 1067: Invalid default value for 'ts' |
型だけ書いた最初の TIMESTAMP 列に現在時刻が自動で入るのは、explicit_defaults_for_timestamp が OFF の場合の動作です。8.4 の既定は ON(1)です。
DATETIME には式の既定値も書けます。d datetime DEFAULT (CURRENT_DATE + INTERVAL 7 DAY) は、11 月 8 日に INSERT すると 2026-11-15 00:00:00 が入りました。
小数秒は四捨五入される
| 列 | 入れた値 | 入った値 |
|---|---|---|
datetime |
10:00:00.789 |
10:00:01 |
datetime(3) |
10:00:00.12345 |
10:00:00.123 |
timestamp(6) |
10:00:00.1234567 |
10:00:00.123457 |
警告は出ません。切り捨てたいときは sql_mode に TIME_TRUNCATE_FRACTIONAL を足します。足した接続で datetime に .789 を入れると 10:00:00 が入りました。
小数秒付きの列では、既定値も列と同じ精度で書きます。timestamp(3) DEFAULT CURRENT_TIMESTAMP は ERROR 1067 で、DEFAULT CURRENT_TIMESTAMP(3) と書くと通りました。
型を変えるときの注意
TIMESTAMP を DATETIME に変えると、ALTER を実行した接続の time_zone で表示される値が DATETIME に書き込まれます。+09:00 で入れた 10:00:00 の列を +00:00 の接続で ALTER TABLE c MODIFY ts datetime すると、01:00:00 が残りました。+09:00 に戻して読んでも 01:00:00 のままです。ALTER は、アプリが普段使うのと同じ time_zone の接続で実行します。
DATETIME から TIMESTAMP へは、範囲外の行が1行でもあると ERROR 1292 で ALTER 全体が失敗し、表は元のままでした。
関連: MySQLの日付計算 / MySQLのDATE_FORMAT / PostgreSQLのtimestamptzとは