DBのテーブル設計で、正規化するときれいだけど、アプリから使うとき難しくなる問題。
とても有名で今更な話ではあるけど、明文化したことはなかったので書き残しておく。
正規化していると、複数のテーブルから取る必要がある
たとえば、ユーザー一覧を表示するアプリで、それぞれのユーザーに付いたタグも一緒に表示したい
このように、テーブルが分かれていたら、両方のテーブルから取らないと表示できない。
しかし、アプリ側からはユーザーの保持している情報としてサッとタグ一覧を取り出したい。
同時に複数のテーブルから取り出したいなら、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問題を踏んでしまいそう。