C# EF Coreのパフォーマンス改善ガイド!初心者でもわかる高速化のコツ
生徒
「C#でデータベースを扱うとき、Entity Framework Core(EF Core)を使っているのですが、データの数が増えると動作が重くなってきました。速くする方法はありますか?」
先生
「それはパフォーマンス改善が必要なサインですね。EF Coreには、データの読み込み方を工夫するだけで劇的にスピードアップするコツがいくつかあるんですよ。」
生徒
「初心者でもできる簡単な設定や書き方はありますか?」
先生
「もちろんです!まずは基本となる、無駄な動きを減らすテクニックから順番に解説していきますね。」
1. パフォーマンス改善の第一歩!AsNoTrackingを知ろう
C#のプログラムでデータベースから情報を取ってくる際、Entity Framework Core(EF Core)は標準で「追跡」という機能を使います。これは、取ってきたデータが後で変更されるかどうかを見守る機能です。しかし、単に画面に表示するだけで、データを更新しない場合には、この見守り機能は無駄な体力(メモリや計算時間)を使ってしまいます。
そこで登場するのが AsNoTracking という命令です。これを使うと、EF Coreに「このデータは見守らなくていいよ」と伝えることができ、動作が非常に軽くなります。大量のリストを表示するだけの処理などでは、これを入れるだけで速度が大きく変わります。
追跡(トラッキング)とは、料理店でいうところの「お客様が食べ終わって皿を下げるまでずっと横で待機している店員さん」のようなものです。見るだけ(表示するだけ)なら、店員さんは待機しなくていいですよね。その分、他のお仕事ができるようになるというわけです。
using (var context = new MyDbContext())
{
// データを表示するだけなら、AsNoTracking()を付けるのが鉄則です
var products = context.Products
.AsNoTracking()
.ToList();
foreach (var p in products)
{
Console.WriteLine(p.Name);
}
}
2. 必要な項目だけを取り出す!Select句の活用
データベースのテーブルには、たくさんの項目(列)が含まれていることがあります。例えば「会員テーブル」に、名前、住所、電話番号、自己紹介文、アイコン画像など、たくさんのデータが入っているとしましょう。もし、画面に「名前」だけを表示したいのに、全てのデータを取ってきてしまうと、通信の道が渋滞してしまいます。
これを防ぐのが Select 句です。必要な項目だけを指定して取得することで、転送されるデータの量を最小限に抑えることができます。パソコンを触ったことがない方でも、スーパーで「ジャガイモ1個だけ欲しいのに、段ボール1箱分全部レジに持っていく」のは効率が悪いと想像すれば分かりやすいでしょう。必要な分だけをカゴに入れるのがコツです。
using (var context = new MyDbContext())
{
// 名前と価格だけを抜き出して取得します
var productInfo = context.Products
.Select(p => new { p.Name, p.Price })
.ToList();
foreach (var item in productInfo)
{
Console.WriteLine($"{item.Name}は{item.Price}円です");
}
}
3. 関連データの読み込み!Includeの使い方と注意点
データベースでは、複数の表(テーブル)が繋がっていることがあります。例えば「注文データ」の中に「注文したユーザーの情報」が入っている場合です。EF Coreでこれらをまとめて取得するとき、何も考えずにループの中で一人ずつユーザー情報を取得しようとすると、何度もデータベースに問い合わせに行ってしまい、非常に時間がかかります。これを「N+1問題」と呼びます。
これを解決するのが Include です。最初の一回で、関連するデータもまとめてガバッと取ってきてしまう方法です。これを「積極的読み込み(Eager Loading)」と言います。何度も往復するのではなく、大きなトラックで一気に荷物を運ぶようなイメージです。ただし、何でもかんでもIncludeしすぎると、逆にデータが大きくなりすぎて重くなるので注意が必要です。
using (var context = new MyDbContext())
{
// 注文データ(Orders)と一緒に、注文したユーザー(User)もまとめて取得します
var orders = context.Orders
.Include(o => o.User)
.ToList();
foreach (var order in orders)
{
// ここで order.User にアクセスしても、既に関係データが読み込まれているので速いです
Console.WriteLine($"{order.OrderId}: {order.User.UserName}");
}
}
4. ページング処理で一度に取る件数を制限する
10万件のデータがあるとき、それを一度に全部読み込んで画面に表示しようとすると、パソコンがフリーズしてしまいます。インターネットの検索結果でも、1ページ目に全てのサイトが表示されるわけではなく、10件や20件ずつに分かれていますよね。これを「ページング」と呼びます。
EF Coreでは Skip と Take という命令を使って、簡単にページングが実現できます。Skipは「何件飛ばすか」、Takeは「何件取るか」という意味です。例えば「21件目から10件分だけ欲しい」という命令を出すことで、データベースやネットワークにかかる負担を劇的に減らすことができます。
using (var context = new MyDbContext())
{
int pageNumber = 1; // 1ページ目
int pageSize = 10; // 10件ずつ表示
// 0件飛ばして、最初の10件を取得する例
var pagedData = context.Products
.OrderBy(p => p.Id)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToList();
Console.WriteLine($"{pagedData.Count}件のデータを取得しました。");
}
5. データの数だけを確認するCountメソッド
「データが全部で何件あるか知りたいだけ」という場面はよくあります。このとき、データを一度リスト(ToList)にしてから件数を数えてはいけません。それでは、数を知るためだけに全てのデータをメモリに読み込んでしまうからです。
データベースそのものに「何件あるか教えて!」と聞くのが Count メソッドです。これを使えば、データベース側で計算して、結果の「数字」だけを返してくれるので、無駄な通信が発生しません。初心者の方がついついやってしまう「全部持ってきてから数える」という失敗を避けるだけで、プログラムの品質はぐっと上がります。
using (var context = new MyDbContext())
{
// 条件に合うデータの数だけをデータベース側で計算させます
int expensiveCount = context.Products
.Where(p => p.Price > 1000)
.Count();
Console.WriteLine($"1000円以上の商品は{expensiveCount}個あります。");
}
6. データベースへの一括保存!SaveChangesのタイミング
データを保存する際、1件ずつ「保存して!」と命令を出すと、そのたびにデータベースとの通信が発生します。100回保存するなら100回通信することになり、非常に効率が悪いです。これは、郵便局に100通の手紙を出すとき、1通ずつポストまで往復するようなものです。まとめてカバンに入れて、1回で出しに行った方が早いですよね。
EF Coreでは、変更したいデータを全て準備してから、最後に1回だけ SaveChanges を呼ぶのが基本です。こうすることで、EF Coreが内部で効率的な処理を自動的に行ってくれます。また、最近のバージョンでは大量のデータを一括で追加する「Bulk Insert」といった手法もありますが、まずは「最後にまとめて保存する」という基本をマスターしましょう。
7. 適切なインデックスが貼られているか確認しよう
これはプログラムの書き方というよりは、データベース側の設定の話になりますが、パフォーマンスには欠かせない要素です。インデックスとは、本でいうところの「索引(さくいん)」です。何万ページもある本から特定の言葉を探すとき、最初から最後まで1ページずつめくっていたら日が暮れてしまいます。索引があれば、すぐに目的のページに飛べますよね。
データベースでも、よく検索に使う項目(例えば「メールアドレス」や「商品コード」など)には、この索引を付けておく必要があります。EF Coreのモデル定義(設定ファイル)で、どの項目にインデックスを貼るかを指定することができます。これがないと、どれだけプログラムを工夫しても検索速度は上がりません。
8. コンパイル済みクエリで処理を高速化
EF Coreは、私たちが書いたC#のコードをデータベースが理解できる言葉(SQL)に翻訳してくれます。しかし、この「翻訳作業」には少し時間がかかります。何度も同じような検索を行う場合、毎回翻訳するのはもったいないですよね。
そこで、一度翻訳した結果を再利用するのが コンパイル済みクエリ です。あらかじめ翻訳済みのテンプレートを用意しておくことで、実行時の計算量を減らすことができます。これは少し高度なテクニックですが、システム全体のレスポンスを極限まで高めたいときには非常に有効な手段となります。プログラミングに慣れてきたら、ぜひ挑戦してほしいポイントです。