0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#/.NET】N+1問題を「Dictionary」で撲滅する(JOINやIncludeが使えない時の処方箋)

0
Posted at

はじめに
システム開発における永遠の課題、「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」とも言えるパターンを持っておくと、
 パフォーマンスチューニングの引き出しがぐっと広がります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?