0Pricing
C# Academy · レッスン

コンパイル済みクエリとパフォーマンス

EF.CompileQuery、AsNoTracking、Selectによるプロジェクションを使って、クエリのパフォーマンスを高めます。

「コンパイル済みクエリとパフォーマンス」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC# Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C# Academyコースには全4レッスンが含まれています。

EF Coreのクエリコンパイル

EF CoreがLINQクエリを実行するたびに、式ツリーを解析し、SQLに変換して、検証する必要があります。このオーバーヘッドは、頻繁に実行される処理では積み重なります。EF Coreには、これを最小限に抑えるためのツールがいくつか用意されています。

読み取り専用クエリでのAsNoTracking

AsNoTracking()は変更追跡への登録をスキップします。読み取り専用クエリ(APIレスポンスやレポート)では、最も効果の大きい手軽な改善策です。通常は2~5倍高速になり、メモリ使用量も少なくなります。

// With tracking (default) — for entities you'll modify
var tracked = await _db.Products.FirstOrDefaultAsync(p => p.Id == 1);

// Without tracking — for read-only
var dto = await _db.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .Select(p => new ProductDto { Id = p.Id, Name = p.Name })
    .ToListAsync();

コンパイル済みクエリ

EF.CompileAsyncQueryはLINQクエリをデリゲートに事前コンパイルします。変換は起動時に一度だけ行われ、その後の呼び出しではコンパイル処理が完全にスキップされます。

// Define once, reuse many times
private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =
    EF.CompileAsyncQuery(
        (AppDbContext db, int id) =>
            db.Products.AsNoTracking().FirstOrDefault(p => p.Id == id));

// Usage
var product = await GetProductById(_db, 42);

パフォーマンスのためのSelectによるプロジェクション

必要なフィールドだけを正確にプロジェクションしてください。完全なエンティティではなくDTOを選択すると、取得する列、ネットワーク転送量、GCの負荷を削減できます。

// Load entire entity (over-fetch)
var all = await _db.Products.ToListAsync();

// Projection: only fetch needed columns
var names = await _db.Products
    .AsNoTracking()
    .Select(p => new { p.Id, p.Name, p.Price })
    .ToListAsync();
// SQL: SELECT Id, Name, Price FROM Products

インデックスヒントとRaw Queryの最適化

EF CoreはHasIndex()で構成したデータベースインデックスを利用します。複合インデックス、フィルター付きインデックス、インクルード列はすべてクエリプランの性能に影響します。

// Configure compound index in OnModelCreating
modelBuilder.Entity<Order>()
    .HasIndex(o => new { o.CustomerId, o.Status })
    .HasFilter("[Status] = 0")  // filtered index
    .IncludeProperties(o => o.PlacedAt);

// The query below will use this index efficiently
var pending = await _db.Orders
    .AsNoTracking()
    .Where(o => o.CustomerId == 5 && o.Status == OrderStatus.Pending)
    .ToListAsync();

SkipとTakeによるページング

大量の結果セットは必ずページングしてください。SkipとTakeはSQLのOFFSET/FETCHに変換されるため、数千行をメモリに読み込まずに済みます。

int page = 2, pageSize = 25;

var products = await _db.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .OrderBy(p => p.Name)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();
// SQL: ... ORDER BY Name OFFSET 25 ROWS FETCH NEXT 25 ROWS ONLY

効率的なカウント

件数を数えるだけなら、レコードを読み込む代わりにCountAsync()またはAnyAsync()を使用してください。存在するかどうかを確認するだけの場合、AnyAsync()はCOUNTよりもさらに高速です。

// Total count for pagination
int totalCount = await _db.Products
    .Where(p => p.IsActive)
    .CountAsync();

// Existence check — faster than Count > 0
bool hasExpensive = await _db.Products
    .AnyAsync(p => p.Price > 1000m);
// SQL: IF EXISTS (SELECT 1 FROM ...) SELECT 1

AddRangeによる一括挿入

AddRangeを使うと、複数のエンティティを一度に追加できます。EF CoreはINSERT文をより少ないラウンドトリップにまとめます。バッチサイズはプロバイダーのMaxBatchSizeで制御されます。

var newProducts = Enumerable.Range(1, 1000)
    .Select(i => new Product { Name = $"Product {i}", Price = i * 1.5m })
    .ToList();

_db.Products.AddRange(newProducts);
await _db.SaveChangesAsync();
// Batched: typically 100-1000 rows per round-trip

データベース関数向けのEF.Functions

EF.Functionsはデータベース固有の関数(LIKE、日付部分、全文検索)を提供します。これらはメモリ上で評価されるのではなく、EF CoreによってSQLに変換されます。

// LIKE pattern search (SQL-side)
var results = await _db.Products
    .Where(p => EF.Functions.Like(p.Name, "%keyboard%"))
    .AsNoTracking()
    .ToListAsync();

// Date difference (SQL-side)
var recentOrders = await _db.Orders
    .Where(o => EF.Functions.DateDiffDay(o.PlacedAt, DateTime.UtcNow) <= 7)
    .ToListAsync();

遅いクエリの診断

EF Coreのクエリロギングを有効にして、生成されたSQLを確認してください。開発環境でEnableSensitiveDataLogging()を使用すると、パラメーター値も確認できます。

builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseSqlServer(connStr)
       .LogTo(Console.WriteLine, LogLevel.Information)
       .EnableSensitiveDataLogging() // dev only!
       .EnableDetailedErrors());     // dev only!

実践例:最適化された一覧エンドポイント

本番環境に対応した一覧エンドポイントでは、最大限のパフォーマンスを得るために、AsNoTracking、プロジェクション、インデックス、ページングを組み合わせます。

app.MapGet("/products", async (
    AppDbContext db,
    string? search,
    int page = 1, int size = 20) =>
{
    var query = db.Products
        .AsNoTracking()
        .Where(p => p.IsActive);

    if (!string.IsNullOrEmpty(search))
        query = query.Where(p => EF.Functions.Like(p.Name, $"%{search}%"));

    var total = await query.CountAsync();
    var items = await query
        .OrderBy(p => p.Name)
        .Skip((page - 1) * size).Take(size)
        .Select(p => new { p.Id, p.Name, p.Price })
        .ToListAsync();

    return Results.Ok(new { total, items });
});

確認問題

通常のLINQクエリと比べて、EF.CompileAsyncQueryの主な利点は何ですか?

まとめ:コンパイル済みクエリとパフォーマンス

要点:

  • AsNoTracking():読み取り専用では変更追跡をスキップする — 最も手軽で高速な改善策
  • EF.CompileAsyncQuery:頻繁に実行するクエリをデリゲートに事前コンパイルする
  • Selectによるプロジェクション:必要な列だけを取得する
  • 適切なインデックス(HasIndex)でクエリプランを大幅に改善する
  • Skip/Takeでページングし、存在確認にはAnyAsyncを使用する
  • 開発環境でクエリロギングを有効にして、遅いSQLや予期しないSQLを見つける

よくある質問

「コンパイル済みクエリとパフォーマンス」レッスンは無料ですか?

はい。「コンパイル済みクエリとパフォーマンス」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。

「コンパイル済みクエリとパフォーマンス」で何を学びますか?

EF.CompileQuery、AsNoTracking、Selectによるプロジェクションを使って、クエリのパフォーマンスを高めます。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

C# Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「コンパイル済みクエリとパフォーマンス」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このC# Academyレッスンでコードを書いて実行できますか?

はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Eager、Lazy、Explicit Loading
  2. Raw SQL、ストアドプロシージャと補間
  3. コンパイル済みクエリとパフォーマンス
  4. グローバルクエリフィルターと所有エンティティ
← C# Academyに戻る