MySQLのDATETIMEは「入れた日時をそのまま保存する型」、TIMESTAMPは「UTCに直して保存し、取り出すときにセッションのタイムゾーンへ戻す型」です。 扱える範囲も違い、DATETIMEは9999年まで、TIMESTAMPは2038年1月19日(UTC)までです。どちらも DEFAULT CURRENT_TIMESTAMP と ON UPDATE CURRENT_TIMESTAMP による自動設定が使えます。

この記事では、2つの型の違いを1枚の比較表にまとめたうえで、MySQL 8.0.46 で実際に SQL を流した結果を載せています。SET time_zone を切り替えて同じ行を読む実験、2038年を超える値を入れたときのエラー文、自動更新カラムの定義コピペ集、explicit_defaults_for_timestamp の挙動、そして「どちらを選ぶか」の判断フローまで、順番に確認できます。

DATETIMEとTIMESTAMPの違い早見表

比べる点 DATETIME TIMESTAMP
保存のしかた 入れた値をそのまま保存 UTCに変換して保存
取り出すとき そのまま返す セッションの time_zone に変換して返す
扱える範囲 1000-01-01 00:00:00 〜 9999-12-31 23:59:59 1970-01-01 00:00:01 UTC 〜 2038-01-19 03:14:07 UTC
日本時間(+09:00)での上限 9999-12-31 23:59:59 2038-01-19 12:14:07
サイズ(小数秒なし) 5バイト 4バイト
小数秒 (0)〜(6)、追加0〜3バイト (0)〜(6)、追加0〜3バイト
time_zone を変えたときの表示 変わらない 変わる
何も指定しないときの NULL(8.0 の既定) NULL 可・DEFAULT NULL NULL 可・DEFAULT NULL
DEFAULT CURRENT_TIMESTAMP 使える 使える
ON UPDATE CURRENT_TIMESTAMP 使える 使える
範囲外の値を入れたとき(厳密モード) ERROR 1292 ERROR 1292
向いている用途 予定日時・期限・遠い将来の日付・表示どおりに残したい値 複数のタイムゾーンから読む「記録された瞬間」

範囲とサイズ、UTC 変換の説明は、MySQL 8.4 リファレンスマニュアルの「The DATE, DATETIME, and TIMESTAMP Types」「Data Type Storage Requirements」の各ページを今回開いて確認しました。表示の変化・エラー・自動更新の行は、下の各セクションで実行結果を載せています。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「MySQL DATETIMEとTIMESTAMPの違い|2038年・タイムゾーン」

この記事の検証環境

実行結果はすべて次の環境で取得しました。

項目 値
MySQL 8.0.46(Ubuntu 24.04 のパッケージ)
sql_mode 既定値(STRICT_TRANS_TABLES、NO_ZERO_DATE などを含む)
explicit_defaults_for_timestamp 1(ON、8.0 の既定)
システムのタイムゾーン JST
クライアント mysql コマンド

MySQL 5.7、8.4、9.x は今回動かしていません(未実行)。5.7 との違いに触れる箇所は、その旨を明記しています。

タイムゾーンを切り替えると TIMESTAMP だけ表示が変わる

2つの型のいちばん大きな違いは、タイムゾーンの扱いです。同じ値を両方の型に入れ、セッションのタイムゾーンを切り替えながら読んでみます。

SET time_zone = '+09:00';
CREATE TABLE tz_demo (id INT PRIMARY KEY, dt DATETIME, ts TIMESTAMP NULL);
INSERT INTO tz_demo VALUES (1, '2026-10-03 12:00:00', '2026-10-03 12:00:00');

SELECT @@session.time_zone, dt, ts, UNIX_TIMESTAMP(ts) FROM tz_demo;

SET time_zone = '+00:00';
SELECT @@session.time_zone, dt, ts, UNIX_TIMESTAMP(ts) FROM tz_demo;

SET time_zone = '-05:00';
SELECT @@session.time_zone, dt, ts, UNIX_TIMESTAMP(ts) FROM tz_demo;

実行結果(3つの SELECT の結果を1つにまとめています):

+---------------------+---------------------+---------------------+--------------------+
| @@session.time_zone | dt                  | ts                  | UNIX_TIMESTAMP(ts) |
+---------------------+---------------------+---------------------+--------------------+
| +09:00              | 2026-10-03 12:00:00 | 2026-10-03 12:00:00 |         1790996400 |
| +00:00              | 2026-10-03 12:00:00 | 2026-10-03 03:00:00 |         1790996400 |
| -05:00              | 2026-10-03 12:00:00 | 2026-10-02 22:00:00 |         1790996400 |
+---------------------+---------------------+---------------------+--------------------+

読み取れることは3つです。

  • dt(DATETIME)はどのタイムゾーンでも 12:00:00 のままです。入れた文字がそのまま返ります。
  • ts(TIMESTAMP)は 12:00 → 03:00 → 前日 22:00 と表示が変わります。 日付まで変わる点に注目してください。
  • UNIX_TIMESTAMP(ts) はずっと 1790996400 です。保存されている中身(UTC の瞬間)は同じで、見せ方だけが変わっています。

なぜそうなるのか

TIMESTAMP は、保存するときに「セッションの time_zone → UTC」、読むときに「UTC → セッションの time_zone」の変換を行います。DATETIME はこの変換を行いません。図にすると次のとおりです。

flowchart LR
  A["INSERT '12:00:00'(time_zone=+09:00)"] --> B{"型は?"}
  B -->|DATETIME| C["12:00:00 のまま保存"]
  B -->|TIMESTAMP| D["UTC 03:00:00 に直して保存"]
  C --> E["どの time_zone でも 12:00:00"]
  D --> F["+09:00 なら 12:00:00 / +00:00 なら 03:00:00"]

つまり、TIMESTAMP は「世界共通のある瞬間」を、DATETIME は「カレンダーと時計に書かれた文字」を保存している、と考えると整理しやすくなります。

日付で絞り込むときにも差が出る

表示だけでなく、WHERE 句の結果も time_zone によって変わります。日本時間の 0時30分 を両方の型に入れ、UTC のセッションで読んだ結果です。

SET time_zone = '+09:00';
CREATE TABLE idx (id INT PRIMARY KEY, ts TIMESTAMP NOT NULL, dt DATETIME NOT NULL, KEY(ts), KEY(dt));
INSERT INTO idx VALUES (1, '2026-10-03 00:30:00', '2026-10-03 00:30:00');

SET time_zone = '+00:00';
SELECT id, ts, DATE(ts) d_ts, DATE(dt) d_dt FROM idx;
SELECT COUNT(*) FROM idx WHERE ts >= '2026-10-03 00:00:00';
SELECT COUNT(*) FROM idx WHERE dt >= '2026-10-03 00:00:00';
+----+---------------------+------------+------------+
| id | ts                  | d_ts       | d_dt       |
+----+---------------------+------------+------------+
|  1 | 2026-10-02 15:30:00 | 2026-10-02 | 2026-10-03 |
+----+---------------------+------------+------------+

COUNT(*) (ts で絞り込み): 0
COUNT(*) (dt で絞り込み): 1

同じ「10月3日以降」という条件でも、TIMESTAMP の列は 0 件、DATETIME の列は 1 件になりました。TIMESTAMP を使うなら、アプリ・バッチ・管理ツールの接続すべてで time_zone をそろえることが、日次集計を安定させるポイントです。

time_zone の確認と設定のコピペ集

-- いまの設定を確認
SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone;

-- この接続だけ変更
SET time_zone = '+09:00';

-- サーバー全体の既定を変更(権限が必要。新しい接続から有効)
SET GLOBAL time_zone = '+09:00';

-- 再起動後も保つ(8.0 以降)
SET PERSIST time_zone = '+09:00';

今回の環境での確認結果は次のとおりです(SET GLOBAL と SET PERSIST は未実行です)。

+--------------------+---------------------+--------------------+
| @@global.time_zone | @@session.time_zone | @@system_time_zone |
+--------------------+---------------------+--------------------+
| SYSTEM             | +09:00              | JST                |
+--------------------+---------------------+--------------------+

SYSTEM は「OS のタイムゾーンに従う」という意味です。

'Asia/Tokyo' のような名前で指定するとエラーになるとき

SET time_zone = 'Asia/Tokyo' のような名前指定は、タイムゾーンのテーブルにデータが入っている場合に使えます。今回の環境はテーブルが空だったため、次の結果になりました。

SET time_zone = 'Asia/Tokyo';
SELECT COUNT(*) FROM mysql.time_zone_name;
SELECT CONVERT_TZ('2026-10-03 12:00:00', '+09:00', '+00:00') AS utc,
       CONVERT_TZ('2026-10-03 12:00:00', 'Asia/Tokyo', 'UTC') AS named;
ERROR 1298 (HY000): Unknown or incorrect time zone: 'Asia/Tokyo'

COUNT(*): 0

+---------------------+-------+
| utc                 | named |
+---------------------+-------+
| 2026-10-03 03:00:00 | NULL  |
+---------------------+-------+

'+09:00' のようなオフセット指定はテーブルが空でも使えます。 名前指定の CONVERT_TZ はエラーにならず NULL を返すので、気づきにくい点です。名前を使いたいときは、OS のタイムゾーン情報を mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql で読み込む方法が一般に知られています(今回は未実行)。

オフセットつきの値を入れる

8.0.46 では、値の末尾に +00:00 のようなオフセットを付けて INSERT できました。セッションが +09:00 のときの結果です。

SET time_zone = '+09:00';
INSERT INTO tz_demo VALUES (20, '2026-10-03 12:00:00+00:00', '2026-10-03 12:00:00+00:00');
SELECT * FROM tz_demo WHERE id = 20;
+----+---------------------+---------------------+
| id | dt                  | ts                  |
+----+---------------------+---------------------+
| 20 | 2026-10-03 21:00:00 | 2026-10-03 21:00:00 |
+----+---------------------+---------------------+

UTC の 12:00 が、セッションのタイムゾーン(+09:00)の 21:00 として解釈されました。DATETIME でも、オフセットつきの値はセッションのタイムゾーンに直してから保存されることがわかります。保存後の DATETIME にはオフセットの情報は残りません。

2038年を超える値を入れるとどうなるか

TIMESTAMP の上限は 2038-01-19 03:14:07 UTC です。これは 1970年からの秒数を符号つき32ビット整数で表したときの最大値(2147483647)にあたります。

上限の1秒先でエラーになる

SET time_zone = '+00:00';
INSERT INTO tz_demo VALUES (2, '2038-01-19 03:14:07', '2038-01-19 03:14:07');  -- 上限ちょうど
INSERT INTO tz_demo VALUES (3, '2038-01-19 03:14:08', '2038-01-19 03:14:08');  -- 1秒先
(id=2 は成功)
ERROR 1292 (22007): Incorrect datetime value: '2038-01-19 03:14:08' for column 'ts' at row 1

エラー文に「2038」や「範囲外」といった言葉は出ず、Incorrect datetime value と表示される点を覚えておくと、原因にすぐたどり着けます。

日本時間のセッションでは、境目は 12時14分7秒 です。

SET time_zone = '+09:00';
INSERT INTO tz_demo VALUES (9,  '2038-01-19 12:14:07', '2038-01-19 12:14:07');  -- 成功
INSERT INTO tz_demo VALUES (10, '2038-01-19 12:14:08', '2038-01-19 12:14:08');
ERROR 1292 (22007): Incorrect datetime value: '2038-01-19 12:14:08' for column 'ts' at row 1

下限と DATETIME の上限

同じように、範囲の反対側も確認しました(セッションは +00:00)。

入れた値 列 結果
1970-01-01 00:00:00 TIMESTAMP ERROR 1292 Incorrect datetime value
1970-01-01 00:00:01 TIMESTAMP 成功
2038-01-19 03:14:07 TIMESTAMP 成功
2038-01-19 03:14:08 TIMESTAMP ERROR 1292 Incorrect datetime value
9999-12-31 23:59:59 DATETIME 成功
10000-01-01 00:00:00 DATETIME ERROR 1292 Incorrect datetime value

厳密モードを外すと 0000-00-00 が入る

sql_mode を空にした状態で同じことをすると、エラーにならず警告だけが出て、値が置き換わります。

SET SESSION sql_mode = '';
INSERT INTO tz_demo VALUES (11, '2040-01-01 00:00:00', '2040-01-01 00:00:00');
SHOW WARNINGS;
SELECT * FROM tz_demo WHERE id = 11;
+---------+------+---------------------------------------------+
| Level   | Code | Message                                     |
+---------+------+---------------------------------------------+
| Warning | 1264 | Out of range value for column 'ts' at row 1 |
+---------+------+---------------------------------------------+

+----+---------------------+---------------------+
| id | dt                  | ts                  |
+----+---------------------+---------------------+
| 11 | 2040-01-01 00:00:00 | 0000-00-00 00:00:00 |
+----+---------------------+---------------------+

厳密モード(既定)のままにしておくと、範囲外の値はエラーで知らせてくれます。 sql_mode を変更している環境では、TIMESTAMP の列に 0000-00-00 が入っていないかを確認すると安心です。

関数は 2038年より先も計算できる

8.0.46 では、列への保存とは別に、関数の計算は 2038年より先まで扱えました(セッションは +09:00)。

SELECT UNIX_TIMESTAMP('2038-01-19 12:14:07') a,
       UNIX_TIMESTAMP('2038-01-19 12:14:08') b,
       UNIX_TIMESTAMP('2040-01-01 00:00:00') c,
       FROM_UNIXTIME(2147483648) d;
+------------+------------+------------+---------------------+
| a          | b          | c          | d                   |
+------------+------------+------------+---------------------+
| 2147483647 | 2147483648 | 2208956400 | 2038-01-19 12:14:08 |
+------------+------------+------------+---------------------+

UNIX_TIMESTAMP() と FROM_UNIXTIME() は 2147483647 を超える値を返せています。上限があるのは「TIMESTAMP 型の列に保存できる値」である、と切り分けて理解しておくとよいでしょう。

2038年の影響を受けやすい列

列の例 影響 おすすめ
created_at・updated_at(現在時刻を入れる) 2038年1月までは収まる 長く使う表なら DATETIME も検討
有効期限・契約の終了日・予約日 いますでに範囲を超えることがある(10年以上先の日付) DATETIME
誕生日・設立日など過去の日付 1970年より前は入らない DATE または DATETIME
「無期限」を表す 9999-12-31 入らない DATETIME

「現在時刻 + 15年」のような計算をして TIMESTAMP の列に入れる処理は、2038年を待たずにエラーになります。将来の日付を入れる列は DATETIME を選ぶのが確実です。

TIMESTAMP から DATETIME に変えるときの注意

ALTER TABLE ... MODIFY で型を変えると、ALTER を実行したセッションの time_zone で変換された値が DATETIME として固定されます。

SET time_zone = '+09:00';
CREATE TABLE mig (id INT, ts TIMESTAMP NULL);
INSERT INTO mig VALUES (1, '2026-10-03 12:00:00');

SET time_zone = '+00:00';                 -- UTC のセッションで ALTER
ALTER TABLE mig MODIFY ts DATETIME NULL;

SET time_zone = '+09:00';
SELECT * FROM mig;
+------+---------------------+
| id   | ts                  |
+------+---------------------+
|    1 | 2026-10-03 03:00:00 |
+------+---------------------+

日本時間 12:00 として入れた値が、03:00(UTC の表示)のまま DATETIME になりました。型を変える前に、SELECT @@session.time_zone で「どのタイムゾーンの文字として残したいか」を確認してから ALTER するのが手順のポイントです。大きな表での ALTER は時間がかかるため、検証用のコピーで所要時間を確かめてから進めると落ち着いて作業できます。

サイズと小数秒(ミリ秒・マイクロ秒)の違い

バイト数

定義 DATETIME TIMESTAMP
(0)(小数秒なし) 5バイト 4バイト
(1)・(2) 6バイト 5バイト
(3)・(4) 7バイト 6バイト
(5)・(6) 8バイト 7バイト

MySQL 8.4 のマニュアル「Data Type Storage Requirements」で確認した値です(5.6.4 以降の形式)。差は常に1バイトで、サイズだけを理由に型を選ぶ場面は多くありません。1億行あっても、1列あたりの差は約 100MB です。

小数秒は (3) や (6) を付けたときだけ保存される

CREATE TABLE fsp_demo (
  dt0 DATETIME, dt3 DATETIME(3), dt6 DATETIME(6),
  ts0 TIMESTAMP NULL, ts6 TIMESTAMP(6) NULL
);
INSERT INTO fsp_demo VALUES (
  '2026-10-03 12:00:00.567891', '2026-10-03 12:00:00.567891', '2026-10-03 12:00:00.567891',
  '2026-10-03 12:00:00.567891', '2026-10-03 12:00:00.567891'
);
SELECT * FROM fsp_demo\G
dt0: 2026-10-03 12:00:01
dt3: 2026-10-03 12:00:00.568
dt6: 2026-10-03 12:00:00.567891
ts0: 2026-10-03 12:00:01
ts6: 2026-10-03 12:00:00.567891
  • 桁数を指定しない列(dt0・ts0)は、小数秒が四捨五入されて 12:00:01 になりました。切り捨てではない点に注意します。
  • (3) はミリ秒、(6) はマイクロ秒まで保存されます。この動きは DATETIME と TIMESTAMP で共通です。

現在時刻を小数秒つきで取る関数も、同じ桁数の指定ができます。

SELECT NOW(), NOW(3), NOW(6);
+---------------------+-------------------------+----------------------------+
| NOW()               | NOW(3)                  | NOW(6)                     |
+---------------------+-------------------------+----------------------------+
| 2026-10-03 14:24:49 | 2026-10-03 14:24:49.471 | 2026-10-03 14:24:49.471423 |
+---------------------+-------------------------+----------------------------+

作成日時・更新日時を自動で入れる定義コピペ集

DEFAULT CURRENT_TIMESTAMP(行を作ったときに現在時刻を入れる)と ON UPDATE CURRENT_TIMESTAMP(行を更新したときに現在時刻へ書き換える)は、TIMESTAMP だけでなく DATETIME でも使えます。

定義の早見表

やりたいこと 定義
作成時だけ自動 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
作成時と更新時に自動 updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
更新時だけ自動(作成時は NULL) updated_at DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
ミリ秒つき updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
TIMESTAMP で同じことをする 上の DATETIME を TIMESTAMP に置き換える

「更新時だけ自動」の行は今回未実行で、マニュアルの記載にもとづく書き方です。それ以外は下で実行しています。

DATETIME で作成日時・更新日時を持つ表

CREATE TABLE articles (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(100) NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO articles (title) VALUES ('first');
SELECT * FROM articles;
+----+-------+---------------------+---------------------+
| id | title | created_at          | updated_at          |
+----+-------+---------------------+---------------------+
|  1 | first | 2026-10-03 14:24:49 | 2026-10-03 14:24:49 |
+----+-------+---------------------+---------------------+

列を指定しなくても、両方に現在時刻が入りました。

値が変わらない UPDATE では更新日時は動かない

自動更新でいちばん誤解されやすい点です。UPDATE 文を実行しても、実際に値が変わらなければ updated_at は書き換わりません。

SELECT SLEEP(2);
UPDATE articles SET title = 'first' WHERE id = 1;    -- いまと同じ値
SELECT * FROM articles;
Rows matched: 1  Changed: 0  Warnings: 0

+----+-------+---------------------+---------------------+
| id | title | created_at          | updated_at          |
+----+-------+---------------------+---------------------+
|  1 | first | 2026-10-03 14:24:49 | 2026-10-03 14:24:49 |
+----+-------+---------------------+---------------------+

Rows matched: 1 Changed: 0 と表示され、updated_at は 14:24:49 のままです。値を変えると、ここで初めて動きます。

UPDATE articles SET title = 'second' WHERE id = 1;
SELECT * FROM articles;
Rows matched: 1  Changed: 1  Warnings: 0

+----+--------+---------------------+---------------------+
| id | title  | created_at          | updated_at          |
+----+--------+---------------------+---------------------+
|  1 | second | 2026-10-03 14:24:49 | 2026-10-03 14:24:52 |
+----+--------+---------------------+---------------------+

マニュアル(8.4 の「Automatic Initialization and Updating for TIMESTAMP and DATETIME」)にも、ほかのすべての列が現在の値のままなら自動更新の列は変わらない、と書かれています。

更新日時を「必ず動かす」「動かさない」書き方

やりたいこと 書き方 実行結果
値が同じでも更新日時を進める UPDATE articles SET title='second', updated_at=CURRENT_TIMESTAMP WHERE id=1; updated_at が 14:24:53 に進んだ
値は変えるが更新日時は据え置く UPDATE articles SET title='third', updated_at=updated_at WHERE id=1; title は third、updated_at は 14:24:53 のまま

「最終確認日時」のように、内容が同じでも時刻を進めたい列は、updated_at = CURRENT_TIMESTAMP を明示するのが確実です。反対に、データ移行や一括修正で更新日時を保ちたいときは updated_at = updated_at を添えます。

小数秒つきの列は桁数をそろえる

DATETIME(3) の列に桁数なしの CURRENT_TIMESTAMP を書くと、テーブルを作る段階でエラーになります。

CREATE TABLE bad1 (id INT, u DATETIME(3) DEFAULT CURRENT_TIMESTAMP);
CREATE TABLE bad2 (id INT, u DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP);
ERROR 1067 (42000): Invalid default value for 'u'
ERROR 1294 (HY000): Invalid ON UPDATE clause for 'u' column

列・DEFAULT・ON UPDATE の3か所の桁数をそろえると通ります。

CREATE TABLE ok3 (
  id INT,
  u DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
  c TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
);
INSERT INTO ok3 (id) VALUES (1);
SELECT * FROM ok3;
+------+-------------------------+----------------------------+
| id   | u                       | c                          |
+------+-------------------------+----------------------------+
|    1 | 2026-10-03 14:24:54.490 | 2026-10-03 14:24:54.490794 |
+------+-------------------------+----------------------------+

1つの表に自動更新の列を複数置ける

8.0.46 では、自動初期化・自動更新つきの TIMESTAMP 列を同じ表に2つ定義できました。

CREATE TABLE two (
  a TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  b TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  c INT
);
SHOW CREATE TABLE two\G
CREATE TABLE `two` (
  `a` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `b` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `c` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

NOT NULL を書かなかったので、timestamp NULL として作られている点も確認できます。

いまの定義を確認する

自動更新が付いているかどうかは、SHOW CREATE TABLE か information_schema で確認できます。

SELECT COLUMN_NAME, DATA_TYPE, DATETIME_PRECISION, IS_NULLABLE, COLUMN_DEFAULT, EXTRA
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 't245' AND TABLE_NAME = 'articles'
ORDER BY ORDINAL_POSITION;
+-------------+-----------+--------------------+-------------+-------------------+-----------------------------------------------+
| COLUMN_NAME | DATA_TYPE | DATETIME_PRECISION | IS_NULLABLE | COLUMN_DEFAULT    | EXTRA                                         |
+-------------+-----------+--------------------+-------------+-------------------+-----------------------------------------------+
| id          | int       |               NULL | NO          | NULL              | auto_increment                                |
| title       | varchar   |               NULL | NO          | NULL              |                                               |
| created_at  | datetime  |                  0 | NO          | CURRENT_TIMESTAMP | DEFAULT_GENERATED                             |
| updated_at  | datetime  |                  0 | NO          | CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP |
+-------------+-----------+--------------------+-------------+-------------------+-----------------------------------------------+

EXTRA 列に on update CURRENT_TIMESTAMP があれば、自動更新が有効です。

explicit_defaults_for_timestamp で変わる TIMESTAMP の初期動作

explicit_defaults_for_timestamp は、「何も書かなかった TIMESTAMP 列に、自動でデフォルトを付けるかどうか」を決めるシステム変数です。8.0.46 の既定値は ON(1)でした。

ON と OFF の違い

項目 ON(8.0.46 の既定) OFF(以前からの動作)
ts TIMESTAMP とだけ書いた列 NULL DEFAULT NULL NOT NULL になる
表の最初の TIMESTAMP 列 何も付かない DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP が自動で付く
2つ目以降の TIMESTAMP 列 何も付かない DEFAULT '0000-00-00 00:00:00' が付く
NOT NULL の TIMESTAMP 列に NULL を入れる エラー(Column cannot be null) 現在時刻が入る
DATETIME 列 影響なし 影響なし

OFF 側の説明は、8.4 のマニュアル「Automatic Initialization and Updating for TIMESTAMP and DATETIME」で確認し、下のとおり 8.0.46 のセッションで OFF に切り替えて動作も確かめました。

ON のとき(8.0.46 の既定)

SELECT @@explicit_defaults_for_timestamp;   -- 1
CREATE TABLE ed_on (id INT, ts1 TIMESTAMP, ts2 TIMESTAMP, ts3 TIMESTAMP NOT NULL);
SHOW CREATE TABLE ed_on\G
CREATE TABLE `ed_on` (
  `id` int DEFAULT NULL,
  `ts1` timestamp NULL DEFAULT NULL,
  `ts2` timestamp NULL DEFAULT NULL,
  `ts3` timestamp NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

書いたとおりの定義になり、自動の DEFAULT や ON UPDATE は付きません。NOT NULL の列は、値を省略したり NULL を入れたりするとエラーで知らせてくれます。

INSERT INTO ed_on (id) VALUES (1);
INSERT INTO ed_on (id, ts3) VALUES (2, NULL);
ERROR 1364 (HY000): Field 'ts3' doesn't have a default value
ERROR 1048 (23000): Column 'ts3' cannot be null

OFF にしたとき

セッション単位で OFF に切り替えると、まず非推奨の警告が出ました。

SET SESSION explicit_defaults_for_timestamp = OFF;
SHOW WARNINGS;
+---------+------+------------------------------------------------------------------------------------------+
| Level   | Code | Message                                                                                  |
+---------+------+------------------------------------------------------------------------------------------+
| Warning | 1287 | 'explicit_defaults_for_timestamp' is deprecated and will be removed in a future release. |
+---------+------+------------------------------------------------------------------------------------------+

この状態で、TIMESTAMP 列だけを並べた表を作ります。

CREATE TABLE ed_off2 (
  id INT,
  ts1 TIMESTAMP,
  ts2 TIMESTAMP NULL,
  ts3 TIMESTAMP DEFAULT '2000-01-01 00:00:00'
);
SHOW CREATE TABLE ed_off2\G
INSERT INTO ed_off2 (id, ts1) VALUES (1, NULL);
SELECT * FROM ed_off2;
CREATE TABLE `ed_off2` (
  `id` int DEFAULT NULL,
  `ts1` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `ts2` timestamp NULL DEFAULT NULL,
  `ts3` timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

+------+---------------------+------+---------------------+
| id   | ts1                 | ts2  | ts3                 |
+------+---------------------+------+---------------------+
|    1 | 2026-10-03 14:25:16 | NULL | 2000-01-01 00:00:00 |
+------+---------------------+------+---------------------+
  • ts1 TIMESTAMP としか書いていないのに、NOT NULL と DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP が自動で付きました。
  • ts1 に NULL を INSERT すると、現在時刻が入りました。
  • NULL を明示した ts2 と、定数の DEFAULT を書いた ts3 には、自動更新は付きません。

OFF のまま、何も付けない TIMESTAMP 列を2つ並べると、次のエラーになりました。

CREATE TABLE ed_off (id INT, ts1 TIMESTAMP, ts2 TIMESTAMP);
ERROR 1067 (42000): Invalid default value for 'ts2'

2つ目の列に自動で付く DEFAULT '0000-00-00 00:00:00' が、既定の sql_mode(NO_ZERO_DATE)と合わないためです。

5.7 と 8.x の違い

バージョン explicit_defaults_for_timestamp の既定 ts TIMESTAMP とだけ書いた最初の列 今回の確認
5.7 OFF NOT NULL + 自動初期化 + 自動更新 未実行(一般に知られた仕様)
8.0 ON NULL DEFAULT NULL 8.0.46 で実行
8.4・9.x ON NULL DEFAULT NULL 未実行(8.4 のマニュアルで ON/OFF 時の動作を確認)

5.7 の実機は今回用意できなかったため、5.7 の行は 8.0.46 で OFF に切り替えた結果からの類推を含みます。

この違いは、5.7 で作った表の定義を 8.x に持っていくときに効いてきます。

  • mysqldump や SHOW CREATE TABLE で取った定義には DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP が明示されるため、8.x に流しても同じ動きになります。
  • 手書きの CREATE TABLE(マイグレーションのファイルなど)で ts TIMESTAMP とだけ書いてある場合は、8.x では自動更新が付かず、NULL も入るようになります。

どちらのバージョンでも同じ結果にするには、NULL か NOT NULL か、DEFAULT、ON UPDATE をすべて明示して書くのがいちばん確実です。明示して書いた定義は、この変数の値に左右されません。

DATETIMEとTIMESTAMPの選び方

判断フロー

flowchart TD
  A["日時の列を作る"] --> B{"2038年より先や1970年より前の値が入る?"}
  B -->|入る| D["DATETIME"]
  B -->|入らない| C{"複数のタイムゾーンの接続から読む?"}
  C -->|読む| E{"表示を各接続の時刻に自動で合わせたい?"}
  C -->|読まない| F{"長く使う表?"}
  E -->|合わせたい| G["TIMESTAMP"]
  E -->|アプリ側で変換する| H["DATETIME に UTC で保存"]
  F -->|10年以上使う| D
  F -->|短期のログなど| I["どちらでも可。チームの決まりに合わせる"]

用途別のおすすめ

用途 おすすめ 理由
予定・締め切り・公開予定日時 DATETIME 「その土地の時計で何時」をそのまま残したい
有効期限・終了予定日 DATETIME 2038年より先の値が入りうる
誕生日・記念日 DATE 時刻が不要。1970年より前も入る
created_at・updated_at(1つの国の中で使う) DATETIME time_zone 設定の影響を受けず、範囲も広い
created_at・updated_at(複数の地域から読む) TIMESTAMP、または DATETIME に UTC で保存 瞬間をそろえて比較できる
短期間で消すログ・セッション情報 TIMESTAMP でも DATETIME でも可 範囲の心配が少ない

迷ったときの考え方

  • 「その瞬間」を残したいのか、「時計の表示」を残したいのかを先に決めると選びやすくなります。予約の「10月3日 12時開始」は、サーバーの設定が変わっても 12時のままであってほしい値なので DATETIME が合います。
  • DATETIME に UTC で保存するという決まりにする方法もあります。保存時に UTC_TIMESTAMP() を使い、表示するときにアプリ側で各地の時刻に変換します。範囲が広く、接続の time_zone に左右されないのが利点です。そのかわり、「この列は UTC」という決まりを列名やコメントで共有しておくことが大切です。
  • 既存の表が TIMESTAMP で統一されているなら、そのまま合わせるのも立派な判断です。同じ表の中で型が混ざると、比較や集計のときに変換の有無を意識する必要が出てきます。

つまずきやすい点と確認方法

起きること 原因 確認・対処
時刻が9時間ずれて見える TIMESTAMP の列を、別の time_zone の接続で読んでいる SELECT @@session.time_zone; で接続ごとの設定を確認
ツールによって件数が違う TIMESTAMP の WHERE 条件が time_zone で変わる すべての接続で time_zone をそろえる
ERROR 1292 Incorrect datetime value TIMESTAMP の範囲外(2038年より先、1970年ちょうど以前) 列を DATETIME にする
ERROR 1067 Invalid default value DATETIME(3) に桁数なしの CURRENT_TIMESTAMP、または 0000-00-00 のデフォルト 桁数をそろえる・DEFAULT を明示する
ERROR 1294 Invalid ON UPDATE clause ON UPDATE の桁数が列と違う ON UPDATE CURRENT_TIMESTAMP(3) のようにそろえる
ERROR 1298 Unknown or incorrect time zone タイムゾーンのテーブルが空 '+09:00' 形式で指定する
UPDATE したのに updated_at が変わらない 値が実際には変わっていない(Changed: 0) updated_at = CURRENT_TIMESTAMP を明示
8.x に移したら updated_at が自動で入らない ts TIMESTAMP とだけ書いた定義が、ON では自動更新にならない DEFAULT と ON UPDATE を明示して定義し直す
秒が1つ進んで保存される 小数秒が四捨五入された 列を DATETIME(3) や (6) にする

よくある質問

Q. DATETIME と TIMESTAMP、結局どちらを使えばよいですか? A. 迷ったら DATETIME が扱いやすい選択です。範囲が 9999年まであり、接続の time_zone に左右されません。複数のタイムゾーンから同じデータを読み、表示を自動で切り替えたい場合は TIMESTAMP が向いています。

Q. 2038年問題は、MySQL のバージョンを上げれば解消しますか? A. 今回確かめた 8.0.46 では、TIMESTAMP 型の列の上限は 2038-01-19 03:14:07 UTC のままで、1秒先の値は ERROR 1292 になりました。8.4 のマニュアルでも同じ範囲が書かれています。一方で UNIX_TIMESTAMP() などの関数は 8.0.46 で 2038年より先の値を返せました。列に将来の値を入れるなら DATETIME を選ぶのが確実です。

Q. DATETIME でも ON UPDATE CURRENT_TIMESTAMP は使えますか? A. 使えます。この記事の articles 表は DATETIME で定義し、INSERT 時の自動設定と UPDATE 時の自動更新の両方を 8.0.46 で確認しました。小数秒つきの列では CURRENT_TIMESTAMP(3) のように桁数をそろえます。

Q. NOW() と CURRENT_TIMESTAMP は違うものですか? A. 同じ値を返す別名です。8.4 のマニュアルにも、CURRENT_TIMESTAMP の同義語として NOW() などが挙げられています。DEFAULT 句や ON UPDATE 句には CURRENT_TIMESTAMP と書くのが一般的です。

まとめ

  • DATETIME は入れた値をそのまま保存、TIMESTAMP は UTC に直して保存し、読むときにセッションの time_zone へ戻す。
  • time_zone を切り替えると TIMESTAMP だけ表示が変わる。 WHERE 句の結果も変わるので、接続ごとの設定をそろえる。
  • TIMESTAMP の上限は 2038-01-19 03:14:07 UTC。 1秒先は ERROR 1292 Incorrect datetime value になる。将来の日付を入れる列は DATETIME にする。
  • DEFAULT CURRENT_TIMESTAMP と ON UPDATE CURRENT_TIMESTAMP は DATETIME でも使える。 小数秒つきは桁数をそろえる。
  • 値が変わらない UPDATE では更新日時は動かない。 必ず進めたいときは updated_at = CURRENT_TIMESTAMP を明示する。
  • explicit_defaults_for_timestamp が ON(8.0 の既定)なら、TIMESTAMP 列に自動のデフォルトは付かない。 NULL・DEFAULT・ON UPDATE を明示して書けば、どのバージョンでも同じ定義になる。
  • 迷ったら「瞬間を残すか、時計の表示を残すか」で選ぶ。 多くの場面では DATETIME が扱いやすい。

参考・出典

  • MySQL 8.4 Reference Manual「The DATE, DATETIME, and TIMESTAMP Types」(範囲、UTC 変換)
  • MySQL 8.4 Reference Manual「Automatic Initialization and Updating for TIMESTAMP and DATETIME」(自動初期化・自動更新、explicit_defaults_for_timestamp)
  • MySQL 8.4 Reference Manual「Data Type Storage Requirements」(バイト数)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「MySQL DATETIMEとTIMESTAMPの違い|2038年・タイムゾーン」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?