MySQL道普請便り

第259回知ってるとちょっと便利なON UPDATE CURRENT_TIMESTAMP

皆さんはDEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPをご存じでしょうか? この指定を行ったカラムはINSERTやUPDATEを行った際に自動更新をしてくれるようになります。特に忘れがちなcreated_atやupdated_atの挿入や更新を自動管理できる、非常に便利な機能です。こういったカラムを業務ロジックとして使用することはアンチパターンとしてされていて、そのような場合は別途日付に合わせた運用をすることが多いです。

そんな事情もあり、created_atやupdated_atのような値は、見た目の動作上影響が出にくいこと多いです。そのため、うっかりテスト項目から漏れてしまった、なんてこともあります。何もなければよいのですが、DBの値に問題があって何かしらかの作業を行うために過去に行った作業を確認する場合に、これらの値が残っているのと残っていないのでは、値がいつ作成されていつ変更されたかという事実によってできることが変わってくる場合もあるくらい、証跡としては活用できます。

この機能自体は昔からMySQLにある機能なので、知っている人も多いと思いますが、入れておくと非常に安心できる守護神のような存在なので、知っておいて損はありません。

検証環境

今回はMySQL 8.4をベースに確認できる環境をdocker composeで用意します。compose.ymlとして下記の内容を利用します。Dockerのインストールは割愛させていただきます。

version: '3.8'

services:
  mysql84:
    image: mysql:8.4
    container_name: mysql-84
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: testdb
    ports:
      - "3307:3306"  # 8.4 は 3308 に割り当て
    command: >
      mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./mysql84-init:/docker-entrypoint-initdb.d

ファイル構成は以下の通りで、各initディレクトリの下には初期化のSQLを置いてあります。

.
├─ docker-compose.yml
└─ mysql84-init/
    └─ 01-table.sql

01-table.sqlとして以下のものを設定しています。itemの何かをカウントするテーブルで、countが徐々に増えていくようなテーブルを想定しています。

CREATE TABLE item_count (
    id INT AUTO_INCREMENT PRIMARY KEY,
    count INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

上記の用意を行ったうえで、以下のコマンドで起動します。

docker compose up -d

今回はローカルのmysqlクライアントを使用しないで、docker内のmysqlクライアントを使用してMySQLに接続するようにしています。使用するコマンドは以下のようになります。

docker exec -it mysql-84 mysql -uroot -proot testdb # 8.4へ

以下のクエリを実行して同様の結果が得られていれば大丈夫です。

mysql> select * FROM item_count;
Empty set (0.00 sec)

挙動を確認してみる

さっそくですが、item_countテーブルにはidとcreated_atとupdated_atが指定されているので、とりあえずINSERTをしてみましょう。

INSERT INTO item_count (count) VALUES (1);

SELECT * FROM item_count;

実行すると以下のような結果が得られます。created_atとupdated_atは指定していませんが、自動的にdefault値である現在時刻が入っていることがわかります。

mysql> SELECT * FROM item_count;
+----+-------+---------------------+---------------------+
| id | count | created_at          | updated_at          |
+----+-------+---------------------+---------------------+
|  1 |     1 | 2025-11-21 04:06:23 | 2025-11-21 04:06:23 |
+----+-------+---------------------+---------------------+
1 row in set (0.00 sec)

続けてupdateをしてみましょう。countのカラムを1から2に更新してみます。

mysql> UPDATE item_count SET count=count+1 WHERE id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from item_count;
+----+-------+---------------------+---------------------+
| id | count | created_at          | updated_at          |
+----+-------+---------------------+---------------------+
|  1 |     2 | 2025-11-21 04:06:23 | 2025-11-21 04:07:40 |
+----+-------+---------------------+---------------------+
1 row in set (0.00 sec)

updated_atの値が変更されているのが見えると思います。created_atは変更されていません。このように、注意してNOW()などを入れていなくても、自動的に時刻を保存してくれることがわかります。指定をあえてしないことで、本来updated_atを書き換えるはずだったのにcreated_atを上書きしてしまった、といううっかりミスも防げます。

ここからですが、実はこのupdaated_atで指定したDEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPは、1個だけではなく複数のカラムで指定することができます。

それでは実際にlatest_viewed_atというカラムを、同じオプションで追加して確認してみましょう。

mysql> ALTER TABLE item_count ADD COLUMN latest_viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> select * from item_count;
+----+-------+---------------------+---------------------+---------------------+
| id | count | created_at          | updated_at          | latest_viewed_at    |
+----+-------+---------------------+---------------------+---------------------+
|  1 |     2 | 2025-11-21 04:06:23 | 2025-11-21 04:07:40 | 2025-11-21 04:10:50 |
+----+-------+---------------------+---------------------+---------------------+
1 row in set (0.00 sec)

DEFAULT値を指定していたので、ALTER TABLE文が実行された時刻が入っています。後追いでALTER TABLEW文を実行した場合に同じ値が並ぶことになるので、そこは考慮して実装しましょう。

さらに、ここからUPDATE文を実行してみましょう。

mysql> update item_count SET count = count + 1 WHERE id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from item_count;
+----+-------+---------------------+---------------------+---------------------+
| id | count | created_at          | updated_at          | latest_viewed_at    |
+----+-------+---------------------+---------------------+---------------------+
|  1 |     3 | 2025-11-21 04:06:23 | 2025-11-21 04:11:34 | 2025-11-21 04:11:34 |
+----+-------+---------------------+---------------------+---------------------+
1 row in set (0.00 sec)

このように、2カラムとも同じ値が入っていることがわかります。2カラム以上設定できるようになっているので、updated_atと同じ値が必要になったとしても、本当に更新履歴が欲しい場合以外は、必要に応じてカラムを用意して同じ設定を入れるようにしましょう。

TIMESTAMP型はマイクロ秒に対応しているので、もっと細かい値を取りたい場合は、以下のようにTIMESTAMP型に指定を入れて使用しましょう。

mysql> ALTER TABLE item_count ADD COLUMN latest_count_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6);
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> SELECT * FROM item_count;
+----+-------+---------------------+---------------------+---------------------+----------------------------+
| id | count | created_at          | updated_at          | latest_viewed_at    | latest_count_at            |
+----+-------+---------------------+---------------------+---------------------+----------------------------+
|  1 |     3 | 2025-11-21 04:06:23 | 2025-11-21 04:11:34 | 2025-11-21 04:11:34 | 2025-11-21 04:16:23.172051 |
+----+-------+---------------------+---------------------+---------------------+----------------------------+
1 row in set (0.00 sec)

ちなみに以下のようにTIMESTAMP型でマイクロ秒を指定した場合に、DEFAULT値やON UPDATEの記載の際のマイクロ秒の指定の数字がずれていると、ALTER TABLE文に失敗するので注意しましょう。

mysql> ALTER TABLE item_count ADD COLUMN latest_count_at1 TIMESTAMP(4) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6);
ERROR 1067 (42000): Invalid default value for 'latest_count_at1'

上記のエラーはDEFAULT値の値が合っていないことを示していて、下記のエラーはON UPDATE句が一致していないことを示しています。

mysql> ALTER TABLE item_count ADD COLUMN latest_count_at1 TIMESTAMP(4) DEFAULT CURRENT_TIMESTAMP(4) ON UPDATE CURRENT_TIMESTAMP(6);
ERROR 1294 (HY000): Invalid ON UPDATE clause for 'latest_count_at1' column

またこのオプションを指定していても、SQLで直接時刻を指定をすることで上書きすることもできます。

mysql> UPDATE item_count
    -> SET updated_at = '2025-01-01 00:00:00'
    -> WHERE id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * FROM item_count;
+----+-------+---------------------+---------------------+---------------------+----------------------------+
| id | count | created_at          | updated_at          | latest_viewed_at    | latest_count_at            |
+----+-------+---------------------+---------------------+---------------------+----------------------------+
|  1 |     3 | 2025-11-21 04:06:23 | 2025-01-01 00:00:00 | 2025-11-21 04:32:34 | 2025-11-21 04:32:34.996611 |
+----+-------+---------------------+---------------------+---------------------+----------------------------+
1 row in set (0.00 sec)

DBをSQLで操作するうえでは基本的に非常に便利なのですが、ORMが予期せずUPDATE文を発火する場合や、すでに持っている値の更新などが漏れる場合があり、ORMとの相性を見て導入を検討した方が良いでしょう。

まとめ

今回はDEFAULT CURRENT_TIMESTAMPの紹介を行いました。

最初にも書きましたが、created_atとupdated_atをしっかりと入れておくことは信頼性の向上にもつながるので、ぜひ使っていきましょう。

ORMを使用して開発しているときは、アプリケーションが更新処理に対してcreated_atとupdated_atに対して責任を持ってくれることがほとんどだと思いますが、直接SQLを発行する運用がある場合は、使用しているORMとの相性を見て、つけるのを検討しても良いかもしれません。

おすすめ記事

記事・ニュース一覧