🎃

論理削除はアンチパターンなのか?

2023/12/25に公開

QiitaからZennに引越し中のため、Qiitaからコピーしてきた記事です

はじめに

この記事ではDB管理でよく使われる論理削除がアンチパターンかどうかを考えます。
結論から言うと、

  • 論理削除で実現したいことが他の方法でできないか検討する
  • 筆者はどちらかといえばアンチパターンだと思っている

もちろんシステムの要件においては論理削除の方が適切な場合もありますので、アンチパターンだと言い切ることはしません。
ただしシステムの規模が大きくなるにつれ副作用が顕著になるため、論理削除のメリットデメリットを考慮した上で論理削除を選択しましょう。

論理削除とは?

論理削除とは、データを実際には消さずにデータを削除されたように見せる処理のことです。
この処理では、DELETE文で物理的にレコードを削除するのではなく、is_deleteddeleted_atなどの削除フラグを立たせることによって削除されたとみなします。
削除フラグが立っている場合は、アプリケーション側などで削除されているものだと判断されます。

アンチパターンとは?

ソフトウェア開発におけるアンチパターンとは、一見良さそうな設計やアイデア、解決策に思えるものの、実際には問題を引き起こす原因となる誤ったアプローチのことです。
様々な現場で繰り返されており、開発の落とし穴のようなものとして扱われています。

論理削除のメリット

論理削除は確かに便利です。
主なメリットは以下の通りだと思います。

心理的に安心感がある
論理削除を採用する理由として最も多いのではないでしょうか。
論理削除だとデータとしては残っているため、消されたデータを容易に確認・復活させることができます。

削除したデータをすぐに戻すことができる
削除したデータを復元しなければならない時、削除フラグを戻すupdate文を実行するだけで復活させることができます。
物理削除だとバックアップからデータを復元する必要があるため、復元工数は高いです。

削除されたデータを取得することができる
データは残っているので、where句で削除フラグが立っているかを条件に指定すれば削除されたレコードを取得することができます。
物理削除だとデータが消えてしまうため、削除されたデータを取得することはできません。
後述しますが、「アーカイブテーブル」や「履歴テーブル」といった仕組みを利用して事前に削除対象のデータを別のテーブルに移動させておくことで、物理削除でも削除されたデータを取得することはできます。

論理削除のデメリット

本題に入りますが、論理削除は便利な一方で、デメリットも存在します。

パフォーマンスへの影響
削除したデータはレコードとして残り続けるため、テーブルのサイズが肥大化していきます。
少量のデータしか保存されないテーブルでは問題がないかもしれませんが、大量のデータが保存されるテーブルの場合は、検索や更新の際にパフォーマンスが低下するリスクがあります。

クエリの複雑化
データを取得する際、削除されたデータを除外する条件をクエリに設定する必要があります。
例えば、現在有効なツイートと投稿者を取得する場合、物理削除を採用していたら以下のようなクエリになるでしょう

物理削除
SELECT * FROM tweets
INNER JOIN users ON tweets.user_id = users.id;

しかし論理削除だと、下記になってしまいます。

論理削除
SELECT * FROM tweets
INNER JOIN users ON tweets.user_id = users.id
AND users.is_deleted = 0
WHERE tweets.is_deleted = 0;

もし他にも複数のテーブルをjoinさせていたら、削除済みかの条件を指定する部分がより複雑になっていくでしょう。
その結果is_deletedを考慮する部分が抜け漏れてしまったり、クエリの見通しが悪くなり、デバッグやメンテナンスが困難になることがあります。

ユニーク制約に工夫が必要
論理削除ではデータが残るため、ユニーク制約が期待通りに機能しない場合があります。
例えば、usersテーブルでは、メールアドレスにユニーク制約をかけることが一般的です。
しかし、論理削除を採用している場合、削除済みのデータもテーブルに残るため、新たに同じメールアドレスを登録することができません。

一つの解決策として、emailis_deletedカラムの複合ユニーク制約を設定する方法が考えられますが、この方法では同じメールアドレスを持つデータを複数回削除することができません。
解決策の一つとしてdeleted_atカラムを用いる方法がありますので、後述します。

論理削除はアンチパターンなのか?

先ほどアンチパターンとは「一見良さそうな設計やアイデア、解決策に思えるものの、実際には問題を引き起こす原因となる誤ったアプローチ」と説明しました。
私見ですが、メリットに着目しすぎてデメリットが与える影響を見誤ってしまうとアンチパターンに陥ってしまうケースが多いと思います。
アンチパターンの意味を分解し、上記のメリットデメリットを当てはめてみましょう。

一見良さそうな設計やアイデア、解決策に思えるものの、

  • 心理的に安心感がある
  • 削除したデータをすぐに戻すことができる
  • 削除されたデータを取得することができる

実際には問題を引き起こす原因となる誤ったアプローチ

  • パフォーマンスへの影響
  • クエリの複雑化
  • ユニーク制約に工夫が必要

論理削除対象のテーブルのレコード数やシステムの規模が大きくなればなるほど、デメリットの影響が顕著になりそうです。
システムの規模が大きくなりレコード数が増えれば、検索や更新速度は時間がかかります。
チームメンバーの入れ替わりもあれば、削除フラグを見落として削除した想定のデータを取得してしまうリスクも大きくなります。
何万ものユーザーついたDBの構造を後々になって変更するのは、非常に困難です。
長期的な視点で見れば、論理削除はアンチパターンになると言えるのではないのでしょうか。

論理削除の代替手段・Tipsなど

アーカイブテーブルを用意する
「事実だけを保存する」と言うやつですね。
削除されたデータを保持しておきたいことが目的なら、deleted_usersのような、削除されたユーザーを保存しておく専用のテーブル(アーカイブテーブル)を用いると良いでしょう。
usersテーブルでユーザーが物理削除されたら、トリガーなどで削除されたユーザーのデータをdeleted_usersにinsertするような流れですね。

注意点として、論理削除とアーカイブテーブルや履歴テーブルを使用する場合とでは、データが保存されるテーブルが異なるため、データベース全体のサイズに大きな変化はありません。
Viewを使う
削除フラグが既に設定されている場合は、Viewを使えば論理削除の条件を書く手間が減ります。
ただしViewに設定されているクエリが実行されますので、高速化などは見込めません。
deleted_atカラムを使う
論理削除を使用する場合の話ですが、ユニーク制約に工夫が必要と述べました。
そこで、deleted_atカラムを用意する方法があります。
この方法では、削除済みデータに対してdeleted_atカラムにタイムスタンプを設定し、emaildeleted_atカラムの複合ユニーク制約を設定することで、2人目以降も重複エラーにならず削除することができます。
ただし、依然としてWHERE句で削除フラグの考慮は必要ですが、Railsなどのフレームワークを使用している場合は、記述を隠蔽することができます。

一定時間経過したレコードは定期的に削除する
論理削除は積み上がっていくため、時間が経てば経つほどレコード数は増えます。
スケジューラーなどで参照されていない古いレコードは定期的に削除し、テーブルサイズの肥大化を少しでも防ぎましょう。

論理削除を適切に使うためのポイント

有名な言葉ですが、「とりあえず論理削除」は控えましょう。
副作用を理解した上で論理削除を選択すれば問題ありません。

個人的に、論理削除を選択しても副作用が少ない条件は以下の通りです。

  • システムが大きくなった際の副作用やデメリットを考慮した上で判断した場合
  • そのテーブルがjoinされる対象になるケースが少ない場合
  • テーブルのサイズが小さくなる想定の場合
  • ユニーク制約が不要の場合

論理削除を選択したい場合は削除したデータを残しておきたいケースがほとんどだと思うので、もし同じDBにデータを残したいのであればアーカイブテーブルを導入する方がいいと思います。
現在有効なデータと削除されたデータをテーブルで分けることができるので、集計もしやすくなります。

まとめ

論理削除は便利ですが、副作用も大きいです。
データを物理削除するのは不安だしとりあえず論理削除にしとこう、と言うのは一旦やめ、データベースの規模が大きくなった時のことを考えたり、論理削除・物理削除のメリットデメリットを適切に天秤にかけたりすることが大事だと思います。
下記に論理削除がアンチパターンか論じている参考資料をまとめておきますので、ご確認ください。

Discussion