根本的なパフォーマンスの違いをもたらすアーキテクチャ
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:
- プリコンパイルクエリ (
CompileQuery) により、LINQ式の解析コストが毎回発生しなくなる。 - SQLを自動的に最適化し、変更されたフィールドのみを更新(LINQ to SQLは頻繁に全フィールドをUPDATE)。
- 分割クエリ、N+1問題の回避、ページネーションの最適化をサポート。
- 複数のデータベースドライバーに対して統一された最適化を提供し、接続プールの再利用がより高度。
- プリコンパイルクエリ (
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万件の挿入を例に:
- LINQ to SQLの唯一の方法:
InsertOnSubmitのループ + 最後に1回のSubmitChanges- 複数の独立したINSERT文を生成し、複数回のネットワーク往復が発生。時間は9~12秒。
- すべてのエンティティスナップショットをメモリに保持するため、GCの負荷が大きい。
- EF Coreの標準的方法:
AddRange+SaveChanges- INSERTを自動的に一括結合し、往復は数十回のみ。時間は約1秒。
- 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への変換、エンティティマッピング、メモリ管理、ネットワーク往復が全く異なり、一括処理シナリオでは性能差が桁違いになる。
技術選定に関するパフォーマンス推奨
- 新規プロジェクト .NET Core/.NET 5以降: 直接EF Coreを採用。パフォーマンス上限がLINQ to SQLを遥かに上回る。
- レガシーな.NET Frameworkの保守システム: LINQ to SQLの置換が不可能な場合、ループによる
SubmitChangesを避け、可能な限り一括コミットを実施。一括データ処理ではネイティブSQLを使用。 - 読み書き分離ビジネス:
- クエリ: EF Core +
AsNoTracking。 - 一括更新/削除: 優先的に
ExecuteUpdate/ExecuteDeleteを使用。
- クエリ: EF Core +
- 数千万件のデータインポート: 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"));