web-dev-qa-db-ja.com

1000個のEntity Frameworkオブジェクトを作成するときにSaveChanges()を呼び出す必要があるのはいつですか? (インポート中など)

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

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

  1. SaveChanges() _AddToClassName()呼び出しごとに実行します。
  2. SaveChanges()を実行しますnAddToClassName()呼び出しの回数。
  3. SaveChanges()呼び出しのallの後にAddToClassName()を実行します。

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

SaveChanges()呼び出しの周りにtry catchをラップでき、一度にnのレコード数を失うだけなので、2番目のオプションは両方の世界で最高だと思います。それらの1つが失敗します。各バッチをList <>に保存することもできます。 SaveChanges()呼び出しが成功した場合、リストを削除します。失敗した場合は、アイテムを記録します。

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

73
John Bubriski

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

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

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

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

[〜#〜] edit [〜#〜]

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

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);
    }
}
55
LukLed

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

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

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

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

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

17
Eric J.

数千のレコードをインポートする必要がある場合は、SqlBulkCopyのようなものを使用し、そのためのEntity Frameworkは使用しません。

12
marc_s

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

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

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

2
David

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

私は同じ問題を抱えていましたが、変更をコミットする前に検証する可能性があります。私のコードは次のようになり、正常に機能しています。とともに 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();
1
Jan Leuenberger