メインコンテンツへスキップ

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples on this page work on VillageSQL. Install Now →
ソフトデリートは、行を削除する代わりに削除済みとしてマークします。行はテーブルに残り、アプリケーションからは見えず、1 つのステートメントで復元できます。「ユーザーが何かを削除して、元に戻したがっている」に対する標準的な答えですが、採用する前に理解しておくべき 2 つの問題を生みます。すべてのクエリがフィルタを忘れてはならないこと、そして一意制約が思っている意味を持たなくなることです。

基本パターン

NULL 許容のタイムスタンプを追加します。NULL は有効、タイムスタンプは削除済みを意味し、いつ削除されたかを記録します。
deleted ブール値よりもタイムスタンプを選んでください。コストは同じで、サポートチケットが必ず尋ねる「いつ」に追加コストなしで答えられます。

問題 1: 一意制約

email に対する一意インデックスは、再登録を永久にブロックします。ソフトデリートされた行がまだその値を占有しているためです。
解決策は関数インデックスの手法です。有効な行では一定で、削除済みの行では一意になる列を一意キーに含めます。生成列を使うときれいに実現できます。
MySQL の一意インデックスは任意の数の NULL を許容するため、削除済みの行が衝突することはなく、同じメールアドレスを持つ 2 つの有効な行は引き続き失敗します。仕組みについては生成列を参照してください。

問題 2: すべてのクエリが覚えていなければならない

WHERE deleted_at IS NULL を 1 つ忘れるだけで、削除済みのデータがレポートや API レスポンスに漏れます。緩和策は 2 つあります。
  • 有効な行のサブセットのビュー。 読み取り経路をビューに向ければ、フィルタを忘れることはできません。
  • ORM のスコープ。 ほとんどの ORM は、フィルタをグローバルに適用できます(Rails の default_scope、Django のマネージャー、Prisma のミドルウェア)。すべてのクエリを監査する代わりに、1 か所で有効にします。
外部キーにも検討が必要です。ソフトデリートされた親はまだ存在するため、ON DELETE CASCADE は決して発火しません。子が親と一緒に消えるべきなら、アプリケーション(またはトリガー)が自ら deleted_at をカスケードしなければなりません。

ソフトデリートすべきでない場合

  • 規制上の消去。 GDPR の削除要求は、データが隠されるのではなく消えることを意味します。ソフトデリートされた行も個人データとして数えられます。
  • 入れ替わりの激しいテーブル。 セッション、キューのジョブ、ログなど数百万件単位で削除されるものは、テーブルとそのすべてのインデックスを肥大化させるだけです。
  • 誰も復元しないデータ。 元に戻すユースケースがなければ、フィルタの負担は何ももたらしません。
よくある中間策: ユーザーの操作ではソフトデリートし、その後スケジュールされたジョブが、deleted_at が保持期間より古い行をハードデリートします。

関連項目

  • 生成列 — 一意制約の修正の背後にある仕組み
  • ビュー — フィルタを忘れることのできない有効行のビュー
  • 外部キー — ソフトデリートでカスケードが発火しない理由
  • MySQL の DELETE — このパターンが置き換えるハードデリート