EF Core と LINQ to SQL の包括的なパフォーマンス比較(シナリオ別と内部構造)

根本的なパフォーマンスの違いをもたらすアーキテクチャ

1. コンテキストと変更追跡

LINQ to SQL (DataContext)

  • 軽量でシンプルなスナップショット、単一テーブルへの1対1マッピングのみサポート。
  • 追跡の選択的な無効化はできず、すべてのクエリでエンティティの追跡が強制されます。
  • バッチ変更検出の最適化がなく、ループ処理によるコミットでパフォーマンスが急激に低下。
  • 非同期I/Oをサポートせず、すべてのデータベース操作はスレッドをブロック。

EF Core (DbContext)

  • 変更追跡のオン/オフが可能: AsNoTracking() を使用すると、読み取り専用シナリオでスナップショットのオーバーヘッドを直接除去し、メモリ使用量と速度が30%~500%向上。
  • 変更検出アルゴリズムが最適化され、頻繁なスナップショット比較が削減。
  • 完全な非同期API (ToListAsync/ExecuteUpdateAsync) をサポートし、高並行性でのスレッド利用率が同期ブロッキングを大きく上回る。
  • サーバーサイド一括操作 (ExecuteUpdate/ExecuteDelete) をサポート。エンティティをメモリにロードすることなく直接SQLを実行できるため、数百万件のデータで巨大な差が生まれる。

2. SQL生成とクエリキャッシュ

  • LINQ to SQL: クエリキャッシュは簡素で、複雑な結合では冗長なSQLが生成され、SQL Serverのみに適合。
  • EF Core:
    1. プリコンパイルクエリ (CompileQuery) により、LINQ式の解析コストが毎回発生しなくなる。
    2. SQLを自動的に最適化し、変更されたフィールドのみを更新(LINQ to SQLは頻繁に全フィールドをUPDATE)。
    3. 分割クエリ、N+1問題の回避、ページネーションの最適化をサポート。
    4. 複数のデータベースドライバーに対して統一された最適化を提供し、接続プールの再利用がより高度。

3. 一括コミットメカニズム

  • LINQ to SQL: 1000件のレコードをループで追加すると、1000個の独立したINSERT文が生成され、毎回 SubmitChanges でデータベース往復が発生し、ネットワークI/Oが爆発的に増加。一括結合機能はない。
  • EF Core: AddRange は複数のINSERTを自動的に単一の複数値INSERTに結合。デフォルトのバッチサイズは42で、往復回数を大幅に削減。EF 7以降では、ExecuteUpdate/ExecuteDelete をネイティブでサポートし、エンティティをロードすることなく単一のSQL文でデータの一括更新が可能で、速度が10倍以上向上。

シナリオ別詳細パフォーマンス比較

シナリオ1: 読み取り専用クエリ(最も一般的)

1) 単純な単一テーブル、少量のデータ (<100件)

  • LINQ to SQL: わずかに高速、コンテキストがより軽量。
  • EF Core: AsNoTracking を有効にすると逆転。差は大きくなく、日常的には認識されない。

2) 複数テーブルの結合、大規模データセット、レポートクエリ (>1000件)

  • LINQ to SQL: エンティティの完全追跡が強制され、メモリ使用量が急増。ストリーミング読み取りはサポートされず、すべての行を一度にメモリにロード。
  • EF Coreの利点:
    • .AsNoTracking() でスナップショットを無効化し、メモリ使用量を半分に削減、クエリ速度を数倍向上。
    • AsAsyncEnumerable() で行ごとのストリーミング読み取りが可能になり、テーブル全体をメモリにロードしないため、レポートでメモリ不足にならない。
    • 結合やサブクエリの生成が最適化され、より簡潔なSQLを生成し、データベースの計算負荷を軽減。
      実測での差: 1万行のレポートで、EF Coreの追跡無効モードはLINQ to SQLの1/3の時間しかかからない。

3) 射影クエリ Selectによる部分フィールド取得

両者とも指定された列のみをマッピングし、基本的な速度は同等。ただし、EF Coreはキャッシュが優れており、繰り返し実行時により高速。

シナリオ2: 単一レコードの追加/変更/削除(単一操作)

両者のパフォーマンスはほぼ同等:

  • LINQ to SQL: InsertOnSubmit の内部ロジックはシンプル。
  • EF Core: Add() は状態管理のレイヤーが多いが、現代の.NETのJIT最適化により差は解消。
    体感できる違いはなく、ボトルネックはORMではなくデータベース自体にある。

シナリオ3: 一括書き込み(核心的な分岐点)

1万件の挿入を例に:

  1. LINQ to SQLの唯一の方法: InsertOnSubmit のループ + 最後に1回の SubmitChanges
    • 複数の独立したINSERT文を生成し、複数回のネットワーク往復が発生。時間は9~12秒。
    • すべてのエンティティスナップショットをメモリに保持するため、GCの負荷が大きい。
  2. EF Coreの標準的方法: AddRange + SaveChanges
    • INSERTを自動的に一括結合し、往復は数十回のみ。時間は約1秒。
  3. EF Coreのサードパーティ一括処理ライブラリ (例: EFCore.BulkExtensions)
    • ネイティブの SqlBulkCopy を使用し、1万件で数百ミリ秒。

一括更新/削除での差はさらに大きい:

  • LINQ to SQL: まずすべてのエンティティをクエリしてメモリにロードし、1件ずつ変更をコミット。1万件で数十秒。
  • EF Core 7以降の ExecuteUpdate: 単一のSQL文によるサーバーサイド一括更新で、エンティティのロードが不要。1万件で数百ミリ秒、メモリ使用量はほとんど増加しない。

シナリオ4: 高並行Web/APIシナリオ

  • LINQ to SQL: 完全に同期ブロッキング。高並行性ではスレッドプールが枯渇し、スループットが低い。DataContextを再利用できず、頻繁な作成と破棄のオーバーヘッドが大きい。
  • EF Core: 完全な非同期操作を提供。I/O完了ポートを使用するためワーカースレッドを占有せず、同じサーバーで2~5倍の並行性を維持可能。DIスコープによるDbContextの自動再利用で接続を共有。

シナリオ5: メモリ使用量の比較

  • LINQ to SQL: すべてのクエリで追跡が強制され、大規模データリストではメモリ使用量が高い。
  • EF Core: 読み取り専用シナリオで AsNoTracking を使用すると、元の値のスナップショットを保存しないため、メモリ使用量が40%~70%減少し、GCがより安定。

主要なパフォーマンス特性比較表

特性 LINQ to SQL EF Core パフォーマンスへの影響
変更追跡の無効化 サポートなし AsNoTracking 読み取り専用シナリオでEFが30%~500%高速
ネイティブ非同期API なし、完全ブロッキング 完全なAsyncセット 並行スループットでEFが大幅にリード
一括結合Insert 1件ずつのSQL AddRangeによる自動バッチ処理 一括書き込みでEFが5~10倍高速
サーバーサイド一括更新/削除 なし、エンティティのロード必須 ExecuteUpdate/Delete 膨大なデータで数十倍の差
変更されたフィールドのみ更新 劣る、全フィールドUpdateが頻発 変更された列のみ正確に更新 ネットワークとデータベースI/Oを削減
大規模データのストリーミング読み取り 一度にメモリにロード AsAsyncEnumerable レポートでのメモリオーバーフローを回避
クエリのプリコンパイル最適化 キャッシュが弱い CompileQuery 高頻度の繰り返しクエリでより高速
複数データベースドライバーの最適化 SQL Serverのレガシードライバーのみ 現代的で高性能なドライバー データベース層のI/O効率がより高い
接続プールの自動管理 シンプル 洗練されたスコープ再利用 接続の作成と破棄のオーバーヘッドを削減

パフォーマンスに関するよくある誤解

誤解1: LINQ to SQLは軽量なので必ず高速

単一小規模操作でのみわずかにリード。一括処理、大規模データ、並行処理、読み取り専用クエリが関わると、EF Coreの最適化手法が圧倒的に優れる。LINQ to SQLは十数年前の技術であり、現代的パフォーマンス最適化策は一切ない。

誤解2: EF Coreの変更追跡は必ず速度を低下させる

読み取り専用シナリオでは .AsNoTracking() を使用することで追跡のオーバーヘッドを完全に除去でき、その場合EF CoreはLINQ to SQLよりも高速。データ更新が必要な場合のみ追跡を保持する。

誤解3: 両者のLINQクエリロジックが同じならパフォーマンスも同じ

LINQ式の文法は同じだが、内部的なSQLへの変換、エンティティマッピング、メモリ管理、ネットワーク往復が全く異なり、一括処理シナリオでは性能差が桁違いになる。

技術選定に関するパフォーマンス推奨

  1. 新規プロジェクト .NET Core/.NET 5以降: 直接EF Coreを採用。パフォーマンス上限がLINQ to SQLを遥かに上回る。
  2. レガシーな.NET Frameworkの保守システム: LINQ to SQLの置換が不可能な場合、ループによる SubmitChanges を避け、可能な限り一括コミットを実施。一括データ処理ではネイティブSQLを使用。
  3. 読み書き分離ビジネス:
    • クエリ: EF Core + AsNoTracking。
    • 一括更新/削除: 優先的に ExecuteUpdate/ExecuteDelete を使用。
  4. 数千万件のデータインポート: EF Coreに一括処理拡張機能を組み合わせ。LINQ to SQLには効率的なソリューションがなく、手動での SqlBulkCopy 記述が必要。

補足: 最小限のコード比較(一括更新のパフォーマンス差を直感的に示す)

LINQ to SQL(非効率、全量取得必須)

var context = new SalesDataContext();
var customers = context.Customers.Where(c => c.Status == "Inactive").ToList();
foreach (var customer in customers)
{
    customer.Status = "Active";
}
context.SubmitChanges(); // N件のUPDATE文を生成

EF Core(高性能、サーバーサイド実行、エンティティロード不要)

using var context = new ApplicationDbContext();
// 単一SQL文による一括更新、エンティティは一切ロードしない
await context.Customers
    .Where(c => c.Status == "Inactive")
    .ExecuteUpdateAsync(s => s.SetProperty(p => p.Status, "Active"));

タグ: Entity Framework Core LINQ to SQL ORM .NET パフォーマンス比較

9月21日 04:20 投稿