1400万条数据更新性能过慢,求高效优化方案
优化1400万条记录更新的性能问题
我需要更新1400万条存储在interval_list中的记录,包括数值、位置及其他信息。每次迭代时,我会通过Interval对象的MeterID获取对应的位置信息与乘数,随后更新位置并基于乘数调整数值。当前代码运行耗时超过4小时,求更优实现方式。
原代码:
foreach (Interval interval in interval_list) { var result = from m in meterList where m.MeterID.Equals(interval.MeterID) where m.StartDate < (interval.UTCDateTime.ToLocalTime()) where m.FinalDate > (interval.UTCDateTime.ToLocalTime()) select m; if (result.Count() == 1) //Ignore if > 1 { int mult1 = result.ElementAt(0).Mult1; int mult2 = result.ElementAt(0).Mult2; interval.ServiceID = result.ElementAt(0).Locserv; // With updating check to see if value is already adjusted by mult if (interval.Mult1 > 1) { interval.Value = interval.Value / interval.Mult1; interval.Value = interval.Value * mult1; } interval.Mult1 = mult1; //MULT1 interval.Mult2 = mult2; //MULT2 } } public class Interval { public string MeterID { get; set; } public DateTime UTCDateTime { get; set; } public float Value { get; set; } public DateTime LocalDateTime { get; set; } public string ServiceID { get; set; } public string Account { get; set; } public string Rate { get; set; } public int Mult1 { get; set; } public int Mult2 { get; set; } } public class Meters { public string MeterID { get; set; } public string Locserv { get; set; } public DateTime StartDate { get; set; } public DateTime FinalDate { get; set; } public int Mult1 { get; set; } public int Mult2 { get; set; } }
核心性能瓶颈分析
原代码的低效点集中在以下几点:
- 线性遍历的高复杂度:每次循环都对
meterList做全量线性查询,1400万次循环带来O(N*M)的时间复杂度(N为interval数量,M为meter数量),这是最大的性能杀手。 - 重复枚举LINQ结果:
Count()和ElementAt(0)会分别枚举一次查询结果,相当于对匹配的meter数据遍历两次,额外消耗资源。 - 重复时间转换:每次循环都调用
UTCDateTime.ToLocalTime(),重复的时间转换累积消耗大量CPU。
分步优化方案
1. 预构建Meter分组索引
先按MeterID对meterList分组并构建字典,将按MeterID查找的时间复杂度从O(M)降到O(1):
// 预构建分组字典:key=MeterID,value=对应Meters列表 var meterDict = meterList .GroupBy(m => m.MeterID) .ToDictionary(g => g.Key, g => g.ToList());
2. 批量转换UTC时间到本地时间
提前将所有Interval的UTC时间转换为本地时间,避免循环内重复计算:
// 批量转换,只执行一次 foreach (var interval in interval_list) { interval.LocalDateTime = interval.UTCDateTime.ToLocalTime(); }
3. 优化循环内的匹配逻辑
从字典直接获取对应MeterID的列表,遍历匹配时间区间时一旦发现超过1个匹配就立即终止,同时只枚举一次结果:
foreach (Interval interval in interval_list) { if (meterDict.TryGetValue(interval.MeterID, out var meters)) { Meters matchedMeter = null; int matchCount = 0; foreach (var m in meters) { if (m.StartDate < interval.LocalDateTime && m.FinalDate > interval.LocalDateTime) { matchedMeter = m; matchCount++; // 超过1个匹配直接跳出,无需继续遍历 if (matchCount > 1) break; } } if (matchCount == 1) { var m = matchedMeter; interval.ServiceID = m.Locserv; // 合并数值计算,减少一次赋值 if (interval.Mult1 > 1) { interval.Value = interval.Value / interval.Mult1 * m.Mult1; } interval.Mult1 = m.Mult1; interval.Mult2 = m.Mult2; } } // 无匹配MeterID时直接跳过 }
4. 进阶优化:二分查找时间区间(可选)
如果每个MeterID下的时间区间是不重叠且按时间有序的,可以对分组后的Meters列表按StartDate排序,然后用二分查找快速定位匹配区间,进一步降低遍历成本:
// 构建字典时同时按StartDate排序 var meterDict = meterList .GroupBy(m => m.MeterID) .ToDictionary(g => g.Key, g => g.OrderBy(m => m.StartDate).ToList());
二分查找实现:
if (meterDict.TryGetValue(interval.MeterID, out var meters)) { int left = 0; int right = meters.Count - 1; Meters matchedMeter = null; int matchCount = 0; while (left <= right) { int mid = left + (right - left) / 2; var m = meters[mid]; if (m.FinalDate <= interval.LocalDateTime) { // 当前区间在目标时间之前,查找右侧 left = mid + 1; } else if (m.StartDate >= interval.LocalDateTime) { // 当前区间在目标时间之后,查找左侧 right = mid - 1; } else { // 找到匹配区间,检查是否有其他重叠区间 matchedMeter = m; matchCount++; // 检查左侧是否有匹配 int temp = mid - 1; while (temp >= 0) { var leftM = meters[temp]; if (leftM.StartDate < interval.LocalDateTime && leftM.FinalDate > interval.LocalDateTime) { matchCount++; break; } temp--; } // 检查右侧是否有匹配 temp = mid + 1; while (temp < meters.Count) { var rightM = meters[temp]; if (rightM.StartDate < interval.LocalDateTime && rightM.FinalDate > interval.LocalDateTime) { matchCount++; break; } temp++; } break; } } if (matchCount == 1 && matchedMeter != null) { // 同前的更新逻辑 interval.ServiceID = matchedMeter.Locserv; if (interval.Mult1 > 1) { interval.Value = interval.Value / interval.Mult1 * matchedMeter.Mult1; } interval.Mult1 = matchedMeter.Mult1; interval.Mult2 = matchedMeter.Mult2; } }
5. 并行处理(多核加速)
如果interval_list中的每个Interval对象是独立无共享状态的,可以用Parallel.ForEach利用多核CPU并行处理,大幅缩短运行时间:
// 注意:确保meterDict是只读的(线程安全),且Interval对象无共享状态 Parallel.ForEach(interval_list, interval => { if (meterDict.TryGetValue(interval.MeterID, out var meters)) { Meters matchedMeter = null; int matchCount = 0; foreach (var m in meters) { if (m.StartDate < interval.LocalDateTime && m.FinalDate > interval.LocalDateTime) { matchedMeter = m; matchCount++; if (matchCount > 1) break; } } if (matchCount == 1) { var m = matchedMeter; interval.ServiceID = m.Locserv; if (interval.Mult1 > 1) { interval.Value = interval.Value / interval.Mult1 * m.Mult1; } interval.Mult1 = m.Mult1; interval.Mult2 = m.Mult2; } } });
优化效果预估
通过上述优化,时间复杂度从O(N*M)降至O(N*K)(K为单个MeterID下的平均记录数),若使用二分查找则进一步降至O(N*logK)。结合并行处理,运行时间可从4小时大幅缩短至几十分钟甚至更短。
内容的提问来源于stack exchange,提问作者BL18
相关产品推荐
相关产品推荐

