テーブルに作成日時(created_at)と更新日時(updated_at)の列を置いておくと、その行がいつ登録され、最後にいつ変わったのかを後から追えます。MySQL では、列の定義に DEFAULT CURRENT_TIMESTAMP と ON UPDATE CURRENT_TIMESTAMP を書くだけで、アプリから時刻を渡さなくても値が入ります。この記事では2つの書き方に加えて、値が変わらない UPDATE では更新日時も動かないこと、ミリ秒まで残す書き方、あとから列を足すときの落とし穴を、実行結果を見ながら説明します。
以下の結果は MySQL 8.0 で実際に実行したものです。
このシナリオで考える
会員を管理する users テーブルを例にします。会員にはプラン(free か pro)があり、プランを変えたときに更新日時が残るようにしたい、という場面です。
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
plan VARCHAR(10) NOT NULL DEFAULT 'free',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
created_at には DEFAULT だけを、updated_at には DEFAULT と ON UPDATE の両方を書いています。結果を読みやすくするため、以降の例では実行の前に SET timestamp でその接続の現在時刻を固定しました。テストで時刻をそろえたいときにも使える方法で、ふだんのアプリでは書く必要はありません。
DEFAULT CURRENT_TIMESTAMPで作成日時を入れる
DEFAULT CURRENT_TIMESTAMP を付けた列は、INSERT で値を指定しなかったときに現在の日時が入ります。INSERT 文には時刻の列を書きません。
SET timestamp = UNIX_TIMESTAMP('2026-11-03 09:00:00');
INSERT INTO users (name) VALUES ('佐藤'), ('鈴木');
SELECT * FROM users;
+----+--------+------+---------------------+---------------------+
| id | name | plan | created_at | updated_at |
+----+--------+------+---------------------+---------------------+
| 1 | 佐藤 | free | 2026-11-03 09:00:00 | 2026-11-03 09:00:00 |
| 2 | 鈴木 | free | 2026-11-03 09:00:00 | 2026-11-03 09:00:00 |
+----+--------+------+---------------------+---------------------+
追加した時点では、created_at と updated_at は同じ時刻になります。列の書き方ごとの動きは次のとおりです。
| 列の書き方 | INSERT したとき | UPDATE したとき |
|---|---|---|
| DEFAULT CURRENT_TIMESTAMP | 現在の日時 | 変わらない |
| DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 現在の日時 | 現在の日時に変わる |
| どちらも書かない | 指定した値(省くと NULL かエラー) | 変わらない |
CURRENT_TIMESTAMP の代わりに NOW() と書いても同じ意味になります。
ON UPDATE CURRENT_TIMESTAMPで更新日時を入れる
ON UPDATE CURRENT_TIMESTAMP を付けた列は、同じ行のほかの列の値が変わったときに、現在の日時に書きかわります。1時間半後に佐藤さんのプランを pro に変えてみます。
SET timestamp = UNIX_TIMESTAMP('2026-11-03 10:30:00');
UPDATE users SET plan = 'pro' WHERE id = 1;
SELECT * FROM users;
+----+--------+------+---------------------+---------------------+
| id | name | plan | created_at | updated_at |
+----+--------+------+---------------------+---------------------+
| 1 | 佐藤 | pro | 2026-11-03 09:00:00 | 2026-11-03 10:30:00 |
| 2 | 鈴木 | free | 2026-11-03 09:00:00 | 2026-11-03 09:00:00 |
+----+--------+------+---------------------+---------------------+
id = 1 の updated_at だけが 10:30 に変わり、created_at は 9:00 のままです。UPDATE 文に updated_at を書かなくても、MySQL が入れてくれます。
値が同じUPDATEではupdated_atも変わらない
ON UPDATE が動くのは、ほかの列の値が実際に変わったときだけです。もともと free の鈴木さんに、もう一度 free を入れてみます。
SET timestamp = UNIX_TIMESTAMP('2026-11-03 11:00:00');
UPDATE users SET plan = 'free' WHERE id = 2;
SELECT ROW_COUNT();
SELECT * FROM users WHERE id = 2;
+-------------+
| ROW_COUNT() |
+-------------+
| 0 |
+-------------+
+----+--------+------+---------------------+---------------------+
| id | name | plan | created_at | updated_at |
+----+--------+------+---------------------+---------------------+
| 2 | 鈴木 | free | 2026-11-03 09:00:00 | 2026-11-03 09:00:00 |
+----+--------+------+---------------------+---------------------+
ROW_COUNT()(直前の文で変わった行数)は 0 で、updated_at も 9:00 のままでした。画面で保存ボタンを押した時刻を残したい場合、この動きでは足りません。値が同じでも更新日時を動かしたいときは、updated_at を自分で指定します。
UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = 2;
SELECT * FROM users WHERE id = 2;
+----+--------+------+---------------------+---------------------+
| id | name | plan | created_at | updated_at |
+----+--------+------+---------------------+---------------------+
| 2 | 鈴木 | free | 2026-11-03 09:00:00 | 2026-11-03 11:00:00 |
+----+--------+------+---------------------+---------------------+
反対に、表記の手直しのように会員の操作ではない更新では、updated_at を動かしたくないこともあります。そのときは updated_at = updated_at と今の値を入れ直すと、自動の更新が止まります。
SET timestamp = UNIX_TIMESTAMP('2026-11-03 12:00:00');
UPDATE users SET name = '佐藤(本店)', updated_at = updated_at WHERE id = 1;
SELECT * FROM users WHERE id = 1;
+----+--------------------+------+---------------------+---------------------+
| id | name | plan | created_at | updated_at |
+----+--------------------+------+---------------------+---------------------+
| 1 | 佐藤(本店) | pro | 2026-11-03 09:00:00 | 2026-11-03 10:30:00 |
+----+--------------------+------+---------------------+---------------------+
名前は変わりましたが、updated_at は 10:30 のままです。3つの書き方を並べると次のようになります。
| したいこと | 書き方 |
|---|---|
| 値が変わったときだけ更新日時を変える | 何も書かない(ON UPDATE に任せる) |
| 値が同じでも更新日時を変える | SET updated_at = CURRENT_TIMESTAMP |
| 値を変えても更新日時は残す | SET updated_at = updated_at |
ミリ秒まで残す
DATETIME は秒までしか残りません。ミリ秒まで必要なら DATETIME(3) にし、DEFAULT と ON UPDATE の CURRENT_TIMESTAMP にも同じ (3) を付けます。
CREATE TABLE events (
id INT PRIMARY KEY AUTO_INCREMENT,
note VARCHAR(20),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
);
SET timestamp = UNIX_TIMESTAMP('2026-11-03 09:00:00.125');
INSERT INTO events (note) VALUES ('login');
SELECT * FROM events;
+----+-------+-------------------------+-------------------------+
| id | note | created_at | updated_at |
+----+-------+-------------------------+-------------------------+
| 1 | login | 2026-11-03 09:00:00.125 | 2026-11-03 09:00:00.125 |
+----+-------+-------------------------+-------------------------+
型だけ DATETIME(3) にして、CURRENT_TIMESTAMP の (3) を忘れるとエラーになります。
CREATE TABLE bad_events (
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ERROR 1067 (42000): Invalid default value for 'created_at'
今あるテーブルの設定は、information_schema.COLUMNS の EXTRA 列で確かめられます。
SELECT COLUMN_NAME, COLUMN_DEFAULT, EXTRA
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users'
AND COLUMN_NAME IN ('created_at', 'updated_at');
+-------------+-------------------+-----------------------------------------------+
| COLUMN_NAME | COLUMN_DEFAULT | EXTRA |
+-------------+-------------------+-----------------------------------------------+
| created_at | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
| updated_at | CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP |
+-------------+-------------------+-----------------------------------------------+
EXTRA に on update CURRENT_TIMESTAMP があれば、自動で更新される列です。
よくある落とし穴
NULLを渡すとエラーになる
MySQL 8.0 では、NOT NULL の列に NULL を入れても現在の日時にはならず、エラーになります。ORM(データベースを操作するライブラリ)が、空の created_at を NULL のまま送ってしまうと起きます。
INSERT INTO users (name, created_at) VALUES ('高橋', NULL);
ERROR 1048 (23000): Column 'created_at' cannot be null
INSERT では時刻の列を書かないか、値に DEFAULT と書きます。古い MySQL で TIMESTAMP 型に NULL を入れると現在の日時になったのは、explicit_defaults_for_timestamp という設定がオフだったころの動きです。8.0 ではこの設定が最初からオンになっています。
あとから列を足すと、今までの行はALTERした時刻になる
運用中のテーブルに created_at を足すと、すでにある行にはALTER TABLE を実行した時刻が入ります。10月1日に登録した注文に、11月3日に列を足した例です。
CREATE TABLE orders (
id INT PRIMARY KEY,
amount INT NOT NULL
);
SET timestamp = UNIX_TIMESTAMP('2026-10-01 09:00:00');
INSERT INTO orders VALUES (1, 1200), (2, 3400);
SET timestamp = UNIX_TIMESTAMP('2026-11-03 15:00:00');
ALTER TABLE orders
ADD COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
SELECT * FROM orders;
+----+--------+---------------------+---------------------+
| id | amount | created_at | updated_at |
+----+--------+---------------------+---------------------+
| 1 | 1200 | 2026-11-03 15:00:00 | 2026-11-03 15:00:00 |
| 2 | 3400 | 2026-11-03 15:00:00 | 2026-11-03 15:00:00 |
+----+--------+---------------------+---------------------+
本当の登録日はわからなくなります。ログなどから登録日がわかるなら、列を足したあとに UPDATE で入れ直してください。わからない場合は、列を足した日より前の created_at は当てにならないことをチームで共有しておくと、登録日での集計を間違えずにすみます。
秒が同じだと、2回の更新を見分けられない
DATETIME は秒までなので、同じ秒に2回更新されると updated_at は同じ値になります。updated_at を比べて「ほかの人が先に更新していないか」を判定すると、この取りこぼしが起きます。その判定には version 列を使う方法が確実で、「楽観ロックと悲観ロックの違い:FOR UPDATEとversion列」で説明しています。
DATETIMEとTIMESTAMPでタイムゾーンの扱いが違う
どちらの型でも同じ書き方で自動化できます。ただし DATETIME は接続のタイムゾーンで見た時刻をそのまま保存し、TIMESTAMP は UTC に直して保存します。サーバーやアプリのタイムゾーン設定がばらばらだと、表示される時刻がずれます。型の選び方は「DATETIMEとTIMESTAMPの違い:タイムゾーンと選び方」にまとめています。
まとめ
created_at には DEFAULT CURRENT_TIMESTAMP、updated_at にはそれに ON UPDATE CURRENT_TIMESTAMP を足せば、アプリで時刻を渡さなくても作成日時と更新日時が入ります。updated_at は値が実際に変わったときだけ動くので、保存した時刻を残したいなら SET updated_at = CURRENT_TIMESTAMP を書きます。ミリ秒が必要なら型と CURRENT_TIMESTAMP の両方に (3) を付けること、あとから列を足すと今までの行は ALTER した時刻になることも覚えておいてください。
もっと学ぶなら【PR】
- MySQL徹底入門 第5版:MySQL の機能を広く解説した本。日付関数などの細かい動きを調べたいときに
※Amazonのアソシエイトとして、当サイトは適格販売により収入を得ています。
参考リンク
- MySQL 8.0 Reference Manual: Automatic Initialization and Updating for TIMESTAMP and DATETIME
- MySQL 8.0 Reference Manual: explicit_defaults_for_timestamp
アイキャッチ画像: Photo by Chirayu Trivedi on Unsplash

