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

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

public IEnumerable GetImagesById(IEnumerable 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?

}

私は比較のためにいくつかのパフォーマンステストを行うことができることを認識していますが、実際には両方より良い方法があるのだろうかと思っており、これらの2つのクエリの違いが何であれ、 '翻訳されました'。

52
まあ、それは不確定で、通常は膨大な数ではありませんが、スケーラブルな方法で大きな数値をサポートできるようにしたいと考えています。
追加された 著者 Tom,
人々の検索をより親切にしてへのより速い置き換えのためにあなたの質問を書き直す自由を取った。含まれています
追加された 著者 spender,
男、どうしてこの質問に戻ってくるの?
追加された 著者 spender,
これはEF 6で修正されていることに注意してください - 第5弾 - blogs.msdn.com/b/adonet/archive/2012/12/10/…
追加された 著者 Ian Gregory,
追加された 著者 Gert Arnold,

5 答え

更新:EF6にInExpressionを追加することで、Enumerable.Contains処理のパフォーマンスが大幅に向上しました。この回答の分析は素晴らしいですが、2013年以降はほとんど廃止されています。

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

これは、 ids コレクション(最初のオプション)で要素ごとに1つのクエリを発行するよりも、 Contains を使用する方が悪いことを意味しません。それはおそらく、おそらくまだ優れている - 少なくともあまりにも大きなコレクションではない。しかし、大規模なコレクションの場合、それは本当に悪いです。 SQLのクエリが1秒未満で実行されたにもかかわらず、動作していたが約1時間半かかった約12.000個の要素を含む Contains クエリをテストしていたことを覚えています。

各往復の包含式の要素数を減らして、データベースへの複数往復の組み合わせのパフォーマンスをテストすることは価値があります。

このアプローチと、Entity Frameworkで Contains を使用する際の制限事項を示し、ここで説明します。

Why does the Contains() operator degrade Entity Framework's performance so dramatically?

It's possible that a raw SQL command will perform best in this situation which would mean that you call dbContext.Database.SqlQuery(sqlString) or dbContext.Images.SqlQuery(sqlString) where sqlString is the SQL shown in @Rune's answer.

編集

いくつかの測定値があります:

私は550000レコードと11列(IDはギャップなしで1から始まります)のテーブルでこれを行い、無作為に20000個のIDを選びました:

using (var context = new MyDbContext())
{
    Random rand = new Random();
    var ids = new List();
    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()
    .Where(e => ids.Contains(e.ID))
    .ToList();

Result -> msec = 85.5 sec

テスト2

var result = context.Set().AsNoTracking()
    .Where(e => ids.Contains(e.ID))
    .ToList();

Result -> 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().SqlQuery(sql).ToList();

Result -> msec = 5.1 sec

テスト4

// same as Test 3 but this time including AsNoTracking
var result = context.Set().SqlQuery(sql).AsNoTracking().ToList();

Result -> msec = 3.8 sec

今回は、トラッキングを無効にする効果が目立つようになりました。

テスト5

// same as Test 3 but this time using Database.SqlQuery
var result = context.Database.SqlQuery(sql).ToList();

Result -> msec = 3.7 sec

My understanding is that context.Database.SqlQuery(sql) is the same as context.Set().SqlQuery(sql).AsNoTracking(), so there is no difference expected between Test 4 and Test 5.

(結果セットの長さは、ランダムID選択の後の可能な重複のため常に同じではなく、常に19600と19640要素の間でした)。

編集2

テスト6

Contains を使用するよりもデータベースへの20000回のラウンドトリップが高速です:

var result = new List();
foreach (var id in ids)
    result.Add(context.Set().SingleOrDefault(e => e.ID == id));

Result -> msec = 73.6 sec

Find の代わりに SingleOrDefault を使用しています。 Find は内部的に DetectChanges を呼び出すため、 Find で同じコードを使用するのは非常に遅いです(数分後にテストをキャンセルしました)。自動変更検出( context.Configuration.AutoDetectChangesEnabled = false )を無効にすると、 SingleOrDefault とほぼ同じパフォーマンスになります。 AsNoTracking を使用すると、時間が1〜2秒短縮されます。

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

122
追加された
素晴らしい答え。おそらく私がそれに追加する唯一のものは..もしこの種のものがそれを回避することができればそれはおそらくそうでなければならない。同意しますか?
追加された 著者 Tom,
私は理想的には、パフォーマンスの観点から考えると、このタイプのクエリは単純に回避すべきものだと考え始めています。
追加された 著者 Tom,
非常に助けてくれてありがとう。
追加された 著者 Yaron Levi,
この回答は素晴らしいです。大きな努力。
追加された 著者 spender,
@ThisGuy:はい、ありがとう、それはずっと良いです!
追加された 著者 Slauma,
@トム:私はいくつかのテストを行った、私の編集を参照してください。
追加された 著者 Slauma,
@トム:私はテスト番号6をやった、私の編集2を参照してください。はい、最初のこの状況を避けることは良い戦略のようです。しかし、時にはそのようなクエリが必要なだけで、それを回避することはできません。私の結論はもっとです:生のSQLを使用することは意味がありますし、エンティティフレームワークのようなORMでパフォーマンス上の理由が必要な場合もあります。
追加された 著者 Slauma,
@トム:あなたの質問のために今、私はそれを忘れました。私は多くのことを学び、自分のアプリケーションで重大なパフォーマンスのボトルネックを検出するのに役立ちました。質問ありがとう:)
追加された 著者 Slauma,
素晴らしい答えと研究。スーパーマイナーノートですが、Test 3のIDリストを次のように1行で作成することができます:string values = string.Join( "、"、ids);
追加された 著者 ThisGuy,
ループを走らせることは私にとってはより速かった
追加された 著者 stack,

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

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

ここで、value1、value2などはids変数の値です。しかし、このようにしてクエリに直列化できる値の数には上限があると私は考えています。私はいくつかのドキュメントを見つけることができるかどうか見てみましょう...

4
追加された
ありがとう、これはあなたの考え方に行くのですか?それとも代替アプローチがありますか?
追加された 著者 Tom,
私は、この種のクエリが(少しの)コードの臭いを持っていると結論づけるのは正しいでしょうか?
追加された 著者 Tom,
どのくらいの時間と正確に4096を超えたのか?私が疑問に思っているこのクラスのより良い一般的なアプローチはありますか?
追加された 著者 Tom,
@ティム:これは間違いなく行く方法です。あなたがEF 4+を使用している限り、これを使用して、データベースへの単一のフェッチを得ることができます...
追加された 著者 Reed Copsey,
@トム:はい、より良い一般的なアプローチ - テーブル値のパラメータがあります。しかし、これにはストアドプロシージャを書く必要があり、LINQ-to-SQLやEFではサポートされていません。また、それ自体の不具合(不適切なクエリプランをキャッシュするなど)もあります。
追加された 著者 Allon Guralnek,
LINQ-to-SQLがすべてのIN値をパラメータ化するのに対して(列IN(val1、val2、val3、...))パラメータ化されていないIN式に変換するので、 列のIN(@ p1、@ p2、@ p3、...))、2100パラメータの制限をかなり早く打ちます。
追加された 著者 Allon Guralnek,
私はこれが行く方法だと思います - 私はよりよいアプローチを認識していません。私はどこかに記載されたサイズの限界を見つけることができるかどうかを見ていきます。存在する場合は、IDを適切なサイズのチャンクに分割することができます。あなたのIDの集合が小さいと分かっている場合は、その問題を無視することさえできるかもしれません。私は限界が512または1024のようなものだと信じています...
追加された 著者 Rune,
うーん、その上限はどこにも書かれていません。たぶん私は誤解されてLinq2SQLにのみ適用されます。私はupvote誰かが文書化する(おそらく、その欠如)その上限:-)
追加された 著者 Rune,
私はコレクションの4096を超えるエントリでこれをテストしました。エントリをシリアライズするのは時間がかかりませんが、動作します。
追加された 著者 Rune,

私はEntity Framework 6.1を使用しており、コードを使用していることがわかりました。

return db.PERSON.Find(id);

のではなく:

return db.PERSONA.FirstOrDefault(x => x.ID == id);

Performance of Find() vs. FirstOrDefault are some thoughts on this.

1
追加された
また、 stackoverflow.com/questions/11686225/… のsingleordefaultと比較して遅い特にコメント
追加された 著者 Tom,
優れた情報Tom、それはまさに私が探していたものでした
追加された 著者 Juanito,

toArray()を使用してリストを配列に変換すると、パフォーマンスが向上します。あなたはこのようにすることができます:

ids.Select(id => Images.Find(id));     
    return Images.toArray().Where( im => ids.Contains(im.Id));  
1
追加された
申し訳ありませんが、これは理にかなっていません。 Images.toArray()全体画像テーブルをメモリにプルします。あなたは真剣にそれを意味することはできません。また、最初の行が何をしているのかは明らかではありません。
追加された 著者 Gert Arnold,

Weelは最近同様の問題を抱えています。私が見つけた最良の方法は、一時テーブルに含まれているリストを挿入してから結合した後です。

private List GetFoos(IEnumerable 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(sb.ToString()).ToList();
}

これはかなりの方法ではありませんが、大きなリストの場合は非常に効果的です。

0
追加された