何千ものEntityFrameworkオブジェクトを作成するときに、いつSaveChanges()を呼び出す必要がありますか?(インポート時のように)


80

実行ごとに数千のレコードを持つインポートを実行しています。私の仮定の確認を探しているだけです:

これらのどれが最も理にかなっています:

  1. SaveChanges()すべてのAddToClassName()呼び出しを実行します。
  2. n回の呼び出しSaveChanges()ごとに実行します。AddToClassName()
  3. すべての呼び出しのSaveChanges()後に実行します。AddToClassName()

最初のオプションはおそらく遅いですよね?メモリ内のEFオブジェクトを分析する必要があるため、SQLなどを生成します。

2番目のオプションは、両方の長所であると思いSaveChanges()ます。これは、その呼び出しの周りにtry catchをラップでき、一方が失敗した場合に一度にn個のレコードしか失うことができないためです。たぶん、各バッチをリスト<>に保存します。SaveChanges()呼び出しが成功した場合は、リストを削除します。失敗した場合は、アイテムをログに記録します。

最後のオプションも、SaveChanges()呼び出されるまですべてのEFオブジェクトがメモリ内にある必要があるため、おそらく非常に遅くなります。そして、保存が失敗した場合、何もコミットされませんよね?

回答:


62

確かに最初にテストします。パフォーマンスはそれほど悪くなくてもかまいません。

1つのトランザクションにすべての行を入力する必要がある場合は、AddToClassNameクラスのすべての後に呼び出します。行を個別に入力できる場合は、行ごとに変更を保存します。データベースの一貫性は重要です。

私が好きではない2番目のオプション。システムにインポートした場合、(最終ユーザーの観点から)混乱し、1が悪いという理由だけで、1000行のうち10行が減少します。10をインポートして失敗した場合は、1つずつ試してからログに記録してください。

時間がかかるかどうかをテストします。「適切に」と書かないでください。あなたはまだそれを知りません。それが実際に問題である場合にのみ、他の解決策(marc_s)について考えてください。

編集

私はいくつかのテストを行いました(ミリ秒単位の時間):

10000行:

1行後のSaveChanges():18510,534
100行後の
SaveChanges():4350,3075 10000行後のSaveChanges():5233,0635

50000行:

1行後のSaveChanges():78496,929
500行後の
SaveChanges():22302,2835 50000行後のSaveChanges():24022,8765

したがって、実際には、n行後にコミットする方が結局よりも高速です。

私の推奨事項は次のとおりです。

  • n行後のSaveChanges()。
  • 1つのコミットが失敗した場合は、1つずつ試して、障害のある行を見つけてください。

テストクラス:

テーブル:

CREATE TABLE [dbo].[TestTable](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [SomeInt] [int] NOT NULL,
    [SomeVarchar] [varchar](100) NOT NULL,
    [SomeOtherVarchar] [varchar](50) NOT NULL,
    [SomeOtherInt] [int] NULL,
 CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

クラス:

public class TestController : Controller
{
    //
    // GET: /Test/
    private readonly Random _rng = new Random();
    private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private string RandomString(int size)
    {
        var randomSize = _rng.Next(size);

        char[] buffer = new char[randomSize];

        for (int i = 0; i < randomSize; i++)
        {
            buffer[i] = _chars[_rng.Next(_chars.Length)];
        }
        return new string(buffer);
    }


    public ActionResult EFPerformance()
    {
        string result = "";

        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "<br/>";
        TruncateTable();

        return Content(result);
    }

    private void TruncateTable()
    {
        using (var context = new CamelTrapEntities())
        {
            var connection = ((EntityConnection)context.Connection).StoreConnection;
            connection.Open();
            var command = connection.CreateCommand();
            command.CommandText = @"TRUNCATE TABLE TestTable";
            command.ExecuteNonQuery();
        }
    }

    private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows)
    {
        var startDate = DateTime.Now;

        using (var context = new CamelTrapEntities())
        {
            for (int i = 1; i <= noOfRows; ++i)
            {
                var testItem = new TestTable();
                testItem.SomeVarchar = RandomString(100);
                testItem.SomeOtherVarchar = RandomString(50);
                testItem.SomeInt = _rng.Next(10000);
                testItem.SomeOtherInt = _rng.Next(200000);
                context.AddToTestTable(testItem);

                if (i % commitAfterRows == 0) context.SaveChanges();
            }
        }

        var endDate = DateTime.Now;

        return endDate.Subtract(startDate);
    }
}

私が「おそらく」と書いた理由は、私が知識に基づいた推測をしたからです。「わからない」ということをより明確にするために、質問にしました。また、潜在的な問題に遭遇する前に、それらについて考えることは完全に理にかなっていると思います。それが私がこの質問をした理由です。私は誰かがどの方法が最も効率的であるかを知っていることを望んでいました、そして私はすぐにそれで行くことができました。
JohnBubriski

素晴らしい男。まさに私が探していたもの。これをテストするために時間を割いていただきありがとうございます!私は、各バッチをメモリに保存し、コミットを試して、失敗した場合は、あなたが言ったようにそれぞれを個別に実行できると思います。次に、そのバッチが完了したら、それらの100アイテムへの参照を解放して、メモリからクリアできるようにします。再度、感謝します!
JohnBubriski

3
すべてのオブジェクトがObjectContextによって保持されるため、メモリは解放されませんが、コンテキストに50000または100000があると、最近はあまりスペースを取りません。
LukLed 2009

6
SaveChanges()を呼び出すたびに、パフォーマンスが低下することが実際にわかりました。これに対する解決策は、各SaveChanges()呼び出しの後にコンテキストを実際に破棄し、追加されるデータの次のバッチのために新しいコンテキストを再インスタンス化することです。
Shawn de Wet

1
@LukLedは完全ではありません... Forループ内でSaveChangesを呼び出しています...コードはctxの同じインスタンスのforループ内に保存されるアイテムを追加し続け、後で同じインスタンスでSaveChangesを再度呼び出すことができます。
Shawn de Wet

18

私は自分のコードで非常によく似た問題を最適化したばかりで、自分に合った最適化を指摘したいと思います。

一度に100レコードを処理するか1000レコードを処理するかにかかわらず、SaveChangesの処理にかかる時間の多くはCPUに依存していることがわかりました。したがって、プロデューサー/コンシューマーパターン(BlockingCollectionで実装)を使用してコンテキストを処理することで、CPUコアをより有効に活用でき、合計4000回/秒の変更(SaveChangesの戻り値で報告)から14,000回以上の変更/秒。CPU使用率は約13%(私は8コア)から約60%に移動しました。複数のコンシューマースレッドを使用していても、(非常に高速な)ディスクIOシステムにほとんど負担をかけず、SQL ServerのCPU使用率は15%以下でした。

保存を複数のスレッドにオフロードすることで、コミット前のレコード数とコミット操作を実行するスレッド数の両方を調整できます。

1つのプロデューサースレッドと(CPUコアの数)-1つのコンシューマースレッドを作成することで、BlockingCollection内のアイテムの数が0と1の間で変動するようにバッチごとにコミットされるレコードの数を調整できることがわかりました(コンシューマースレッドが1つ取った後)項目)。そうすれば、消費スレッドが最適に機能するのに十分な作業がありました。

もちろん、このシナリオでは、バッチごとに新しいコンテキストを作成する必要があります。これは、私のユースケースのシングルスレッドシナリオでも高速であることがわかりました。


こんにちは、@ eric-jは、コードを試すことができるように、「プロデューサー/コンシューマーパターン(BlockingCollectionで実装)でコンテキストを処理することによって」この行を少し詳しく説明していただけますか?
Foyzul Karim 2017

14

何千ものレコードをインポートする必要がある場合は、Entity Frameworkではなく、SqlBulkCopyのようなものを使用します。


15
人々が私の質問に答えないとき、私はそれを嫌います:)まあ、私がEFを使うことが「必要」だとしましょう。では、どうしますか?
JohnBubriski

3
ええと、もしあなたが本当にEFを使わなければならないのなら、私は例えば500または1000レコードのバッチの後でコミットしようとします。そうしないと、リソースを使いすぎてしまい、100000行目が失敗したときに、失敗すると、更新した99999行すべてがロールバックされる可能性があります。
marc_s 2009

同じ問題で、その場合のEFよりもはるかにパフォーマンスの高いSqlBulkCopyを使用して終了しました。私はデータベースにアクセスするためにいくつかの方法を使用するのは好きではありませんが。
Julien N

2
私も同じ問題を抱えているので、この解決策を検討しています...バルクコピーは優れた解決策ですが、私のホスティングサービスはそれを使用することを許可していません(そして他の人もそうすると思います)ので、これは実行可能ではありません一部の人々のためのオプション。
デニスワード

3
@marc_s:SqlBulkCopyを使用するときに、ビジネスオブジェクトに固有のビジネスルールを適用する必要性をどのように処理しますか?ルールを冗長に実装せずにEFを使用しない方法がわかりません。
Eric J.

2

ストアドプロシージャを使用します。

  1. SQLServerでユーザー定義のデータ型を作成します。
  2. このタイプの配列を作成して、コードに入力します(非常に高速です)。
  3. 1回の呼び出しで配列をストアドプロシージャに渡します(非常に高速)。

これが最も簡単で最速の方法だと思います。


7
通常、SOでは、「これが最速」という主張は、テストコードと結果で実証する必要があります。
Michael Blackburn

2

申し訳ありませんが、このスレッドは古いことは知っていますが、これはこの問題を抱えている他の人々に役立つと思います。

同じ問題が発生しましたが、コミットする前に変更を検証する可能性があります。私のコードはこのように見え、正常に機能しています。でchUser.LastUpdated、それは新しいエントリまたは変更のみである場合、私を確認してください。まだデータベースにないエントリをリロードすることはできないためです。

// Validate Changes
var invalidChanges = _userDatabase.GetValidationErrors();
foreach (var ch in invalidChanges)
{
    // Delete invalid User or Change
    var chUser  =  (db_User) ch.Entry.Entity;
    if (chUser.LastUpdated == null)
    {
        // Invalid, new User
        _userDatabase.db_User.Remove(chUser);
        Console.WriteLine("!Failed to create User: " + chUser.ContactUniqKey);
    }
    else
    {
        // Invalid Change of an Entry
        _userDatabase.Entry(chUser).Reload();
        Console.WriteLine("!Failed to update User: " + chUser.ContactUniqKey);
    }                    
}

_userDatabase.SaveChanges();

はい、それはほぼ同じ問題ですよね?これにより、1000レコードすべてを追加でき、実行saveChanges()する前に、エラーの原因となるレコードを 削除できます。
Jan Leuenberger 2017年

1
ただし、質問の重点は、1回のSaveChanges呼び出しで効率的にコミットする挿入/更新の数にあります。あなたはその問題に取り組んでいません。SaveChangesが失敗する潜在的な理由は、検証エラーよりも多いことに注意してください。ちなみに、エンティティUnchangedをリロード/削除する代わりに、エンティティにマークを付けることもできます。
Gert Arnold

1
そうです、それは直接質問に対処していませんが、他の理由SaveChangesが失敗するものの、このスレッドにつまずいたほとんどの人が検証に問題を抱えていると思います。そして、これは問題を解決します。この投稿がこのスレッドで本当に邪魔をしている場合は、これを削除できます。問題は解決しました。他の人を助けようとしています。
Jan Leuenberger 2017年

これについて質問があります。あなたが呼び出すとき、それGetValidationErrors()はデータベースへの呼び出しを「偽造」し、エラーを取得しますか?返信ありがとうございます:)
JeancarloFontalvo19年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.