データモデル設計ガイド

created_at / updated_at パターンと時刻型の選び方

TIMESTAMPTZ を推す理由と DEFAULT NOW() / トリガ / アプリ側設定の使い分け。

created_at / updated_at パターンと時刻型の選び方 diagram

ほぼすべてのテーブルに created_at / updated_at を

created_at (作成日時) と updated_at (最終更新日時) は、ほぼすべての業務テーブルに入れておくのが定石です。データの新旧を判断したいとき、キャッシュ無効化、監査、デバッグなど、あらゆる場面で必要になります。後から追加するのは簡単だが、値を埋めることはできないので、最初から入れておくのが楽です。

加えて deleted_at (論理削除時刻) を含めた「3 点セット」で管理するチームも多いです。

sql
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_atDEFAULT NOW() で十分ですが、updated_at を自動更新するには RDBMS ごとに方法が違います。

  • MySQL: updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP と書けば自動更新される
  • PostgreSQL / SQL Server / SQLite: 自動更新の組み込み構文はなく、トリガで BEFORE UPDATENEW.updated_at = NOW() をセットする
  • アプリ側で更新: ORM の共通フック (ActiveRecord の touch、TypeORM の @UpdateDateColumn) で設定する

アプリ側で設定する方式は移植性が高いが、生 SQL で UPDATE した行は更新されないので、DB トリガと併用するのが安全です。

sql
-- 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 統一 + クライアント表示時に変換を徹底します。