論理削除
レコードを物理的に削除せず、削除フラグで非表示にするデータ管理手法
データベース設計
論理削除とは
論理削除 (Soft Delete) は、データベースのレコードを DELETE 文で物理的に削除するのではなく、deleted_at タイムスタンプや is_deleted フラグを設定して論理的に削除する手法である。データを残しつつ、アプリケーションからは「削除された」ように見せる。
物理削除との比較
| 観点 | 論理削除 | 物理削除 |
|---|---|---|
| データ復旧 | フラグを戻すだけ | バックアップからの復元 |
| 監査証跡 | 削除履歴が残る | 履歴が消える |
| クエリ性能 | WHERE 条件が常に必要 | テーブルサイズが小さく保たれる |
| ストレージ | 削除データが蓄積 | 不要データが解放される |
| 外部キー | 参照整合性が保たれる | CASCADE DELETE が必要 |
| GDPR 対応 | 不十分 (データが残る) | 対応可能 |
実装パターン
deleted_at タイムスタンプ (推奨)
-- 論理削除
UPDATE users SET deleted_at = NOW() WHERE id = 123;
-- 取得時は常にフィルタ
SELECT * FROM users WHERE deleted_at IS NULL;
-- 復元
UPDATE users SET deleted_at = NULL WHERE id = 123;
deleted_at は is_deleted (boolean) より情報量が多い。いつ削除されたかが分かるため、監査やデバッグに有用だ。
ユニーク制約との共存
論理削除で問題になるのがユニーク制約だ。ユーザーがメールアドレス user@example.com で登録し、論理削除後に同じメールで再登録しようとすると、ユニーク制約に違反する。
-- PostgreSQL: 部分インデックスで解決
CREATE UNIQUE INDEX idx_users_email_active
ON users (email) WHERE deleted_at IS NULL;
DynamoDB での論理削除 + TTL
DynamoDB では TTL を使った「遅延物理削除」が実用的だ。
// 論理削除: deleted_at を設定し、TTL に 30 日後を設定
await ddb.send(new UpdateCommand({
TableName: 'Users',
Key: { userId: '123' },
UpdateExpression: 'SET deletedAt = :now, expiresAt = :ttl',
ExpressionAttributeValues: {
':now': new Date().toISOString(),
':ttl': Math.floor(Date.now() / 1000) + 30 * 86400, // 30日後
},
}));
// 取得時: 論理削除されたアイテムを除外
const result = await ddb.send(new QueryCommand({
TableName: 'Users',
FilterExpression: 'attribute_not_exists(deletedAt)',
}));
30 日間は復元可能で、期限後に DynamoDB の TTL が自動的に物理削除する。
論理削除の落とし穴
WHERE 条件の付け忘れ
全クエリに WHERE deleted_at IS NULL が必要だが、1 箇所でも忘れると削除済みデータが表示される。ORM のデフォルトスコープやミドルウェアで自動付与する。
データの肥大化
論理削除されたデータが蓄積し、テーブルサイズが肥大化する。定期的に古い論理削除データを物理削除するバッチ処理を設ける。
GDPR の「忘れられる権利」
GDPR のデータ削除要求には論理削除では不十分で、物理削除が必要になる場合がある。個人情報を含むフィールドだけを物理的に消去し、レコード自体は監査用に残す方法もある。
論理削除の背景や設計思想は関連書籍に詳しい。