87

我正在运行一个导入,每次运行都会有 1000 条记录。只是想对我的假设进行一些确认:

以下哪一项最有意义:

  1. 运行SaveChanges()每个AddToClassName()呼叫。
  2. SaveChanges()nAddToClassName()调用运行一次。
  3. 所有呼叫SaveChanges()之后运行。AddToClassName()

第一个选项可能很慢吧?因为它需要分析内存中的 EF 对象,生成 SQL 等。

我假设第二个选项是两全其美的选择,因为我们可以在该调用周围包装一个 try catch ,并且如果其中一个失败,则一次SaveChanges()只会丢失n条记录。也许将每个批次存储在 List<> 中。如果SaveChanges()调用成功,则摆脱列表。如果失败,请记录项目。

最后一个选项可能最终也很慢,因为每个单个 EF 对象都必须在内存中,直到SaveChanges()被调用。如果保存失败,则不会提交任何内容,对吗?

4

5 回答 5

67

我会先测试它以确定。性能不必那么差。

如果您需要在一个事务中输入所有行,请在所有 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);
    }
}
于 2009-12-18T22:22:42.477 回答
20

我刚刚在自己的代码中优化了一个非常相似的问题,并想指出一个对我有用的优化。

我发现处理 SaveChanges 的大部分时间,无论是一次处理 100 条还是 1000 条记录,都受 CPU 限制。因此,通过使用生产者/消费者模式(使用 BlockingCollection 实现)处理上下文,我能够更好地利用 CPU 内核并从每秒总共 4000 次更改中获得(由 SaveChanges 的返回值报告)到超过 14,000 次更改/秒。CPU 利用率从大约 13%(我有 8 个内核)上升到大约 60%。即使使用多个消费者线程,我也几乎不会对(非常快的)磁盘 IO 系统产生负担,SQL Server 的 CPU 利用率也不高于 15%。

通过将保存分流到多个线程,您可以调整提交前的记录数和执行提交操作的线程数。

我发现创建 1 个生产者线程和 (# of CPU Cores)-1 个消费者线程允许我调整每批提交的记录数,以便 BlockingCollection 中的项目数在 0 和 1 之间波动(在消费者线程占用一个物品)。这样一来,消耗线程的工作量就足够了,可以以最佳方式工作。

这个场景当然需要为每个批次创建一个新的上下文,我发现即使在我的用例的单线程场景中也更快。

于 2012-10-28T18:22:31.823 回答
13

如果您需要导入数千条记录,我会使用 SqlBulkCopy 之类的东西,而不是实体框架。

于 2009-12-18T22:17:50.580 回答
2

使用存储过程。

  1. 在 Sql Server 中创建用户定义的数据类型。
  2. 在您的代码中创建并填充此类型的数组(非常快)。
  3. 通过一次调用(非常快)将数组传递给您的存储过程。

我相信这将是最简单和最快的方法。

于 2015-07-09T20:30:22.180 回答
2

抱歉,我知道这个帖子很旧,但我认为这可以帮助其他人解决这个问题。

我遇到了同样的问题,但有可能在提交更改之前验证更改。我的代码看起来像这样,它工作正常。使用chUser.LastUpdatedI 检查它是新条目还是仅更改。因为无法重新加载尚未在数据库中的条目。

// 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();
于 2017-06-14T09:32:31.840 回答