はじめに
システム開発における永遠の課題、「N+1問題」。Entity FrameworkなどのO/Rマッパーを使っていると、.Include() や JOIN を使って解決するのが王道です。
しかし、実際の現場ではこういった状況に遭遇しませんか?
・レガシーなコードで、リレーションが綺麗に定義されていない
・結合したいデータが「データベース」と「CSV(または外部API)」で分かれている
・複雑なクエリになりすぎて、EF Coreの自動生成SQLが遅すぎる
そんな時、**「IDリストで一括取得して、メモリ上でDictionary化して紐付ける」**という戦法が非常に強力です。今回はその具体的な実装パターンを紹介します。
そもそもN+1問題とは(おさらい)
例えば、注文一覧(Orders)を表示する際に、注文者の名前(User.Name)を表示したいケース。
悪い例(N+1発生)C#// 1. 注文データを全件取得 (SQL 1回)
var orders = _repo.GetOrders();
foreach (var order in orders)
// 2. ループの中で都度ユーザー情報を取得 (SQL N回!!)
// 注文が1000件あれば1000回DBアクセスが発生
var user = _repo.GetUserById(order.UserId);
Console.WriteLine($"注文ID: {order.Id}, 注文者: {user.Name}");
これではDBサーバーが悲鳴を上げます。
解決策:Dictionaryキャッシュ戦法アプローチは以下の3ステップです。
1.注文データを取得する。
2.注文データに含まれる「ユーザーID」をリストアップし、IN句などでユーザーを一括取得する。
3.取得したユーザーをIDをキーにしたDictionaryに変換する。
実装コード
C#//
1. まず主となるデータ(注文)を取得 (SQL 1回目)
var orders = _repo.GetOrders();
2. 必要なユーザーIDのリストを作成 (重複排除)
var userIds = orders.Select(o => o.UserId).Distinct().ToList();
3. ユーザー情報を一括取得 (SQL 2回目)
イメージ: SELECT * FROM Users WHERE Id IN (1, 2, 5, ...)
var users = _repo.GetUsersByIds(userIds);
【重要】ここでDictionary化して「メモリ内検索」を爆速にする
Key: UserId, Value: Userオブジェクト
var userMap = users.ToDictionary(u => u.Id);
4. 紐付け処理
foreach (var order in orders)
{
DictionaryからO(1)で高速取得
if (userMap.TryGetValue(order.UserId, out var user))
{
Console.WriteLine($"注文ID: {order.Id}, 注文者: {user.Name}");
}
}
なぜDictionaryなのか?
List のまま Where や Find で検索すると、結局ループのたびにリスト全走査が発生し、計算量が $O(N^2)$ になってしまいます。ToDictionary してハッシュテーブル化することで、検索オーダーが $O(1)$ になり、件数が増えても処理速度が落ちません。
この手法のメリット・デメリット
メリット
・DBアクセスが確実に「2回」で済む(データ量に依存しない)。
・データソースが別々でも使える(例:OrdersはSQL Server、UsersはWeb APIから取得、
という場合でもメモリ上で結合できる)。
・発行されるSQLが単純なので、インデックスが効きやすくDB負荷予測が容易。
デメリット
・メモリ使用量:数万〜数十万件のオブジェクトをDictionaryに展開するため、
メモリを消費します(数百万件レベルなら注意が必要)。
・コード量は Include() 一発よりは増える。まとめO/Rマッパーの機能ですべて解決できれば理想ですが、
現実のシステム開発ではそうもいかない場面が多々あります。
「必要なIDだけ引っこ抜く」→「一括取得」→「Dictionaryでマッピング」この「手動JOIN」
とも言えるパターンを持っておくと、パフォーマンスチューニングの引き出しがぐっと広がります。
O/Rマッパーの機能ですべて解決できれば理想ですが、現実のシステム開発ではそうもいかない場面が多々あります。
「必要なIDだけ引っこ抜く」→「一括取得」→「Dictionaryでマッピング」
この「手動JOIN」とも言えるパターンを持っておくと、
パフォーマンスチューニングの引き出しがぐっと広がります。