論理削除

レコードを物理的に削除せず、削除フラグで非表示にするデータ管理手法

データベース設計

論理削除とは

論理削除 (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_atis_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 のデータ削除要求には論理削除では不十分で、物理削除が必要になる場合がある。個人情報を含むフィールドだけを物理的に消去し、レコード自体は監査用に残す方法もある。

論理削除の背景や設計思想は関連書籍に詳しい。

関連用語