在創建1000個Entity Framework對象時應該何時調用SaveChanges()? (比如導入時)

entity-framework import loops performance savechanges

我正在運行一個導入,每次運行將有1000個記錄。只是在我的假設上尋找一些確認:

以下哪一項最有意義:

  1. 每次AddToClassName()調用運行SaveChanges()
  2. nAddToClassName()調用運行SaveChanges()
  3. 所有 AddToClassName()調用之後運行SaveChanges()

第一種選擇可能是慢的嗎?因為它需要分析內存中的EF對象,生成SQL等。

我假設第二個選項是兩個世界中最好的,因為我們可以圍繞SaveChanges()調用包裝try catch,並且一次只丟失n個記錄,如果其中一個失敗。也許將每個批次存儲在List <>中。如果SaveChanges()調用成功,則刪除列表。如果失敗,請記錄項目。

最後一個選項可能最終也會非常慢,因為在調用SaveChanges()之前,每個EF對像都必須在內存中。如果保存失敗,則不會發生任何事情,對吧?

一般承認的答案

我會首先測試它以確定。性能不一定非常糟糕。

如果需要在一個事務中輸入所有行,請在所有AddToClassName類之後調用它。如果可以單獨輸入行,請在每行之後保存更改。數據庫一致性很重要。

第二種選擇我不喜歡。如果我對系統進行導入並且它會從1000中減少10行,那將會讓我感到困惑(從最終用戶的角度來看),因為1是壞的。您可以嘗試導入10,如果失敗,請逐個嘗試然後登錄。

測試是否需要很長時間。不要寫'可行'。你還不知道。只有當它實際上是一個問題時,請考慮其他解決方案(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()。
  • 如果一個提交失敗,請逐個嘗試查找錯誤的行。

測試類:

表:

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

熱門答案

我只是在我自己的代碼中優化了一個非常類似的問題,並想指出一個對我有用的優化。

我發現處理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之間波動(在消費者線程佔用一個之後)項目)。這樣,消費線程就有足夠的工作來最佳地工作。

這個場景當然需要為每個批處理創建一個新的上下文,我發現即使在我的用例的單線程場景中也會更快。



Related

許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
這個KB合法嗎? 是的,了解原因
許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
這個KB合法嗎? 是的,了解原因