【问题标题】:How to Optimize Code Performance in .NET [closed]如何在 .NET 中优化代码性能 [关闭]
【发布时间】:2016-11-16 03:01:19
【问题描述】:

我有一个将数据从旧数据库迁移到新数据库的导出作业。我遇到的问题是用户数量约为 300 万,而这项工作需要很长时间才能完成(15 多个小时)。我使用的机器只有 1 个处理器,所以我不确定 threading 是否是我应该做的。有人可以帮我优化这段代码吗?

static void ExportFromLegacy()
{
    var usersQuery = _oldDb.users.Where(x =>
        x.status == 'active');

    int BatchSize = 1000;
    var errorCount = 0;
    var successCount = 0;
    var batchCount = 0;

    // Using MoreLinq's Batch for sequences
    // https://www.nuget.org/packages/MoreLinq.Source.MoreEnumerable.Batch
    foreach (IEnumerable<users> batch in usersQuery.Batch(BatchSize))
    {
        Console.WriteLine(String.Format("Batch count at {0}", batchCount));
        batchCount++;

        foreach(var user in batch)
        {
            try
            {               
                var userData = _oldDb.userData.Where(x =>
                    x.user_id == user.user_id).ToList();

                if (userData.Count > 0)
                {                   
                    // Insert into table
                    var newData = new newData()
                    {                       
                        UserId = user.user_id; // shortened code for brevity.                       
                    };

                    _db.newUserData.Add(newData);
                    _db.SaveChanges();

                    // Insert item(s) into table
                    foreach (var item in userData.items)
                    {
                        if (!_db.userDataItems.Any(x => x.id == item.id)
                        {
                            var item = new Item()
                            {                               
                                UserId = user.user_id, // shortened code for brevity.   
                                DataId = newData.id // id from object created above
                            };

                            _db.userDataItems.Add(item);                            
                        }

                        _db.SaveChanges();
                        successCount++;
                    }
                }               
            }
            catch(Exception ex)
            {
                errorCount++;
                Console.WriteLine(String.Format("Error saving changes for user_id: {0} at {1}.", user.user_id.ToString(), DateTime.Now));
                Console.WriteLine("Message: " + ex.Message);
                Console.WriteLine("InnerException: " + ex.InnerException);
            }
        }
    }

    Console.WriteLine(String.Format("End at {0}...", DateTime.Now));
    Console.WriteLine(String.Format("Successful imports: {0} | Errors: {1}", successCount, errorCount));
    Console.WriteLine(String.Format("Total running time: {0}", (exportStart - DateTime.Now).ToString(@"hh\:mm\:ss")));
}

【问题讨论】:

  • 您的研究表明了什么?不要将 EF 用于 ETL/批处理,回退到 SQL。例如,一个 MERGE 语句可能会有很大帮助。绝对不要在循环中调用 SaveChanges。这一切都假设数据库实际上是瓶颈,这意味着线程根本无法帮助您。
  • 水晶球说旧数据库在 user_id 列上没有索引。你真的需要一个。如果由于某种原因这不切实际,请确保只读取旧数据库中的所有行一次。
  • 先做一些分析。此外,并行化可能仍然有帮助,因为您可能会在数据库繁忙时做有用的 CPU 工作。
  • 您是否考虑过使用 SQL Server Data Tools 执行迁移?

标签: c# multithreading entity-framework optimization


【解决方案1】:

不幸的是,主要问题是数据库往返次数。

你来回旅行:

  • 对于每个用户,您可以通过旧数据库中的用户 ID 检索用户数据
  • 对于每个用户,您都将用户数据保存在新数据库中
  • 对于每个用户,您将用户数据项保存在新数据库中

因此,如果您说您有 300 万用户,并且每个用户平均有 5 个用户数据项,这意味着您至少进行了 3m + 3m + 15m = 2100 万次数据库往返,这太疯狂了。

显着提高性能的唯一方法是减少数据库往返次数。

批处理 - 按 id 检索用户

您可以通过一次检索所有用户数据来快速减少数据库往返次数,并且由于您不必跟踪它们,因此使用“AsNoTracking()”可以获得更多性能提升。

var list = batch.Select(x => x.user_id).ToList();
var userDatas = _oldDb.userData
                  .AsNoTracking()
                  .Where(x => list.Contains(x.user_id))
                  .ToList();

foreach(var userData in userDatas)
{
    ....
}

仅通过此更改,您应该已经节省了几个小时。

批处理 - 保存更改

每次保存用户数据或项目时,都会执行一次数据库往返。

免责声明:我是项目的所有者Entity Framework Extensions

这个库允许执行:

  • 批量保存更改
  • 批量插入
  • 批量更新
  • 批量删除
  • 批量合并

您可以在批处理结束时调用 BulkSaveChanges 或创建一个列表以插入并直接使用 BulkInsert 来获得更高的性能。

但是,您必须使用与 newData 实例的关系,而不是直接使用 ID。

foreach (IEnumerable<users> batch in usersQuery.Batch(BatchSize))
{
    // Retrieve all users for the batch at once.
   var list = batch.Select(x => x.user_id).ToList();
   var userDatas = _oldDb.userData
                         .AsNoTracking()
                         .Where(x => list.Contains(x.user_id))
                         .ToList(); 

    // Create list used for BulkInsert      
    var newDatas = new List<newData>();
    var newDataItems = new List<Item();

    foreach(var userData in userDatas)
    {
        // newDatas.Add(newData);

        // newDataItem.OwnerData = newData;
        // newDataItems.Add(newDataItem);
    }

    _db.BulkInsert(newDatas);
    _db.BulkInsert(newDataItems);
}

编辑:回答子问题

newDataItem 的属性之一是newData 的id。 (前任。 newDataItem.newDataId.) 所以 newData 必须先保存在 为了生成它的id。如果有,我将如何 BulkInsert 另一个对象的依赖关系?

您必须改用导航属性。通过使用导航属性,您将永远不必指定父 id,而是设置父对象实例。

public class UserData
{
    public int UserDataID { get; set; }
    // ... properties ...

    public List<UserDataItem> Items { get; set; }
}

public class UserDataItem
{
    public int UserDataItemID { get; set; }
    // ... properties ...

    public UserData OwnerData { get; set; }
}

var userData = new UserData();
var userDataItem = new UserDataItem();

// Use navigation property to set the parent.
userDataItem.OwnerData = userData;

教程:Configure One-to-Many Relationship

另外,我在您的示例代码中没有看到 BulkSaveChanges。会是 必须在所有 BulkInserts 之后调用?

Bulk Insert 直接插入到数据库中。您不必指定“SaveChanges”或“BulkSaveChanges”,一旦您调用该方法,它就完成了;)

这是一个使用 BulkSaveChanges 的示例:

foreach (IEnumerable<users> batch in usersQuery.Batch(BatchSize))
{
    // Retrieve all users for the batch at once.
   var list = batch.Select(x => x.user_id).ToList();
   var userDatas = _oldDb.userData
                         .AsNoTracking()
                         .Where(x => list.Contains(x.user_id))
                         .ToList(); 

    // Create list used for BulkInsert      
    var newDatas = new List<newData>();
    var newDataItems = new List<Item();

    foreach(var userData in userDatas)
    {
        // newDatas.Add(newData);

        // newDataItem.OwnerData = newData;
        // newDataItems.Add(newDataItem);
    }

    var context = new UserContext();
    context.userDatas.AddRange(newDatas);
    context.userDataItems.AddRange(newDataItems);
    context.BulkSaveChanges();
}

由于必须使用 Entity Framework 的一些内部方法,BulkSaveChanges 比 BulkInsert 慢,但仍然比 SaveChanges 快。

在示例中,我为每个批次创建一个新上下文以避免内存问题并获得一些性能。如果您对所有批次重复使用相同的上下文,您将在 ChangeTracker 中拥有数百万个被跟踪的实体,这绝不是一个好主意。

【讨论】:

  • 这看起来很有希望。我今晚会试试这个,因为我白天不能运行这个。我会跟进的。感谢您的意见!
  • 有可能进一步优化性能,但让我们先从这个开始,看看发生了什么;)
  • newDataItem 的属性之一是newData 的id。 (例如 newDataItem.newDataId。)所以必须先保存 newData 才能生成它的 id。如果存在另一个对象的依赖关系,我将如何 BulkInsert?另外,我在您的示例代码中没有看到 BulkSaveChanges。在所有 BulkInserts 之后必须调用它吗?
  • 所以我昨晚正在运行它,它运行得非常快。谢谢你的提示。但是突然之间,我的 BulkInsert 抛出了一个错误:“给定的键不在字典中。”这仅发生在特定表的一个 BulkInsert 上。这是什么原因造成的?我似乎无法弄清楚发生了什么。
  • 您能否就这个问题直接联系我并提供完整的堆栈跟踪信息?我会尽快检查的。
【解决方案2】:

Entity Framework 是导入大量数据的非常糟糕的选择。我从个人经验中知道这一点。

话虽如此,当我尝试以与您相同的方式使用它时,我发现了一些优化方法。

Context 将在您添加对象时缓存它们,并且您执行的插入越多,未来的插入速度就越慢。我的解决方案是在处理该实例并创建一个新实例之前将每个上下文限制为大约 500 个插入。这显着提高了性能。

我能够利用多个线程来提高性能,但您必须非常小心资源争用。每个线程肯定都需要自己的Context,甚至不要考虑尝试在线程之间共享它。我的机器有 8 个内核,所以线程可能对你没有多大帮助;单核我怀疑它对你有帮助。

使用 AutoDetectChangesEnabled = false; 关闭 ChangeTracking,更改跟踪速度非常慢。不幸的是,这意味着您必须修改代码以直接通过上下文进行所有更改。没有Entity.Property = "Some Value";,它变成Context.Entity(e=&gt; e.Property).SetValue("Some Value");(或类似的东西,我不记得确切的语法),这使得代码很难看。

您所做的任何查询都应该使用AsNoTracking

尽管如此,我能够将大约 20 小时的流程缩短到大约 6 小时,但我仍然不建议为此使用 EF。这是一个非常痛苦的项目,几乎完全是因为我选择了错误的 EF 来添加数据。请使用其他东西...任何其他东西...

我不想给人留下 EF 是一个糟糕的数据访问库的印象,它非常擅长它的设计目的,不幸的是 不是它的设计目的。

【讨论】:

  • 我想把它缩短到 6 小时!我使用 EF 是因为我已经有一个支持旧应用程序和新应用程序的项目。但是您建议我使用什么来迁移数据?
  • 对于我在 EF 之后做的类似项目,我使用原始 SqlConnectionSqlCommand 插入数据。您可能能够避免使用 EF 从源数据库中读取数据,但在这种情况下,我会不惜一切代价避免使用它来写入数据。 @Eric Yeoman 提到使用 SQL Server Data Tools,这也可能是一个不错的选择。我与一位同事讨论了该选项,但此后无需进行任何数据导入,因此我对此没有任何个人经验。
  • 我已经有一段时间没有使用原始 ADO sqlconnection 和 sqlcommand 了。今晚我会考虑试试这个。我必须在下班时间做这个。感谢您的提示!
【解决方案3】:

我可以考虑几个选项。

1) 将 _db.SaveChanges() 移到 foreach() 右括号下可以稍微提高速度

foreach (...){
}
successCount += _db.SaveChanges();

2) 将项目添加到列表中,然后添加到上下文中

List<ObjClass> list = new List<ObjClass>();
foreach (...)
{
  list.Add(new ObjClass() { ... });
}
_db.newUserData.AddRange(list);
successCount += _db.SaveChanges();

3)如果是大量的dada,请节省一堆

List<ObjClass> list = new List<ObjClass>();
int cnt=0;
foreach (...)
{
  list.Add(new ObjClass() { ... });
  if (++cnt % 100 == 0) // bunches of 100
  {
    _db.newUserData.AddRange(list);
    successCount += _db.SaveChanges();
    list.Clear();
    // Optional if a HUGE amount of data
    if (cnt % 1000 == 0)
    {
      _db = new MyDbContext();
    } 
  }
}
// Don't forget that!
_db.newUserData.AddRange(list);
successCount += _db.SaveChanges();
list.Clear();

4) 如果太大,请考虑使用bulkinserts。互联网上有一些例子和一些免费的图书馆。 参考:https://blogs.msdn.microsoft.com/nikhilsi/2008/06/11/bulk-insert-into-sql-from-c-app/

在大多数这些选项中,您会失去对错误处理的一些控制,因为很难知道哪个选项失败了。

【讨论】:

  • 请在 AutoDetectChangesEnabled = fasle 和 AsNoTracking 上添加 Bradley 建议以进行查询。顺便说一句,SqlBulkInserts 不是“ef”插入,它们被映射到 SQL 并且非常非常快。 6 小时可能变成 60 秒。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-02
  • 2011-11-03
  • 2017-02-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多