データベースのcreated_at・updated_at・deleted_atとは?新人エンジニア向けに活用方法を解説
こんにちは。ゆうせいです。
新人研修中に受講者から以下の質問をいただきました。
created_at・updated_at・deleted_atなどのフィールドはなんのためにあるのですか?
今回は、データベースでよく見かけるcreated_at、updated_at、deleted_atについて解説します。
システム開発をしていると、テーブルに次のようなカラムが入っていることがあります。
created_at updated_at deleted_at
最初に見たときは、こう思うかもしれません。
「全部、日時っぽいけど何が違うの?」
「created_atだけあればよくない?」
「deleted_atって削除された日時?でも削除したらデータは消えるのでは?」
とても良い疑問です。
created_at、updated_at、deleted_atは、データがいつ作られ、いつ更新され、いつ削除扱いになったのかを記録するための重要なカラムです。
たとえるなら、学校の図書館にある本の管理カードのようなものです。
「いつ登録された本か」
「いつ情報が修正されたか」
「いつ廃棄扱いになったか」
このような履歴がわかると、あとから確認しやすいですよね。
データベースでも同じです。
created_atとは何か
created_atとは、そのデータが作成された日時を表すカラムです。
createdは「作成された」という意味です。
atは「時点」を表します。
つまり、created_atは「作成された時点」という意味になります。
たとえば、ユーザー登録機能があるとします。
山田さんが2026年6月1日 10時30分に会員登録した場合、usersテーブルには次のようなデータが入ります。
| user_id | name | created_at |
|---|---|---|
| 1 | 山田 | 2026-06-01 10:30:00 |
created_atを見ると、山田さんがいつ登録されたユーザーなのかがわかります。
この情報は、意外といろいろな場面で役立ちます。
| 活用場面 | 内容 |
|---|---|
| 新規登録ユーザーの分析 | 今月何人登録したかを調べられる |
| 並び替え | 新しい順、古い順で一覧表示できる |
| 問い合わせ調査 | いつ作られたデータなのか確認できる |
| 初回特典の判定 | 登録から何日以内かを判定できる |
created_atは、データの誕生日のようなものです。
人に誕生日があるように、データにも「生まれた日時」があります。
updated_atとは何か
updated_atとは、そのデータが最後に更新された日時を表すカラムです。
updatedは「更新された」という意味です。
つまり、updated_atは「最後に更新された時点」という意味です。
たとえば、商品テーブルを考えてみましょう。
| car_id | name | price | created_at | updated_at |
|---|---|---|---|---|
| 1 | プリウス | 2500000 | 2026-06-01 10:30:00 | 2026-06-01 10:30:00 |
最初に登録した時点では、created_atとupdated_atが同じ日時になることが多いです。
その後、価格を250万円から240万円に変更したとします。
| car_id | name | price | created_at | updated_at |
|---|---|---|---|---|
| 1 | プリウス | 2400000 | 2026-06-01 10:30:00 | 2026-06-05 15:20:00 |
created_atは変わりません。
なぜなら、作成日時は変わらないからです。
一方で、updated_atは更新日時に変わります。
updated_atを見ると、「このデータは最後にいつ直されたのか」がわかります。
updated_atは、ノートの最終更新日のようなものです。
ノートを最初に買った日はcreated_atです。
最後に書き込みをした日はupdated_atです。
deleted_atとは何か
deleted_atとは、そのデータが削除扱いになった日時を表すカラムです。
deletedは「削除された」という意味です。
つまり、deleted_atは「削除された時点」という意味です。
ただし、ここが少し大切です。
deleted_atを使う場合、データを本当にDELETE文で消すとは限りません。
deleted_atに日時を入れて、「削除されたものとして扱う」ことが多いです。
この考え方を論理削除と呼びます。
論理削除とは、データそのものはテーブルに残したまま、削除済みとして扱う方法です。
反対に、DELETE文でデータを完全に消す方法を物理削除と呼びます。
| 削除方法 | 意味 | イメージ |
|---|---|---|
| 物理削除 | データを完全に消す | ノートのページを破って捨てる |
| 論理削除 | 削除日時を入れて削除扱いにする | ノートに赤字で「使用しない」と書く |
たとえば、carsテーブルに次のデータがあるとします。
| car_id | name | price | deleted_at |
|---|---|---|---|
| 1 | プリウス | 2400000 | NULL |
| 2 | 古い車両 | 300000 | 2026-06-10 09:00:00 |
deleted_atがNULLのデータは、まだ削除されていないデータです。
deleted_atに日時が入っているデータは、削除済みとして扱います。
一覧画面に表示したいのは、基本的にdeleted_atがNULLのデータだけです。
SELECT car_id, name, price, created_at, updated_at, deleted_at FROM cars WHERE deleted_at IS NULL;このSQLによって、削除済みではない車だけを取得できます。
3つのカラムの違い
created_at、updated_at、deleted_atの違いを表で整理しましょう。
| カラム名 | 意味 | 変更されるタイミング | たとえ |
|---|---|---|---|
| created_at | 作成日時 | データを作成したとき | データの誕生日 |
| updated_at | 更新日時 | データを変更したとき | ノートの最終記入日 |
| deleted_at | 削除日時 | 論理削除したとき | 使用停止になった日 |
この3つを入れておくと、データの状態がかなり追いやすくなります。
「いつ作られたのか」
「いつ変更されたのか」
「削除済みなのか」
この3つがわかるだけで、運用中の調査がかなり楽になります。
MySQLでのテーブル定義例
carsテーブルを例にして、created_at、updated_at、deleted_atを入れたテーブル定義を見てみましょう。
CREATE TABLE cars (
car_id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price INT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL
);それぞれの意味を見ていきます。
| 定義 | 意味 |
|---|---|
| created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | 作成時に現在日時を自動で入れる |
| updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 作成時と更新時に現在日時を自動で入れる |
| deleted_at DATETIME NULL | 削除されていなければNULL、削除時に日時を入れる |
CURRENT_TIMESTAMPは、現在日時を表します。
DEFAULT CURRENT_TIMESTAMPを指定すると、INSERT時に自動で現在日時が入ります。
ON UPDATE CURRENT_TIMESTAMPを指定すると、UPDATE時に自動で現在日時が更新されます。
自動販売機でたとえるなら、ボタンを押した瞬間に自動で時刻が記録されるようなものです。
INSERT時の動き
新しい車を登録するときは、次のようにINSERTします。
INSERT INTO cars (name, price) VALUES ('プリウス', 2500000);created_atとupdated_atは、MySQLが自動で入れてくれます。
| car_id | name | price | created_at | updated_at | deleted_at |
|---|---|---|---|---|---|
| 1 | プリウス | 2500000 | 2026-06-01 10:30:00 | 2026-06-01 10:30:00 | NULL |
登録直後は、created_atとupdated_atが同じ日時になります。
作成した瞬間が、最後に更新した瞬間でもあるからです。
UPDATE時の動き
価格を変更するとします。
UPDATE cars SET price = 2400000 WHERE car_id = 1 AND deleted_at IS NULL;updated_atにON UPDATE CURRENT_TIMESTAMPを指定している場合、updated_atは自動で更新されます。
| car_id | name | price | created_at | updated_at | deleted_at |
|---|---|---|---|---|---|
| 1 | プリウス | 2400000 | 2026-06-01 10:30:00 | 2026-06-05 15:20:00 | NULL |
ここで大切なのは、WHERE deleted_at IS NULLを入れている点です。
削除済みのデータを間違って更新しないためです。
実務では、論理削除を使うテーブルに対して、更新や検索をするときにdeleted_at IS NULLを忘れないようにしましょう。
DELETEではなく論理削除する
論理削除では、DELETE文を使わずにUPDATE文を使います。
UPDATE cars SET deleted_at = CURRENT_TIMESTAMP WHERE car_id = 1 AND deleted_at IS NULL;このSQLを実行すると、データはテーブルに残ります。
ただし、deleted_atに日時が入るため、削除済みとして扱われます。
| car_id | name | price | deleted_at |
|---|---|---|---|
| 1 | プリウス | 2400000 | 2026-06-10 09:00:00 |
この状態になったデータは、通常の一覧では表示しません。
SELECT car_id, name, price FROM cars WHERE deleted_at IS NULL;削除済みデータも管理者だけ見たい場合は、条件を変えます。
SELECT car_id, name, price, deleted_at FROM cars WHERE deleted_at IS NOT NULL;deleted_atがあると、「削除済み一覧」も作れます。
ゴミ箱機能のようなものですね。
deleted_atを使うメリット
deleted_atを使う大きなメリットは、データを復元できることです。
物理削除でDELETEしてしまうと、基本的にデータは消えます。
バックアップがなければ戻すのは難しいです。
一方で、論理削除ならdeleted_atをNULLに戻せば復元できます。
UPDATE cars SET deleted_at = NULL WHERE car_id = 1;このSQLで、削除済み扱いだった車を復元できます。
スマートフォンの写真アプリにある「最近削除した項目」に似ています。
完全に消す前に、一定期間残しておけるので安心です。
| メリット | 内容 |
|---|---|
| 復元できる | 間違って削除しても戻せる |
| 削除履歴が残る | いつ削除したか確認できる |
| 監査に使える | 運用上の調査に役立つ |
| 関連データとの整合性を保ちやすい | 急にデータが消えて参照できなくなる問題を減らせる |
監査とは、あとから問題が起きたときに、何がいつ起きたのか確認することです。
システム運用では、「誰がいつ何をしたのか」を追えることがとても重要です。
deleted_atを使うデメリット
deleted_atは便利ですが、デメリットもあります。
一番多いミスは、検索時にdeleted_at IS NULLを忘れることです。
削除済みデータまで一覧に出てしまいます。
| デメリット | 内容 |
|---|---|
| 検索条件が増える | 毎回deleted_at IS NULLを意識する必要がある |
| データ量が増える | 削除済みデータもテーブルに残る |
| 一意制約が難しくなる場合がある | 削除済みデータと同じ値を再登録したいときに工夫が必要 |
| 本当に消したいデータには向かない場合がある | 個人情報などは保存し続けることが問題になる場合がある |
たとえば、メールアドレスにUNIQUE制約を付けているusersテーブルを考えてみます。
論理削除されたユーザーのメールアドレスが残っていると、同じメールアドレスで再登録できない場合があります。
このような場合は、設計段階でルールを決めておく必要があります。
論理削除は便利ですが、何でもかんでも使えばよいわけではありません。
created_at・updated_at・deleted_atのよくある使い方
実務では、次のような使い方をよくします。
| やりたいこと | 使うカラム | SQL例 |
|---|---|---|
| 新しい順に表示する | created_at | ORDER BY created_at DESC |
| 最近更新されたデータを探す | updated_at | ORDER BY updated_at DESC |
| 削除されていないデータだけ表示する | deleted_at | WHERE deleted_at IS NULL |
| 削除済みデータを探す | deleted_at | WHERE deleted_at IS NOT NULL |
| 今月作られたデータを集計する | created_at | WHERE created_at >= '2026-06-01' |
たとえば、車を新しい順に表示するSQLです。
SELECT car_id, name, price, created_at FROM cars WHERE deleted_at IS NULL ORDER BY created_at DESC;最近更新された車を表示するSQLです。
SELECT car_id, name, price, updated_at FROM cars WHERE deleted_at IS NULL ORDER BY updated_at DESC;削除済みの車だけを確認するSQLです。
SELECT car_id, name, price, deleted_at FROM cars WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC;JavaのDAOで使う場合
JavaでDAOを作る場合も、deleted_at IS NULLを忘れないことが大切です。
たとえば、削除されていない車だけを取得するDAOのSQLは次のようになります。
SELECT car_id, name, price, created_at, updated_at, deleted_at FROM cars WHERE deleted_at IS NULL ORDER BY created_at DESC;論理削除する場合は、DELETEではなくUPDATEを使います。
UPDATE cars SET deleted_at = CURRENT_TIMESTAMP WHERE car_id = ? AND deleted_at IS NULL;?には、PreparedStatementでcar_idを入れます。
PreparedStatementとは、SQLに安全に値を渡すための仕組みです。
値を文字列で直接つなげるより安全です。
新人エンジニアのうちは、次の考え方を覚えてください。
| 処理 | SQL |
|---|---|
| 登録 | INSERT |
| 更新 | UPDATEとupdated_at |
| 論理削除 | UPDATEでdeleted_atに現在日時を入れる |
| 検索 | WHERE deleted_at IS NULLを入れる |
Javaで扱うDTOの例
created_at、updated_at、deleted_atをJavaで扱うなら、DTOやEntityに日時用のフィールドを用意します。
package com.example.demo;
import java.time.LocalDateTime;
public class CarDto {
private Long carId;
private String name;
private Integer price;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private LocalDateTime deletedAt;
public Long getCarId() {
return carId;
}
public void setCarId(Long carId) {
this.carId = carId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}
public void setDeletedAt(LocalDateTime deletedAt) {
this.deletedAt = deletedAt;
}
}Javaでは、日時をLocalDateTimeで扱うことが多いです。
LocalDateTimeとは、日付と時刻を表すJavaのクラスです。
たとえば、2026年6月1日 10時30分のような情報を扱えます。
ResultSetから日時を取り出す例
JDBCでResultSetからDATETIMEを取り出す場合は、Timestampを経由してLocalDateTimeに変換することがあります。
package com.example.demo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
public class Snippet {
private static final String DB_URI =
"jdbc:mysql://localhost:3306/sip_a?"
+ "characterEncoding=utf8&useSSL=false"
+ "&serverTimezone=GMT%2B9"
+ "&rewriteBatchedStatements=true";
private static final String DB_USER = "newuser";
private static final String DB_PASS = "0";
// DB接続を取得
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(DB_URI, DB_USER, DB_PASS);
}
// 車の全件リストを取得
public List<CarDto> getCarList() {
List<CarDto> carList = new ArrayList<>();
String sql = "SELECT * FROM cars";
try (Connection con = getConnection();
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
CarDto carDto = new CarDto();
carDto.setCarId(rs.getLong("car_id"));
carDto.setName(rs.getString("name"));
carDto.setPrice(rs.getInt("price"));
// ★ 元のスニペット:Timestampをnullチェックしてからセット
Timestamp createdAt = rs.getTimestamp("created_at");
if (createdAt != null) {
carDto.setCreatedAt(createdAt.toLocalDateTime());
}
Timestamp updatedAt = rs.getTimestamp("updated_at");
if (updatedAt != null) {
carDto.setUpdatedAt(updatedAt.toLocalDateTime());
}
Timestamp deletedAt = rs.getTimestamp("deleted_at");
if (deletedAt != null) {
carDto.setDeletedAt(deletedAt.toLocalDateTime());
}
carList.add(carDto);
}
} catch (SQLException e) {
e.printStackTrace();
}
return carList;
}
public static void main(String[] args) {
Snippet snippet = new Snippet();
List<CarDto> carList = snippet.getCarList();
if (carList.isEmpty()) {
System.out.println("データが見つかりませんでした");
} else {
carList.forEach(System.out::println);
}
}
}deleted_atはNULLになることが多いため、NULLチェックが重要です。
NULLとは、値が存在しないことを表します。
deleted_atがNULLなら、削除されていないという意味です。
deleted_atを使うときの注意点
論理削除を使うときは、検索、更新、集計でdeleted_at IS NULLを意識してください。
特に一覧画面や検索機能では忘れやすいです。
悪い例です。
SELECT car_id, name, price FROM cars;このSQLだと、削除済みの車も表示されます。
良い例です。
SELECT car_id, name, price FROM cars WHERE deleted_at IS NULL;削除済みデータを除外できます。
たとえるなら、図書館の本棚に「廃棄予定の本」を並べないようにするイメージです。
倉庫には残っていても、利用者の目に見える棚には出しません。
created_at・updated_at・deleted_atにインデックスは必要か
インデックスとは、データ検索を速くするための仕組みです。
本の索引に似ています。
索引があれば、目的のページをすばやく探せますよね。
データベースでも同じです。
検索条件や並び替えによく使うカラムには、インデックスを付けることがあります。
たとえば、deleted_atでよく検索するなら、インデックスを検討します。
CREATE INDEX idx_cars_deleted_at ON cars (deleted_at);created_atで新しい順に並べることが多いなら、created_atのインデックスも検討できます。
CREATE INDEX idx_cars_created_at ON cars (created_at);ただし、インデックスは付ければ付けるほど良いわけではありません。
検索は速くなりやすいですが、登録や更新は少し重くなる場合があります。
学校のプリントに索引を作る作業を想像してください。
探すときは便利ですが、プリントを追加するたびに索引も直す必要がありますよね。
インデックスも同じです。
日時カラムの命名ルール
created_at、updated_at、deleted_atのように、スネークケースで書くことが多いです。
スネークケースとは、単語をアンダースコアでつなぐ書き方です。
created_at updated_at deleted_atJavaでは、キャメルケースにすることが多いです。
createdAt updatedAt deletedAtキャメルケースとは、2語目以降の先頭を大文字にする書き方です。
データベースではcreated_at、JavaではcreatedAtと覚えておくとよいです。
| 場所 | 書き方 | 例 |
|---|---|---|
| データベース | スネークケース | created_at |
| Java | キャメルケース | createdAt |
よくある設計ミス
新人エンジニアがやりがちなミスも確認しておきましょう。
| ミス | 問題点 |
|---|---|
| deleted_at IS NULLを忘れる | 削除済みデータが表示される |
| created_atを更新してしまう | 作成日時がわからなくなる |
| updated_atが更新されない | 最後に変更された日時が追えない |
| deleted_atをNOT NULLにする | 未削除の状態を表しにくくなる |
| 物理削除と論理削除を混ぜる | データの扱いがわかりにくくなる |
特に、created_atを更新してしまうミスには注意してください。
created_atは作成日時です。
一度決まったら基本的に変えません。
誕生日を毎年変えないのと同じです。
created_at・updated_at・deleted_atを使うべきテーブル
すべてのテーブルに必ず必要とは限りませんが、業務データには入れておくことが多いです。
| テーブル例 | 入れる理由 |
|---|---|
| users | 登録日時や退会状態を管理したい |
| cars | 商品情報の更新や削除状態を管理したい |
| orders | 注文日時や変更履歴が重要になる |
| posts | 投稿日時、編集日時、削除状態を管理したい |
| comments | コメントの作成、編集、削除を管理したい |
一方で、完全に一時的なデータや、ログ専用のデータでは設計が変わる場合もあります。
ただし、新人エンジニアのうちは、業務テーブルにはcreated_at、updated_at、deleted_atを入れることが多いと覚えておくとよいです。
まとめ
created_at、updated_at、deleted_atは、データの状態を管理するための重要な日時カラムです。
| カラム | 意味 | 主な使い方 |
|---|---|---|
| created_at | 作成日時 | 新しい順の表示、登録数の集計 |
| updated_at | 更新日時 | 最近変更されたデータの確認 |
| deleted_at | 削除日時 | 論理削除、復元、削除済み一覧 |
一言でまとめるなら、こうです。
created_atはデータの誕生日、updated_atは最後に直した日、deleted_atは削除扱いになった日です。
この3つを正しく使うと、データの管理がかなり楽になります。
特にdeleted_atを使う場合は、検索時にdeleted_at IS NULLを忘れないでください!
今後は、論理削除と物理削除の違い、インデックス、監査ログ、タイムゾーン、JDBCでのLocalDateTimeの扱い、Spring BootのJPA監査機能を順番に学ぶと、実務でのデータ設計がかなり理解しやすくなります。
まずは自分でcarsテーブルを作り、INSERT、UPDATE、論理削除、復元のSQLを実際に動かしてみてください。日時カラムの動きを手で確認すると、データベース設計の感覚が一気につかめます!
セイ・コンサルティング・グループでは新人エンジニア研修のアシスタント講師を募集しています。
投稿者プロフィール


