はじめに
たまたまタイトルのことを知る機会があり、自分でまとめたものをAIに食わせて加筆修正させました。
ちなみに私は基本的なCRUD操作がわかる程度のクソザコです。
📚 削除フラグとは?
そもそも削除フラグとは、データを物理的に削除せず「削除済み」という状態にする「論理削除」と呼ばれる手法です。
-- 物理削除:データそのものを削除
DELETE FROM users WHERE id = 1;
-- 論理削除:削除フラグを立てるだけ
UPDATE users SET is_deleted = true WHERE id = 1;
一見便利そうに見えるこの手法ですが、実は多くの問題を抱えています。
⚠️ 削除フラグの4つの問題
1. 🔍 クエリが複雑になる
最も大きな問題は、すべてのデータ取得クエリで「削除されていないデータ」という条件を毎回追加する必要があることです。
-- 削除フラグがない場合(シンプル!)
SELECT * FROM users;
-- 削除フラグがある場合(毎回WHERE句が必要...)
SELECT * FROM users WHERE is_deleted = false;
問題点:
- WHERE句を忘れると削除済みデータまで取得してしまう
- JOINするテーブルすべてに条件が必要
- コードレビューで見落としやすい
2. 🔑 データの一意性が壊れる
データベースのユニーク制約が、削除フラグのせいでうまく機能しなくなります。
具体例:
-
taro@example.comというユーザーが退会(is_deleted = true) - 新しい別のユーザーが同じ
taro@example.comで登録しようとする - データベース「既に存在します!」→ エラー 😱
これを避けるためには、ユニーク制約の設計を複雑にしたり、アプリケーション側で複雑なチェックロジックを追加したりする必要があり、設計が歪んでいきます。
3. 📈 データ肥大化によるパフォーマンス低下
論理削除されたデータはテーブルに残り続けるため、データベースのサイズがどんどん大きくなります。
影響:
- 💰 ストレージコストの増加:ディスク容量を圧迫
- 🐢 パフォーマンスの低下:インデックスサイズ増大により検索・更新が遅くなる
- ⏰ バックアップ時間の増加:リストアにも時間がかかる
4. 🤔 意図が不明確になる
is_deleted = trueという状態は、単に「削除された」という意味しか持ちません。
失われる情報:
- なぜ削除されたのか?(ユーザー退会?管理者による無効化?)
- いつ削除されたのか?
- 誰が削除したのか?
💡 3つの対策とそれぞれのトレードオフ
対策1:状態カラムとして扱う 📊
「削除されたかどうか」の二元論ではなく、データがどのような状態にあるかを示すstatusカラムを持たせます。
実装例:
| id | name | status | |
|---|---|---|---|
| 1 | 田中 | tanaka@... | active |
| 2 | 鈴木 | suzuki@... | suspended |
| 3 | 佐藤 | sato@... | withdrawn |
-- 意図が明確なクエリ
SELECT * FROM users WHERE status = 'active';
-- 一時停止中のユーザーのみ取得も簡単
SELECT * FROM users WHERE status = 'suspended';
✅ メリット
- クエリの意図が明確で読みやすい
- ビジネスロジックに応じた柔軟な状態管理が可能
- 将来的な状態追加も容易(
pending,reviewingなど)
❌ デメリット
- WHERE句は依然として必要
- statusカラムにインデックスを貼っても、偏りがあると効きにくい
- ユニーク制約の問題は部分的にしか解決しない
💭 多くのアプリケーションで「現実的な最初の選択肢」として採用されています
対策2:履歴テーブルに移動させる 📦
削除されたデータを別のテーブル(履歴テーブル)に移動させる方法です。
テーブル設計例:
-
current_users:現在有効なユーザーのみ -
withdrawn_users:退会済みユーザー -
banned_users:規約違反ユーザー
-- 退会処理のトランザクション
BEGIN TRANSACTION;
-- 1. 現在のデータを取得
SELECT * FROM current_users WHERE id = ? FOR UPDATE;
-- 2. 履歴テーブルに挿入
INSERT INTO withdrawn_users (...) VALUES (...);
-- 3. 現在のテーブルから削除
DELETE FROM current_users WHERE id = ?;
COMMIT;
✅ メリット
- アクティブなテーブルが常に軽量でパフォーマンス良好
- WHERE句不要でクエリがシンプル
- データは完全に保持される
❌ デメリット
- 実装コストが高い(トランザクション処理が複雑)
- 複数テーブルを横断した検索が面倒
- 外部キー制約の管理が難しい
対策3:イミュータブルデータモデル 🔒
データを「削除」も「更新」もせず、すべての変更をイベントとして記録する方法です。
イベントテーブルの例:
| event_id | user_id | event_type | value | created_at |
|---|---|---|---|---|
| 1 | A | USER_CREATED | {"name": "田中", "email": "..."} | 2024-01-10 |
| 2 | A | NAME_CHANGED | {"name": "鈴木"} | 2025-08-28 |
| 3 | A | USER_WITHDRAWN | {"reason": "自主退会"} | 2025-08-29 |
-- 現在の状態を取得(最新のイベントを集約)
WITH latest_events AS (
SELECT DISTINCT ON (user_id, event_type)
user_id, event_type, value, created_at
FROM user_events
ORDER BY user_id, event_type, created_at DESC
)
SELECT * FROM latest_events WHERE user_id = ?;
✅ メリット
- 完璧な監査ログ(いつ・誰が・何を変更したか完全に記録)
- 任意の時点の状態を再現可能
- データの復元が安全かつ容易
❌ デメリット
- 実装の複雑性が非常に高い
- データ量が爆発的に増える
- 「現在の状態」を取得するクエリが複雑
📊 まとめ:どの手法を選ぶべき?
| 手法 | 実装コスト | パフォーマンス | 監査性 | おすすめ度 |
|---|---|---|---|---|
| 削除フラグ | 🟢 低 | 🔴 悪い | 🔴 低い | ⭐ |
| 状態カラム | 🟢 低 | 🟡 普通 | 🟡 普通 | ⭐⭐⭐⭐ |
| 履歴テーブル | 🟡 中 | 🟢 良い | 🟡 普通 | ⭐⭐⭐ |
| イミュータブル | 🔴 高 | 🟡 普通 | 🟢 高い | ⭐⭐ |
🎯 実践的な選び方
1️⃣ まずは「状態カラム」から始める
- 実装が簡単で、多くのケースで十分な解決策
- 後から他の手法に移行することも可能
2️⃣ パフォーマンスが重要な場合
- 大量のデータを扱うテーブル → 履歴テーブルを検討
- 削除済みデータの割合が高い場合も履歴テーブルが有効
3️⃣ 監査要件が厳しい場合
- 金融・医療・法務系のシステム → イミュータブルを検討
- ただし、チームの技術力と相談して決定
🚀 最後に
「削除フラグ」は一見シンプルで便利そうに見えますが、実は多くの落とし穴があります。
最初は「状態カラム」で始めて、システムの成長に合わせて適切な手法を選択していくのが現実的なアプローチです。完璧な設計を目指すよりも、要件に合った「ちょうどいい」設計を選ぶことが大切ですね!