主キーで複数のエンティティを選択する最も効率的な方法は何ですか?
public IEnumerable<Models.Image> GetImagesById(IEnumerable<int> ids)
{
//return ids.Select(id => Images.Find(id)); //is this cool?
return Images.Where( im => ids.Contains(im.Id)); //is this better, worse or the same?
//is there a (better) third way?
}
私は比較するためにいくつかのパフォーマンステストを行うことができることを理解していますが、実際には両方よりも良い方法があるのではないかと思っています。 「翻訳済み」。
UPDATE:EF6にInExpressionが追加されたことにより、Enumerable.Containsの処理のパフォーマンスが劇的に向上しました。この回答の分析は優れていますが、2013年以降ほとんど廃止されました。
Entity FrameworkでのContains
の使用は実際には非常に遅いです。 SQLのIN
句に変換され、SQLクエリ自体が高速に実行されることは事実です。しかし、問題とパフォーマンスのボトルネックは、LINQクエリからSQLへの変換にあります。作成される式ツリーは、OR
を表すネイティブ式がないため、IN
連結の長いチェーンに展開されます。 SQLが作成されると、多くのOR
sのこの式が認識され、SQL IN
句に折り返されます。
これは、Contains
コレクション(最初のオプション)の要素ごとに1つのクエリを発行するよりもids
を使用する方が悪いという意味ではありません。それはおそらくもっと良いでしょう-少なくともあまり大きくないコレクションのために。しかし、大規模なコレクションの場合、それは本当に悪いです。しばらく前に、SQLのクエリが1秒未満で実行されたにもかかわらず、約12.000の要素を持つContains
クエリをテストしたことを覚えています。
各ラウンドトリップのContains
式の要素数が少ないデータベースへの複数のラウンドトリップの組み合わせのパフォーマンスをテストする価値があるかもしれません。
このアプローチとEntity FrameworkでContains
を使用する際の制限を以下に示し、説明します。
Contains()演算子がEntity Frameworkのパフォーマンスを大幅に低下させるのはなぜですか?
この状況で生のSQLコマンドが最適に実行される可能性があります。つまり、dbContext.Database.SqlQuery<Image>(sqlString)
またはdbContext.Images.SqlQuery(sqlString)
を呼び出します。ここで、sqlString
は@Runeの回答に示されているSQLです。
編集
ここにいくつかの測定があります:
550000レコードと11列(ギャップなしでIDが1から始まる)を持つテーブルでこれを実行し、ランダムに20000 IDを選択しました。
_using (var context = new MyDbContext())
{
Random Rand = new Random();
var ids = new List<int>();
for (int i = 0; i < 20000; i++)
ids.Add(Rand.Next(550000));
Stopwatch watch = new Stopwatch();
watch.Start();
// here are the code snippets from below
watch.Stop();
var msec = watch.ElapsedMilliseconds;
}
_
テスト1
_var result = context.Set<MyEntity>()
.Where(e => ids.Contains(e.ID))
.ToList();
_
結果->msec = 85.5 sec
テスト2
_var result = context.Set<MyEntity>().AsNoTracking()
.Where(e => ids.Contains(e.ID))
.ToList();
_
結果->msec = 84.5 sec
AsNoTracking
のこの小さな効果は非常に珍しいです。これは、ボトルネックがオブジェクトの実体化ではないことを示しています(以下に示すSQLではありません)。
どちらのテストでも、SQLプロファイラーでSQLクエリが非常に遅くデータベースに到着することがわかります。 (正確には測定しませんでしたが、70秒より後でした。)明らかに、このLINQクエリのSQLへの変換は非常に高価です。
テスト3
_var values = new StringBuilder();
values.AppendFormat("{0}", ids[0]);
for (int i = 1; i < ids.Count; i++)
values.AppendFormat(", {0}", ids[i]);
var sql = string.Format(
"SELECT * FROM [MyDb].[dbo].[MyEntities] WHERE [ID] IN ({0})",
values);
var result = context.Set<MyEntity>().SqlQuery(sql).ToList();
_
結果->msec = 5.1 sec
テスト4
_// same as Test 3 but this time including AsNoTracking
var result = context.Set<MyEntity>().SqlQuery(sql).AsNoTracking().ToList();
_
結果->msec = 3.8 sec
今回は、追跡を無効にする効果がより顕著になります。
テスト5
_// same as Test 3 but this time using Database.SqlQuery
var result = context.Database.SqlQuery<MyEntity>(sql).ToList();
_
結果->msec = 3.7 sec
私の理解では、context.Database.SqlQuery<MyEntity>(sql)
はcontext.Set<MyEntity>().SqlQuery(sql).AsNoTracking()
と同じであるため、テスト4とテスト5の間に予想される違いはありません。
(結果セットの長さは、ランダムIDを選択した後に重複する可能性があるため、常に同じではありませんでしたが、常に19600〜19640の要素でした。)
Edit 2
テスト6
データベースへの20000回の往復でも、Contains
を使用するよりも高速です。
_var result = new List<MyEntity>();
foreach (var id in ids)
result.Add(context.Set<MyEntity>().SingleOrDefault(e => e.ID == id));
_
結果->msec = 73.6 sec
SingleOrDefault
の代わりにFind
を使用したことに注意してください。 Find
が内部でFind
を呼び出すため、DetectChanges
で同じコードを使用すると非常に遅くなります(数分後にテストをキャンセルしました)。自動変更検出(_context.Configuration.AutoDetectChangesEnabled = false
_)を無効にすると、SingleOrDefault
とほぼ同じパフォーマンスになります。 AsNoTracking
を使用すると、時間が1〜2秒短縮されます。
テストは、同じマシン上のデータベースクライアント(コンソールアプリ)とデータベースサーバーで行われました。往復が多いため、「リモート」データベースでは最後の結果が大幅に悪化する可能性があります。
2番目のオプションは最初のオプションよりも確実に優れています。最初のオプションはデータベースへのids.Length
クエリになり、2番目のオプションはSQLクエリで'IN'
演算子を使用できます。基本的に、LINQクエリを次のSQLのようなものに変換します。
SELECT *
FROM ImagesTable
WHERE id IN (value1,value2,...)
ここで、value1、value2などは、ids変数の値です。ただし、この方法でクエリにシリアル化できる値の数には上限がある可能性があることに注意してください。ドキュメントが見つかるかどうかを確認します...
Weel、最近同様の問題があり、私が見つけた最良の方法は、一時テーブルに含まれるリストを挿入し、結合した後です。
private List<Foo> GetFoos(IEnumerable<long> ids)
{
var sb = new StringBuilder();
sb.Append("DECLARE @Temp TABLE (Id bitint PRIMARY KEY)\n");
foreach (var id in ids)
{
sb.Append("INSERT INTO @Temp VALUES ('");
sb.Append(id);
sb.Append("')\n");
}
sb.Append("SELECT f.* FROM [dbo].[Foo] f inner join @Temp t on f.Id = t.Id");
return this.context.Database.SqlQuery<Foo>(sb.ToString()).ToList();
}
これはきれいな方法ではありませんが、大きなリストの場合は非常にパフォーマンスが高くなります。
ToArray()を使用してリストを配列に変換すると、パフォーマンスが向上します。次の方法で実行できます。
ids.Select(id => Images.Find(id));
return Images.toArray().Where( im => ids.Contains(im.Id));
私はEntity Framework 6.1を使用していますが、 code を使用していることがわかりました。
return db.PERSON.Find(id);
のではなく:
return db.PERSONA.FirstOrDefault(x => x.ID == id);
Find()とFirstOrDefaultのパフォーマンス は、これに関するいくつかの考えです。