created_at / updated_at パターンと時刻型の選び方
TIMESTAMPTZ を推す理由と DEFAULT NOW() / トリガ / アプリ側設定の使い分け。
ほぼすべてのテーブルに created_at / updated_at を
created_at (作成日時) と updated_at (最終更新日時) は、ほぼすべての業務テーブルに入れておくのが定石です。データの新旧を判断したいとき、キャッシュ無効化、監査、デバッグなど、あらゆる場面で必要になります。後から追加するのは簡単だが、値を埋めることはできないので、最初から入れておくのが楽です。
加えて deleted_at (論理削除時刻) を含めた「3 点セット」で管理するチームも多いです。
CREATE TABLE articles (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);TIMESTAMPTZ / TIMESTAMP WITH TIME ZONE を使う
時刻型はタイムゾーン付き (TIMESTAMPTZ) を基本に選びます。内部的には UTC で保存され、クライアントのタイムゾーンに応じて変換されるため、国や夏時間をまたいでも安全です。
RDBMS ごとの対応:
- PostgreSQL:
TIMESTAMPTZ(内部 UTC、表示はセッション TZ)。第一選択 - MySQL:
TIMESTAMP型は UTC 保存・セッション TZ 変換(TIMESTAMPTZ 相当)。ただし 2038 年問題あり。DATETIMEは TZ を保持せず「壁時計」のみ保存 - SQL Server:
DATETIMEOFFSETが TZ 付き。DATETIME2は TZ なし - SQLite: 専用型はなく、ISO 8601 文字列または UNIX 秒で保存
壁時計で保存すべき場面もあります。たとえば「毎週月曜の 9:00 にミーティング」のような「ローカル時刻の約束」は、TZ なしで保存すべきです(TZ 変換すると意味が変わる)。
DEFAULT NOW() と updated_at の自動更新
created_at は DEFAULT NOW() で十分ですが、updated_at を自動更新するには RDBMS ごとに方法が違います。
- MySQL:
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPと書けば自動更新される - PostgreSQL / SQL Server / SQLite: 自動更新の組み込み構文はなく、トリガで BEFORE UPDATEに
NEW.updated_at = NOW()をセットする - アプリ側で更新: ORM の共通フック (ActiveRecord の
touch、TypeORM の@UpdateDateColumn) で設定する
アプリ側で設定する方式は移植性が高いが、生 SQL で UPDATE した行は更新されないので、DB トリガと併用するのが安全です。
-- PostgreSQL: トリガで updated_at 自動更新
CREATE OR REPLACE FUNCTION touch_updated_at() RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER articles_touch
BEFORE UPDATE ON articles
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();DB とアプリ、どちらが時刻を決めるべきか
時刻を DB サーバ (NOW()) と アプリサーバ のどちらで決めるかは設計判断です。
- DB 側で決める: 単一時刻源なので時計ずれが発生しない。アプリサーバ複数台でもタイムスタンプが一貫。半面、テスト時に時刻を固定しづらい
- アプリ側で決める: テストで時刻をモックできる。監査ログやイベントソーシングで「実際に何が起きたか」の時刻を保ちやすい。半面、アプリサーバの時計がずれると値もずれる(NTP 必須)
原則: created_at / updated_at は DB 側、ドメイン上の「発生時刻」はアプリ側で決めるのが無難です。注文の「発注日時」はアプリ側、行の「INSERT された時刻」は DB 側、という分離。
アプリと DB のタイムゾーン設定を必ず一致させる
時刻周りの事故で最も多いのが アプリのタイムゾーン設定と DB サーバのタイムゾーン設定がずれているケースです。DB サーバが JST、アプリが UTC だと、表示がズレるだけでなく、集計の「日付境界」が 9 時間ずれてレポートが壊れます。
- DB サーバの OS /
timezone設定を UTC に統一する(推奨) - アプリ側も UTC で扱い、表示時のみユーザー TZ に変換
- JSON/API のやり取りは ISO 8601 + オフセット付き (
2026-04-14T12:00:00+09:00) で
「まあ動いているし」で放置せず、UTC 統一 + クライアント表示時に変換を徹底します。
