web-dev-qa-db-ja.com

なぜ.Containsが遅いのですか?主キーで複数のエンティティを取得する最も効率的な方法は?

主キーで複数のエンティティを選択する最も効率的な方法は何ですか?

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?

}

私は比較するためにいくつかのパフォーマンステストを行うことができることを理解していますが、実際には両方よりも良い方法があるのではないかと思っています。 「翻訳済み」。

56
Tom

UPDATE:EF6にInExpressionが追加されたことにより、Enumerable.Containsの処理のパフォーマンスが劇的に向上しました。この回答の分析は優れていますが、2013年以降ほとんど廃止されました。

Entity FrameworkでのContainsの使用は実際には非常に遅いです。 SQLのIN句に変換され、SQLクエリ自体が高速に実行されることは事実です。しかし、問題とパフォーマンスのボトルネックは、LINQクエリからSQLへの変換にあります。作成される式ツリーは、ORを表すネイティブ式がないため、IN連結の長いチェーンに展開されます。 SQLが作成されると、多くのORsのこの式が認識され、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秒短縮されます。

テストは、同じマシン上のデータベースクライアント(コンソールアプリ)とデータベースサーバーで行われました。往復が多いため、「リモート」データベースでは最後の結果が大幅に悪化する可能性があります。

129
Slauma

2番目のオプションは最初のオプションよりも確実に優れています。最初のオプションはデータベースへのids.Lengthクエリになり、2番目のオプションはSQLクエリで'IN'演算子を使用できます。基本的に、LINQクエリを次のSQLのようなものに変換します。

SELECT *
FROM ImagesTable
WHERE id IN (value1,value2,...)

ここで、value1、value2などは、ids変数の値です。ただし、この方法でクエリにシリアル化できる値の数には上限がある可能性があることに注意してください。ドキュメントが見つかるかどうかを確認します...

4
Rune

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();
}

これはきれいな方法ではありませんが、大きなリストの場合は非常にパフォーマンスが高くなります。

0
nelson eldoro

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のパフォーマンス は、これに関するいくつかの考えです。

0
Juanito