テーブルを正規化するほど、アプリから使うのは難しくなる

DBのテーブル設計で、正規化するときれいだけど、アプリから使うとき難しくなる問題。

とても有名で今更な話ではあるけど、明文化したことはなかったので書き残しておく。

正規化していると、複数のテーブルから取る必要がある

たとえば、ユーザー一覧を表示するアプリで、それぞれのユーザーに付いたタグも一緒に表示したい f:id:castaneai:20170909234614p:plain

このように、テーブルが分かれていたら、両方のテーブルから取らないと表示できない。

しかし、アプリ側からはユーザーの保持している情報としてサッとタグ一覧を取り出したい。

同時に複数のテーブルから取り出したいなら、JOINを使ってDB側で結合か、2つのクエリに分けてアプリ側で結合となる。

-- JOIN
SELECT * FROM users LEFT JOIN tags ON users.id = tags.user_id;

-- 2クエリ
SELECT * FROM users WHERE id IN(1, 2);
SELECT * FROM tags WHERE user_id IN(1, 2);

JOINだと1回で済むが、DBが分割されていると無理になるし、複雑な条件が入るとSQLが読みづらいので、2クエリに分けるほうが個人的には好き

まるで1つのテーブルのように扱う

たとえば、ActiveRecordでは、次のようにすると user.tags だけでタグ一覧を取れる

class User < ApplicationRecord
  has_many :tags
end

class Tag < ApplicationRecord
  belongs_to :user
end

これすごい!

でも、楽しようとすると罠にかかることも。

たとえば、N+1問題

N+1問題

関連するデータを取ってくるときに、1つずつ取ってきてしまい、大量のクエリを実行してしまう問題。 くわしくは「N+1問題」で検索すると良い。

-- ユーザー一覧取得
SELECT * FROM users;

-- それぞれのユーザーについたタグを取得
SELECT * FROM tags where user_id = 1;
SELECT * FROM tags where user_id = 2;
SELECT * FROM tags where user_id = 3;
SELECT * FROM tags where user_id = 4;
SELECT * FROM tags where user_id = 5;
...

次のように、一覧のユーザー数が多くなっても、2つのクエリで済むようにすれば回避できる。

-- ユーザー一覧取得
SELECT * FROM users;

-- それぞれのユーザーについたタグを取得
SELECT * FROM tags where user_id IN (1, 2, 3, 4, 5, ...);

簡単な例ならわかりやすいが、複雑になってくると、熟練者でもN+1問題を踏んでしまいそう。

参考リンク