MySQL の論理削除と物理削除

レコードを削除する方法には、物理削除(実際に行を消す)と論理削除(削除フラグを立てる)の2つのアプローチがあります。どちらを採用するかはビジネス要件とシステム特性によって決まり、一律にどちらが優れているというものではありません。

物理削除

物理削除は DELETE 文でレコードそのものをテーブルから除去する方法です。

-- 物理削除
DELETE FROM users WHERE id = 42;

削除したデータは(バックアップがなければ)復元できません。シンプルで分かりやすく、テーブルサイズが肥大化しないという利点があります。

論理削除

論理削除は、削除フラグや削除日時のカラムを用意し、レコード自体は残したまま「削除済み」として扱う方法です。

CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(254) NOT NULL UNIQUE,
  deleted_at DATETIME DEFAULT NULL  -- NULL なら有効、値があれば削除済み
);

-- 論理削除(UPDATE で削除日時を設定)
UPDATE users SET deleted_at = NOW() WHERE id = 42;

-- 有効なユーザーだけ取得
SELECT * FROM users WHERE deleted_at IS NULL;

2つのアプローチの比較

物理削除

テーブルがクリーンに保たれる。クエリに WHERE 条件を追加する必要がない。UNIQUE 制約が素直に機能する。ストレージを節約できる。

論理削除

データの復元が容易。削除履歴を追跡できる。外部キーの参照整合性が崩れない。監査要件に対応しやすい。

論理削除の実装パターン

論理削除の実装には主に2つのパターンがあります。

deleted_at カラム(DATETIME 型)

削除日時を記録する方式。いつ削除されたか分かるため、監査ログとしても機能します。NULL が「有効」、値が入っていれば「削除済み」という判定になります。

is_deleted カラム(BOOLEAN 型)

単純なフラグ方式。削除日時が不要な場合はこちらのほうがシンプルです。ただし、いつ削除されたかの情報が失われるため、実務では deleted_at のほうが好まれます。

論理削除の落とし穴

論理削除は便利ですが、運用上の問題がいくつかあります。

1つ目は、すべての SELECT クエリに WHERE deleted_at IS NULL を付ける必要があることです。これを忘れると削除済みデータが表示されてしまいます。ORM のグローバルスコープ機能で自動的にフィルタする仕組みが一般的ですが、生 SQL を書く場面では注意が必要です。

2つ目は、UNIQUE 制約との競合です。メールアドレスに UNIQUE 制約がある場合、ユーザーを論理削除しても同じメールアドレスで再登録できません。

-- 解決策:部分インデックス(MySQL 8.0 の関数インデックスを利用)
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(254) NOT NULL,
  deleted_at DATETIME DEFAULT NULL
);

-- deleted_at が NULL のレコードだけで一意性を保証
CREATE UNIQUE INDEX uk_email_active 
ON users ((CASE WHEN deleted_at IS NULL THEN email END));

3つ目は、テーブルの肥大化です。削除済みデータが蓄積し続けるため、テーブルサイズが膨らんでクエリ性能に影響します。定期的に古い論理削除データを物理削除するバッチ処理を設けることで対処できます。

実務での使い分け

法的にデータ保持が求められるもの(取引履歴、監査ログ)は論理削除が適しています。一方、セッション情報やキャッシュのような一時データは物理削除で問題ありません。プロジェクト全体で方針を統一し、テーブルごとにどちらを採用するかを設計書に明記しておくのがベストプラクティスです。