我有一个导出作业,将数据从旧数据库迁移到新数据库。我遇到的问题是用户人数约为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")));
}
答案 0 :(得分:0)
实体框架是导入大量数据的非常糟糕选择。我从个人经历中知道这一点。
话虽这么说,当我尝试以同样的方式使用它时,我发现了一些优化方法。
Context
将在您添加对象时缓存对象,并且您执行的插入越多,将来的插入速度就越慢。我的解决方案是在处理该实例并创建一个新实例之前将每个上下文限制为大约500个插入。这显着提升了性能。
我能够使用多个线程来提高性能,但您必须非常小心资源争用。每个线程肯定需要自己的Context
,甚至认为关于尝试在线程之间共享它。我的机器有8个内核,所以线程可能对你没有多大帮助;单核心我怀疑它会对你有所帮助。
使用AutoDetectChangesEnabled = false;
关闭ChangeTracking,更改跟踪速度非常慢。不幸的是,这意味着您必须修改代码才能直接通过上下文进行所有更改。不再有Entity.Property = "Some Value";
,它变成Context.Entity(e=> e.Property).SetValue("Some Value");
(或类似的东西,我不记得确切的语法),这会使代码难看。
您所做的任何查询都应该使用AsNoTracking
。
尽管如此,我能够将约20小时的过程减少到大约6小时,但我仍然不建议使用EF。这是一个非常痛苦的项目,几乎完全是因为我选择不好的EF来添加数据。请使用其他东西......其他任何东西......
我不想让人觉得EF是一个糟糕的数据访问库,它很擅长它的设计目的,遗憾的是这个不是它的设计目标。< / p>
答案 1 :(得分:0)
我可以考虑几个选项。
1)通过在foreach()关闭括号下移动_db.SaveChanges(),可以稍微提高速度
foreach (...){
}
successCount += _db.SaveChanges();
2)将项目添加到列表,然后添加到上下文
List<ObjClass> list = new List<ObjClass>();
foreach (...)
{
list.Add(new ObjClass() { ... });
}
_db.newUserData.AddRange(list);
successCount += _db.SaveChanges();
3)如果它是一个大量的达达,除了束?
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)如果TOOOO大,考虑使用bulkinserts。互联网上有一些例子和一些免费图书馆。
参考:https://blogs.msdn.microsoft.com/nikhilsi/2008/06/11/bulk-insert-into-sql-from-c-app/
在大多数这些选项中,您都会对错误处理失去一些控制权,因为很难知道哪一个失败了。
答案 2 :(得分:0)
不幸的是,主要问题是数据库往返次数。
你往返:
因此,如果你说你有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之后调用吗?
批量插入直接插入数据库。您不必指定“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();
}
BulkSaveChanges比BulkInsert慢,因为必须使用Entity Framework中的一些内部方法,但仍然比SaveChanges快。
在示例中,我为每个批处理创建了一个新的上下文,以避免内存问题并获得一些性能。如果您为所有批次重复使用相同的上下文,那么您将在ChangeTracker中拥有数百万个跟踪实体,这绝不是一个好主意。