やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

中間テーブルへのデータ更新(削除・追加)を効率的に行う方法

以下のようなテーブルがあったとします。
ユーザーは記事(posts)に対して、自由にタグ(tags)を設定することが可能です。
記事とタグは多対多の関係にあるため、中間テーブル(post_tags)にて記事へ設定したタグの一覧を管理しています。

仕様上、ユーザーは記事に設定したタグを自由に付け外しできます。
既存のある記事にタグA・Bが設定されている状態で、新たにユーザーがタグBを外して、タグCを設定したとします。最終的に中間テーブルには記事とタグA・Cが紐付くデータが残ります。 この場合、中間テーブルのデータ更新のアプローチには大きく2つのアイディアが考えられます。

  • 全てのデータを一度、削除して新たにデータを作成し直す
  • 既存のデータは残したまま不必要なデータは削除して、新たにデータを作成する

どちらのやり方にもメリット・デメリットがあり、当然ながらトレードオフです。
前者のアプローチはシンプルで簡単に実装できそうですが、IOのコストが高いため、タグが100個も設定されるような状態を考えるとパフォーマンスが悪いかもしれません。一方、後者のアプローチは複雑さはありますが、必要なデータのみ削除・追加すれば良いため前者のアプローチと比べるとIOのコストが低いです。

今回は後者の必要なデータのみ削除・追加する方法について紹介します。

削除・追加するデータの判定

既存のデータがある状態で、どうやって削除・追加するデータの判定をすれば良いのでしょうか。
色々と考えたのですが、最終的には集合演算の差(difference)を使えば良いということに気づきました。
例えば既存のデータ(とある記事に割り当てられたタグ一覧)に[1,2,3]がある場合に、新しく[1,2,4]を設定したいとすると、削除するデータは[3]であり、追加するデータは[4]です。これは集合演算の差で求めることができます。

require 'set'

exists = Set.new([1,2,3])
news = Set.new([1,2,4])

deletes = exists - news
adds = news - exists

puts deletes # [3]
puts adds # [4]

あとはDELETEINSERTのクエリを同トランザクションで実行すれば、データの更新は完了です。
プログラミング言語は問わないのでActiveRecrodなどのORMを使っても良いですし、SQLで書いても良いです。
先ほど紹介した集合演算はSQLでも行えますが面倒なので、事前にIDの一覧を取得して削除・追加するデータを言語の処理系で算出するのが良いでしょう。

BEGIN;
DELETE FROM post_tags WHERE post_id = 1 AND tag_id = 3;
INSERT INTO post_tags (post_id, tag_id) VALUES (1, 4);
COMMIT;

前提条件

集合演算の差を使う方法は、以下2つの前提条件をクリアする必要があります。

  • 既存のデータ一覧が取得できること
  • 新しく設定するデータの一覧が取得できること

既存のデータ一覧の取得はデータベースから持ってくれば困ることはありません。
新しく設定するデータの一覧はユーザー側から提供してもらう必要があります。一例として、タグの再設定を行うAPIを提供する場合、リクエストボディには既存タグを含めて新しく追加したいタグ情報をリクエストしてもらうのが良いです。

1,2は登録済みで4が新規

{  tag_ids: [1,2,4] }

データの削除・追加をまとめて行いたいが...

自分が調べた限りではSQLでデータの削除・追加をまとめて行う機能(DELINSERTみたいなの)は提供されていませんでした。 やれないことはない...?みたいですが、複雑にしてやるぐらいなら、自分はシンプルに削除と追加は別のクエリとしてトランザクション内で実行した方が良いと考えています。
削除・追加するデータの算出についても同じです。言語の処理系なら簡単に算出できるでしょう。
また、頑張って複雑なSQLを書くよりもクエリを分割した方が、シンプルかつハイパフォーマンスになる場合があります。

少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。