MySQLでcreated_at・updated_atを自動で入れる

黒い丸型のアナログ壁掛け時計 SQL

テーブルに作成日時(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のアソシエイトとして、当サイトは適格販売により収入を得ています。

参考リンク

アイキャッチ画像: Photo by Chirayu Trivedi on Unsplash

タイトルとURLをコピーしました