optimizing-ef-core-queries について
ソースコード、SQLログ、クエリプランから、EF Coreの遅いまたは不正確なデータアクセスを調べるレビュー用スキルです。クエリの挙動を保ちながら、根拠に基づく修正を優先し、実行時検証が必要な点も示します。
主な機能
- 01EF Coreクエリの実行箇所を追跡
- 02往復回数と結果範囲を確認
- 03インデックスに不向きな述語を特定
- 04追跡設定とクエリ形状を確認
- 05根拠と推測を区別
活用シーン
- 01重複クエリやN+1の調査
- 02遅いページングや一括更新のレビュー
- 03クエリプランとSQLログの評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill optimizing-ef-core-queriesインストール後、Claude Code で/optimizing-ef-core-queriesを入力して読み込まれたか確認します。
詳細
EF Core クエリの最適化
EF Core を使ったアプリケーションのデータアクセスが遅い、または正しくない場合に、原因を特定して修正します。ソースコードのみをレビューする場合は、実行せずにコードを追跡し、コードから確認できる動作と実行時の推測を区別します。SQL、ログ、実行プランがある場合は、それらを使ってボトルネックを確認します。マイクロ最適化よりも、ラウンドトリップ、重複行、スキャン、呼び出しごとの変換コストを削減する変更を優先します。
使用する場面
- EF Core のクエリが遅い、または想定よりはるかに多くの SQL ステートメントを発行する
- 同じクエリが行ごとに繰り返される(N+1/遅延読み込み)
- 複数のコレクション
Includeによって、結果が膨れ上がったり重複行が発生したりする Skipの値が増えるにつれて深いページの取得が遅くなる、または一括更新で変更だけのために行を読み込んでいる- 列にインデックスがあるにもかかわらず、フィルターやソートを使うクエリでスキャンが発生する。または、フィルターやソートに使う列に対応するインデックスがない
- 頻繁に実行されるホットクエリで、呼び出しのたびに EF Core の LINQ 変換コストが発生している
使用しない場面
- コードが EF Core ではなく、Dapper または生の ADO.NET を使用している場合。 SQL/インデックス/クエリプランに関する質問に直接回答し、
DbContextを導入したり、AsNoTracking、Include、AsSplitQueryなどの EF Core API を推奨したりしないでください。
ソースコードのレビュー手順
- EF の境界を見つけます。
DbContext、DbSet、リポジトリの実装、ユニットオブワーク、クエリヘルパー、モデル構築コードなどが該当します。インターフェイスのメソッドの動作を推測せず、実装を追跡します。 - 各クエリを実行または列挙される箇所まで追跡します。ヘルパー内も含め、演算子が
QueryableとEnumerableのどちらに結び付くかを確認します。IEnumerableでも、遅延評価されるデータベースシーケンスの場合があります。 - 呼び出し経路に沿って、即時書き込みや列挙の繰り返しを含むデータベース操作の回数と、実際のトランザクション境界を数えます。また、どの操作が同じ
DbContextインスタンスを共有しているかも追跡します。 - 実行経路ごとに、述語、式の構築、プロジェクションの内容、結果件数の上限、並び順、Include、追跡、SQL パラメーター化、モデル構築コードを個別に確認します。
- コントラクトを維持します。結果の対象と順序、任意のページへのアクセス、生成キー、再試行、トランザクションと部分的な失敗の動作、並行処理への対応、ドメイン/インターセプターの副作用が対象です。
- 優先度順に、該当箇所、影響を受ける呼び出し経路、具体的な影響、安全な対策を示して指摘事項を報告します。問題のない対照例も簡潔に示し、検証できなかった点を明記します。ソースコードからの推論を、測定済みの実行プランや速度向上と同一視しないでください。
ソースコードのみの依頼では、次のステップとして実行時の検証を推奨します。
生の SQL には、値がパラメーターとして送信されるよう、EF Core の補間付きオーバーロード(FromSql/FromSqlInterpolated および ExecuteSql[Async])を優先してください。
ソースコードのみの指摘は証拠に基づいて行う
ユーザーがソースコードのみのレビューを求めた場合は、実証された不具合と、ワークロードやデプロイ環境の証拠がまだ必要な最適化を区別します。
- 変更を加えるループの後に
SaveChanges[Async]を一度だけ呼び出すのは、ループ内でSaveChangesを呼び出すこととは異なります。行ごとのラウンドトリップの不具合と呼んではいけません。一括 API は、操作の規模が明らかに大きく、追跡のセマンティクスを回避しても安全な場合にのみ提案します。 - 提示されたファイルに
HasIndexの呼び出しがないからといって、デプロイ済みデータベースにインデックスがないことの証明にはなりません。根拠として、関連するモデル構築コード、マイグレーション、またはクエリプランを使用します。その根拠がない場合、一般論として条件付きのインデックス推奨を加え、ソースコードレビューを水増ししないでください。 AsNoTrackingがないことを指摘できるのは、読み取り専用のエンティティ結果が確認できる場合に限られます。スカラーのみのプロジェクションでは追跡対象はありませんが、DTO/レコード内のエンティティはデフォルトで追跡されます。- レビュー対象のパターンがすべて適切であれば、指摘事項を無理に作らず、対応が必要な変更はないと伝えてください。
SQL とクエリ数を記録する
まず、提供されたログと実行計画を確認します。実行可能な調査を行う場合は、コマンドログを有効にして、変更前後のクエリの形と数を検証します。
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
// or set "Microsoft.EntityFrameworkCore.Database.Command": "Information" in appsettings.json
クエリに .TagWith("...") を付けると、ログ内で見つけやすくなります。遅い操作が実行するステートメント数と、各ステートメントが返す行数を、変更の前後で数えます。
修正
実体化と結果件数の上限を追跡する
ヘルパー内での実体化も、実行境界です。リストや配列にはすでに行が読み込まれており、その後の集計はメモリ上で実行されます。一方、IEnumerable<T> は遅延実行される EF クエリを隠していることがあります。その場合、Enumerable の演算子は行をローカルで処理し、列挙するたびにデータベースクエリが再実行されることがあります。メソッド名だけでなく、実装と静的型を追跡してください。実行するまで、変換可能なフィルター、射影、集計は IQueryable<T> 上に維持します。
// Loads rows, then filters in memory.
var candidates = await repository.LoadCandidatesAsync(cancellationToken);
return candidates.Where(o => o.CreatedAt < cutoff).ToList();
// Composes and filters in SQL.
return await repository.QueryCandidates()
.Where(o => o.CreatedAt < cutoff)
.ToListAsync(cancellationToken);
真偽値による存在確認には AnyAsync を使います。ただし、数値の合計が必要な場合は CountAsync を維持してください。シーケンス全体に対する集計を繰り返し計算する場合は、上限のないリストをキャッシュしたり、1 つのコンテキストでクエリを並列実行したりするのではなく、サーバー側の集計にまとめます。
これとは別に、結果の件数も確認してください。対話的な結果にはページネーションまたは件数制限を適用します。完全なエクスポートでは、仕様上の内容を切り捨てずに、ストリーミングやチャンク処理でバッファリングを減らします。
インデックスに対応する述語を維持する
列に対する関数や算術演算、または前方一致しない部分一致検索は、通常の列インデックスでのシークを妨げることがよくあります。列をそのまま使う比較を優先します。多くの場合、半開区間が適しています。LINQ だけを見て全表スキャンだと断定しないでください。プロバイダーによる変換、計算列/式インデックス、その他の述語によって実行計画が変わることがあります。
// Computes a year expression instead of comparing the indexed date.
db.Logs.Where(l => l.CreatedAt.Year == year);
// Range compatible with an ordinary date index.
var start = new DateTime(year, 1, 1);
db.Logs.Where(l => l.CreatedAt >= start && l.CreatedAt < start.AddYears(1));
同じルールは、次のような一般的なパターンにも当てはまります。
- 大文字と小文字を区別しないテキスト検索 — 列を関数で囲むのではなく、既存のインデックス付き正規化値/計算値、または列と互換性のある照合順序を使用します。クエリ固有の照合順序を変更すると、照合順序が異なるインデックスも使えなくなることがあります。正規化と比較のセマンティクスを維持してください。
StringComparisonオーバーロードは、一般にリレーショナルプロバイダーでは変換されません。現在の EF Core は、変換できない述語を暗黙にローカルでフィルターするのではなく、例外をスローします。 - 計算式 —
column * k > xではなく、事前計算した定数と比較します。 - 列を別の型に変換する場合 —
column.ToString()に対する述語(たとえば数値や日付の「テキスト形式」を検索するtotal.ToString().StartsWith(p))は、すべての行に関数を適用するため、変換できないことがあります。現在の EF Core は、変換できない述語をクライアント側で暗黙に評価するのではなく、変換エラーを報告します。代わりに、型付きの列を実際の比較または範囲でフィルターします。 - 部分一致検索 —
name.Contains(term)は、通常の B ツリーインデックスでは一般にシークできません。前方一致なら可能な場合があります。前方一致もトークンベースの全文検索も、任意の部分一致検索の完全な代替にはなりません。必要な検索モードを維持するか、変更内容を明示してください。プロバイダーに適した部分一致用のインデックス/検索方式を検討します。
検証: 実行時の検証が求められている場合は、生成された述語と実行計画を確認します。通常のインデックスを追加しても、互換性のない式の問題がそれだけで解消するわけではありません。
頻繁に実行される高負荷のクエリをコンパイルする
EF Core は、一致するクエリ形状の変換をすでにキャッシュしています。それでも、特に頻繁に実行される単純なクエリでは、式の処理やキャッシュ検索に時間がかかることがあります。EF.CompileQuery / EF.CompileAsyncQuery は、その検索を省略するデリゲートを再利用します。データベース I/O を高速化するものではありません。
private static readonly Func<AppDbContext, int, ProductListItem> GetProduct =
EF.CompileQuery((AppDbContext db, int id) =>
db.Products.Where(p => p.Id == id)
.Select(p => new ProductListItem(p.Id, p.Name, p.Price))
.First());
public ProductListItem Lookup(AppDbContext db, int id) => GetProduct(db, id);
デリゲートは static(一度だけコンパイル)で、DbContext と各パラメーターを引数として受け取ります。クエリ形状が同じクエリを非常に高頻度で実行するエンドポイントやループで使用します。一度きりのクエリには効果がありません。
検証: 結果が同一であることを確認したうえで、高頻度ループの平均時間が短縮されることを確認します。
SQL パラメーター化とデータベースの実行プラン再利用を制御する
EF のクエリ変換キャッシュと、データベースの実行プランキャッシュを区別してください。キャプチャされた LINQ の値は通常 SQL パラメーターになり、固定リテラルは定数のままです。パラメーターを使うと、値が異なっても SQL テキストが安定し、通常はデータベースのプラン再利用が促進されます。一方、リテラルを使うと特定の値がオプティマイザーに伝わるため、選択性の推定が改善することがありますが、異なるリテラル値が多いとサーバーのプランキャッシュが断片化する可能性があります。ワークロードの根拠がない限り、デフォルト設定を維持してください。
| オーバーライド | SQL への影響 | パフォーマンス上の判断 |
|---|---|---|
EF.Parameter(value)(EF Core 9+) | リテラルを含め、強制的にパラメーター化します | 値が異なっても SQL を再利用することが重要な場合に使用します。すでにキャプチャされた引数には通常、オーバーライドは不要です |
EF.Constant(value)(EF Core 8.0.2+) | キャプチャされた値を含め、強制的にインライン定数化します | SQL のバリエーションが増えることを上回る、値固有のプラン上のメリットが実証されている場合に限って使用します |
db.Orders.Where(order => order.Status == EF.Parameter("Pending"));
db.Orders.Where(order => order.CustomerId == EF.Constant(customerId));
最初のクエリでは固定のステータスがパラメーター化され、2 番目のクエリでは顧客 ID ごとに SQL が特化されます。どちらも本質的に高速とは限りません。すべての構造上のリテラルをパラメーター化したり、根拠なくカーディナリティの高いリクエスト値をインライン化したりしないでください。プロバイダーの動作、パラメーター依存のプラン、データの偏り、コレクションの変換によって、トレードオフは変わります。
EF Core 9 では、これらのオーバーライドはクエリ形状のキャッシュより後に処理されます。EF.Constant は EF の変換を再利用しながら、異なる SQL リテラルを生成できます。これは、動的ツリーで Expression.Constant を使って各リクエスト値を埋め込むこととは同じではありません。コンパイル済みデリゲートを使っても、サーバーのプランキャッシュの断片化は解消されません。また、EF Core 9 では、これらのオーバーライドは EF.CompileQuery / EF.CompileAsyncQuery 内ではサポートされません。
確認: 実行できる場合は、生成された SQL、サーバーのコンパイルとプラン再利用、推定値、代表的なレイテンシを比較してください。ソースのレビューでは、オーバーライドと想定されるトレードオフを特定できますが、高速なプランであることを証明することはできません。
動的に構築するクエリ形状を安定させる
リクエスト値が式ビルダー内でどのように使われるかを確認してください。比較式の中で Expression.Constant(requestValue) を使うと、値ごとに新しい EF キャッシュエントリと異なる SQL が生成されることがあります。変化する値は、適切に型付けされたキャプチャ値の式で表してください。
Expression<Func<string>> valueAccess = () => filterValue;
var comparison = Expression.Equal(propertyExpression, valueAccess.Body);
エンティティの ParameterExpression を作成しても、比較対象の値がパラメーター化されるわけではありません。通常、キャプチャされた LINQ 引数はすでにパラメーター化できます。固定された構造上の定数は、自動的に問題となるわけではありません。null や変換のセマンティクスを維持し、安全な場合に限って、同等のフィルター順序を正規化してください。ビルダーを static にすること、値ごとにデリゲートをキャッシュすること、または新しい動的式を毎回コンパイルすることでは、形状は安定しません。無制限のリークが実証されたと報告するのではなく、コンパイルやキャッシュの負荷が増える可能性として報告してください。
実際の結果グラフに合わせて追跡方法を選択する
| 結果と利用者 | 判断 |
|---|---|
| スカラー値のみのプロジェクション | 削除対象となるエンティティ追跡はありません。指摘事項として AsNoTracking を追加しないでください |
| 後で編集するエンティティ、またはエンティティを含む DTO | 追跡とその ID セマンティクスを維持します |
| 読み取り専用のエンティティグラフ | 追跡なしを検討します。完全なエンティティが不要な場合は、まずプロジェクションの範囲を絞ります |
| 共有エンティティが多い、または参照同一性を利用するコードがある読み取り専用グラフ | AsNoTrackingWithIdentityResolution、またはキーに基づく DTO 処理を検討します |
通常の AsNoTracking では、同じキーを持つ結果が 1 つのオブジェクトに解決されることはありません。ID 解決モードでは、結果をコンテキストにアタッチせずにクエリ内限定のマップを使用します。その分、管理処理のコストがかかり、結合された SQL のペイロードが減ったり、別々のクエリ間で重複排除されたりするわけではありません。
フィルター付き Include では、同じ追跡コンテキストで以前または後に読み込まれたデータを確認してください。ナビゲーションの修正により、Include の述語に一致しない、すでに追跡中の子要素が追加される場合があります。また、フィルター付きナビゲーションは読み込み済みとしてマークされます。対象のサブセットが重要な場合は、独自の子要素フィルターを適用したデタッチ済みプロジェクション、適切な追跡なし読み取り、または新しい読み取り用コンテキストを使用してください。読み取りを整理するためだけに、保留中の作業がある書き込み用トラッカーをクリアしないでください。
N+1 を解消し、遅延読み込みを避ける
同じ SELECT が行ごとに繰り返される(ループ内でナビゲーションにアクセスする)場合、N+1 です。関連データは 1 回のラウンドトリップで読み込みます。Select で集計結果をプロジェクションするか、Include で先行読み込みします。
var summaries = await db.Orders
.Select(o => new OrderSummary(o.Id, o.Items.Count, o.Items.Sum(i => i.Price)))
.ToListAsync();
遅延読み込みよりもプロジェクションまたは Include を優先してください。遅延読み込みは N+1 の主な原因の 1 つであり、同期 I/O を強制します。サーバーアプリでは、遅延読み込みのために Microsoft.EntityFrameworkCore.Proxies を有効にしたり、ナビゲーションを virtual としてマークしたりしないでください。
確認事項: 行数にかかわらず、クエリ数が少数かつ一定であること。
結合による行の増加と、幅の広い行の重複を区別する
兄弟コレクションの Include は互いの行数を増加させることがあります。ネストされたコレクションの連鎖はこの直積とは異なり、参照ナビゲーションは追加コレクションとして数えません。コレクションが 1 つだけでも、親の幅の広い Blob/テキスト列が何度も繰り返される場合があります。利用側でその列やグラフが不要なら、範囲を絞ったプロジェクションを優先してください。それ以外の場合は、AsSplitQuery または対象を絞った読み込みを検討します。
db.Blogs.Include(b => b.Posts).Include(b => b.Contributors).AsSplitQuery();
分割するとステートメントとラウンドトリップが増え、結果がバッファリングされる場合があります。意図的な AsSingleQuery の指定と一貫性の要件は維持してください。複数ステートメントによる読み取りで一貫性を保つには、適切なスナップショット/シリアライズ可能トランザクションが必要な場合があります。ページング結果には一意の順序が必要です(特に EF Core 10 より前の分割ページング)。順序付けによってエンティティの同一性が確立されるわけではありません。Include の数だけを根拠に分割を推奨しないでください。
確認事項: 必要な一貫性を保ちつつ、ペイロードと行の重複が減ることで、追加のラウンドトリップを上回る効果があること。
フィルターとページネーション:オフセットよりキーセット方式を優先する
大きな結果セットは Where で絞り込み、Skip/Take ではなくキーセット(シーク)ページネーションを使用してください。Skip/Take は深いページでは、スキップする行をスキャンして破棄します。
db.Orders.Where(o => o.Id > lastSeenId).OrderBy(o => o.Id).Take(pageSize);
適切なインデックスでカバーされる、一意で安定したキーで並べ替えてください。降順の (CreatedAt, Id) カーソルでは、両方のフィールドに独立した境界条件を指定するのではなく、CreatedAt < lastDate || (CreatedAt == lastDate && Id < lastId) が必要です。並べ替えに使うすべてのキーと、その方向を含めてください。ID が業務上のタイムスタンプ順である必要はありません。
クエリーヘルパーや実行境界を通じて、並べ替えの順序を追跡してください。
IQueryableに対する 2 つ目のOrderBy/OrderByDescendingは、先行する主な並べ替え順序を置き換えます。副次キーにはThenByを使用してください。OrderBy(...).Skip(...).Take(...).AsEnumerable().OrderBy(...)は、まずデータベースで安定した範囲を選び、その後、メモリ内でその範囲に限定された結果だけを並べ替えます。これは意図的な場合があり、並べ替えのないページではありません。- 安定した一意の順序を指定しない
Skip/Takeは非決定的です。深いオフセットページングは別個のスケーリング上の問題であり、ページ番号を指定したランダムアクセスでは、キーセットページネーションは動作を維持できません。 - 完全に順序付けられたオフセットページであっても、同時に発生する挿入や削除によって内容が変わることがあります。並べ替えを追加すれば非決定性は解消されますが、リクエストをまたいだスナップショットの一貫性は保証されません。
検証: 先頭付近のページから深いページまで、ページのレイテンシがほぼ一定に保たれることを確認します。
不足しているインデックスを追加する
SQL へのフィルター移動とは分けて、ワークロード固有のインデックスカバレッジを監査してください。適用済みのモデル設定と規約を確認します。各依存側 FK のプロパティの順序は HasForeignKey から特定してください。通常、EF は、その順序のプレフィックスと必要な一意性を既存のインデックス/キーがカバーしている場合、または規約が削除されている場合を除き、そのタプルにインデックスを追加します。HasIndex の呼び出しだけを数えたり、主側のキーを依存テーブルのカバレッジと混同したりしないでください。ステータス、タイムスタンプなどの非キー列には、明示的なカバレッジが必要な場合があります。レビューごとに汎用的なインデックスをすべて推奨するのではなく、根拠のある不足を頻繁に実行されるクエリーについて推奨してください。
EF Core がスキーマを管理している場合は、モデルにインデックスを追加してマイグレーションを作成します。
modelBuilder.Entity<Order>()
.HasIndex(o => new { o.CustomerId, o.CreatedAt }); // equality column first, then range/sort
次に、dotnet ef migrations add ... でマイグレーションを作成します。明示的なユーザー承認なしに、dotnet ef database update(またはデータベースに書き込みを行う同等の操作)で適用してはいけません。マイグレーションの適用はデータベースを変更するため、マイグレーションを追加してユーザーに提示し、ユーザーがレビューを終えてから更新を実行できるようにしてください。EF Core がスキーマを管理していない場合は、データベースの管理者に同じインデックスを推奨してください。インデックスを過剰に作成しないでください。インデックスを追加するたびに書き込みが遅くなります。
等価条件の列に続けて、範囲/並べ替えキーを配置してください。途中のキーに条件が指定されていない場合、前の等価条件の列がカバーされていても、必要な範囲や並べ替えが妨げられることがあります。先頭のキーがすべて等価条件の場合は、複数の並べ替え順序が機能する場合があります。リレーションシップのメタデータだけを根拠に、逆順の複合インデックスを不適切と判断しないでください。FK インデックスはリレーションシップをカバーしても、別の日付順のワークロードはカバーしないことがあります。別のパスをサポートするインデックスを置き換えるのではなく、ワークロード用のインデックスを追加することを検討してください。モデルコードから分かるのは意図されたメタデータであり、現在の物理インデックスではありません。書き込みコストとプロバイダー固有のカバレッジを考慮してください。
検証: 実行計画で、スキャンではなくシーク/インデックスが使用されることを確認します。
リポジトリを介したトレースの永続化
サービスループでは、SaveChanges がリポジトリ、監査ジャーナル、または進行状況フックの背後に隠れていることがあります。最後に 1 回だけユニットオブワークをコミットしても、それ以前の書き込みは取り消されません。項目ごとのコミットを主張する前に、外側のトランザクションが存在するか確認してください。保存処理自体が複数の SQL バッチを必要とする場合もあります。
契約で許される場合は、上限のあるバッチを準備してまとめて保存します。監査/アウトボックスのレコード、生成されたキー、リレーションシップの修正、順序、並行性、ドメインイベントを維持してください。保存後は、入力順に並んだエンティティ参照から ID を取得し、数値 ID の割り当て順序を前提にしないでください。
再試行されるフィードでは、すでにコミットされた内容と、呼び出し元が再生できる内容を考慮します。一意性と再生処理、または確認済みのチェックポイントを備えた永続的な行/フィード ID を維持するか、設計してください。チャンク化だけでは冪等性は得られません。また、保護されていない存在確認後の挿入は競合に対して安全ではありません。
増加するインポートで変更検出の範囲を制限する
正常に保存されたエンティティも引き続き追跡されます。自動検出が有効な場合、ChangeTracker.Entries<T>()、HasChanges、SaveChanges は追跡対象の状態を調べます。エントリを型で絞り込んでも、その前段階のスキャン範囲は限定されません。追跡対象のグラフが大きくなると、保存をチャンク化していても、行ごとに繰り返し検査することで CPU 処理が超線形に増加する場合があります。
追跡対象を繰り返し列挙する代わりに、インポート専用の進行状況カウンターを使用します。コンテキストがそのバッチの所有物である場合に限り、コンテキストを切り替えるか、処理済みの作業をクリアしてください。保留中の変更と参照は維持します。EF Core の Add と AddRange は、自動的に DetectChanges を呼び出すわけではありません。一方を他方に置き換えても、変更検出の問題は解決しません。冗長なスキャンが実証された場合に限り自動変更検出を抑制し、finally で以前の設定に戻してください。また、永続化/監査の前に必要な変更を明示的に検出してください。
セットベースの一括更新と削除
エンティティごとの処理が不要な、明らかに大規模で単純な更新/削除では、ExecuteUpdateAsync/ExecuteDeleteAsync(EF Core 7 以降および対応プロバイダー)を検討してください。これらはエンティティを実体化せずに、直ちに実行されます。
await db.Products.Where(p => p.LastSoldDate < cutoff)
.ExecuteUpdateAsync(s => s.SetProperty(p => p.IsActive, false));
書き込みワークフロー全体を確認してください。
- 即時書き込みでは、読み込み済みエンティティの値やスナップショットは更新されません。同じプロパティを後から変更すると、同じリクエスト内で順次実行していても、SQL による変更が上書きされる場合があります。追跡対象を使った一貫した保存、または明示的に同期された追跡対象外のセットベース処理を使用してください。トランザクションだけでは追跡状態は更新されません。
- これらの API では、設定済みの並行性トークンは自動的にチェックされません。楽観的並行性が必要な場合は、トークンの述語、トークンの更新、影響を受けた行の処理を維持してください。
- 呼び出しはそれぞれ個別に実行されます。後続の
SaveChangesによってバッチ化されることも、監査/アウトボックスへの書き込みと原子的になることもありません。必要に応じて、1 回の保存単位を使用するか、明示的な共有トランザクションを使用してください。 - クライアント側のカスケードと、SaveChanges ベースのフックを維持してください。コマンドインターセプターは別の仕組みであり、引き続き SQL を監視できます。クエリ数を減らすためだけにドメインの動作を迂回しないでください。
検証: 先行する SELECT がなく、WHERE を含む単一の UPDATE/DELETE が実行されること。
DbContext の操作を重複させない
EF Core は、同じ DbContext 上での操作の重複をサポートしていません。これには、そのコンテキストで構築された別々のリポジトリオブジェクトを介した操作も含まれます。どちらかを待機する前に両方の呼び出しを開始するだけで、重複実行になります。Task.WhenAll を使っても共有が安全になるわけではありません。順次 await するか、並行操作ごとに独立したコンテキストを 1 つ作成して破棄してください。ファクトリーで作成した別々のコンテキストなら、安全に並行実行できます。静的変数やシングルトンが保持するコンテキストも、ライフタイムやスレッドセーフ性の問題を引き起こします。
よくある落とし穴
| 落とし穴 | 対処法 |
|---|---|
| 関数でラップされた述語が通常のインデックスと互換性がない | 範囲を使うか、既存のインデックス付き正規化値/計算値を使います |
| クエリの構成を修正しても、ワークロードに対するインデックスのカバー範囲を考慮しない | 規約によって作成されたインデックスも含め、実際の等価条件/範囲/並び順のプレフィックスに合わせます |
| まれにしか実行されないクエリをコンパイルする | 本当に頻繁に実行されるクエリ形状だけをコンパイルします |
| EF の変換の再利用とデータベースの実行計画の再利用を混同する | SQL のパラメーター/リテラルの選択を確認し、ワークロードの根拠に基づいてオーバーライドを正当化します |
| DTO にエンティティが含まれているのに、追跡されないと想定する | 外側の戻り値の型ではなく、具体化されたグラフを確認します |
| 一括書き込みの後、古い値を使って追跡中の算術演算を行う | 書き込みを統一し、同時実行性/原子性を維持します。単にトランザクションを追加するだけでは不十分です |
リポジトリの遅延実行される IEnumerable を繰り返し列挙する | ローカル演算子に移る前に、データベースの集計を構成します |