搜索可枚举的最有效方法
Most efficient way to search enumerable
我正在编写一个小程序,它接受一个 .csv 文件作为输入,其中包含大约 45k 行。我正在尝试将此文件的内容与数据库上 table 的内容进行比较(SQL 服务器通过使用 Xrm.Sdk 的动态 CRM,如果它有所不同)。
在我当前的程序中(比较需要大约 25 分钟 - 文件和数据库在此处完全相同,均为 45k 行,没有差异),我将数据库中的所有现有记录都保存在 DataCollection<Entity>
它继承了 Collection<T>
和 IEnumerable<T>
在我下面的代码中,我使用 Where
方法进行过滤,然后根据匹配计数执行逻辑。 Where
似乎是这里的瓶颈。有比这更有效的方法吗?我绝不是 LINQ 专家。
foreach (var record in inputDataLines)
{
var fields = record.Split(',');
var fund = fields[0];
var bps = Convert.ToDecimal(fields[1]);
var withdrawalPct = Convert.ToDecimal(fields[2]);
var percentile = Convert.ToInt32(fields[3]);
var age = Convert.ToInt32(fields[4]);
var bombOutTerm = Convert.ToDecimal(fields[5]);
var matchingRows = existingRecords.Entities.Where(r => r["field_1"].ToString() == fund
&& Convert.ToDecimal(r["field_2"]) == bps
&& Convert.ToDecimal(r["field_3"]) == withdrawalPct
&& Convert.ToDecimal(r["field_4"]) == percentile
&& Convert.ToDecimal(r["field_5"]) == age);
entitiesFound.AddRange(matchingRows);
if (matchingRows.Count() == 0)
{
rowsToAdd.Add(record);
}
else if (matchingRows.Count() == 1)
{
if (Convert.ToDecimal(matchingRows.First()["field_6"]) != bombOutTerm)
{
rowsToUpdate.Add(record);
entitiesToUpdate.Add(matchingRows.First());
}
}
else
{
entitiesToDelete.AddRange(matchingRows);
rowsToAdd.Add(record);
}
}
编辑:我可以确认所有 existingRecords
在执行此代码之前都在内存中。上述循环中没有IO或DB访问。
在 Convert.ToDecimal(r["field_5"]) == age);
行后添加 ToList
以强制立即执行查询。
var matchingRows = existingRecords.Entities.Where(r => r["field_1"].ToString() == fund
&& Convert.ToDecimal(r["field_2"]) == bps
&& Convert.ToDecimal(r["field_3"]) == withdrawalPct
&& Convert.ToDecimal(r["field_4"]) == percentile
&& Convert.ToDecimal(r["field_5"]) == age)
.ToList();
Where
实际上并没有执行 您的查询,它只是准备 它。实际执行稍后以延迟的方式发生。在您的情况下,调用 Count
时会发生这种情况,它本身将迭代整个项目集合。但是,如果第一个条件失败,则会检查第二个条件,从而在调用 Count
时导致完整集合的第二次迭代。在这种情况下,您实际上在调用 matchingRows.First()
.
时执行了 第三次 次查询
当强制立即执行时,你只执行一次查询,因此只迭代整个集合一次,这也会减少你的总时间。
是对的,你应该先执行查询并将结果放入集合中,然后再使用 Any
、Count
、AddRange
或任何方法再次执行查询。在您的代码中,查询可能在每次循环迭代中执行 5 次。
注意文档中的术语延迟执行。如果一个方法以这种方式实现,则意味着该方法可用于构造 LINQ 查询(因此您可以将它与其他方法链接起来,最后您有一个查询)。但只有不使用延迟执行的方法,如 Count
、Any
、ToList
(或普通的 foreach
)才会真正执行它。如果您不希望每次都执行整个查询并且必须多次访问此查询,最好将结果存储在一个集合中(.f.e 和 ToList
)。
但是,您可以使用一种更有效的方法,Lookup<TKey, TValue>
,它类似于字典,可以使用匿名类型作为键:
var lookup = existingRecords.Entities.ToLookup(r => new
{
fund = r["field_1"].ToString(),
bps = Convert.ToDecimal(r["field_2"]),
withdrawalPct = Convert.ToDecimal(r["field_3"]),
percentile = Convert.ToDecimal(r["field_4"]),
age = Convert.ToDecimal(r["field_5"])
});
现在您可以非常高效地在循环中访问此查找。
foreach (var record in inputDataLines)
{
var fields = record.Split(',');
var fund = fields[0];
var bps = Convert.ToDecimal(fields[1]);
var withdrawalPct = Convert.ToDecimal(fields[2]);
var percentile = Convert.ToInt32(fields[3]);
var age = Convert.ToInt32(fields[4]);
var bombOutTerm = Convert.ToDecimal(fields[5]);
var matchingRows = lookup[new {fund, bps, withdrawalPct, percentile, age}].ToList();
entitiesFound.AddRange(matchingRows);
if (matchingRows.Count() == 0)
{
rowsToAdd.Add(record);
}
else if (matchingRows.Count() == 1)
{
if (Convert.ToDecimal(matchingRows.First()["field_6"]) != bombOutTerm)
{
rowsToUpdate.Add(record);
entitiesToUpdate.Add(matchingRows.First());
}
}
else
{
entitiesToDelete.AddRange(matchingRows);
rowsToAdd.Add(record);
}
}
请注意,即使键不存在(返回空列表),这也会起作用。
另一个选项,基本上与其他答案相同,是先准备数据,这样你就不会重复调用 r["field_2"]
之类的东西(查找速度相对较慢).
这是 (1) 清理您的数据,(2) query/join 您的数据,(3) 处理您的数据的方法。
这样做:
(1)
var inputs =
inputDataLines
.Select(record =>
{
var fields = record.Split(',');
return new
{
fund = fields[0],
bps = Convert.ToDecimal(fields[1]),
withdrawalPct = Convert.ToDecimal(fields[2]),
percentile = Convert.ToInt32(fields[3]),
age = Convert.ToInt32(fields[4]),
bombOutTerm = Convert.ToDecimal(fields[5]),
record
};
})
.ToArray();
var entities =
existingRecords
.Entities
.Select(entity => new
{
fund = entity["field_1"].ToString(),
bps = Convert.ToDecimal(entity["field_2"]),
withdrawalPct = Convert.ToDecimal(entity["field_3"]),
percentile = Convert.ToInt32(entity["field_4"]),
age = Convert.ToInt32(entity["field_5"]),
bombOutTerm = Convert.ToDecimal(entity["field_6"]),
entity
})
.ToArray()
.GroupBy(x => new
{
x.fund,
x.bps,
x.withdrawalPct,
x.percentile,
x.age
}, x => new
{
x.bombOutTerm,
x.entity,
});
(2)
var query =
from i in inputs
join e in entities on new { i.fund, i.bps, i.withdrawalPct, i.percentile, i.age } equals e.Key
select new { input = i, matchingRows = e };
(3)
foreach (var x in query)
{
entitiesFound.AddRange(x.matchingRows.Select(y => y.entity));
if (x.matchingRows.Count() == 0)
{
rowsToAdd.Add(x.input.record);
}
else if (x.matchingRows.Count() == 1)
{
if (x.matchingRows.First().bombOutTerm != x.input.bombOutTerm)
{
rowsToUpdate.Add(x.input.record);
entitiesToUpdate.Add(x.matchingRows.First().entity);
}
}
else
{
entitiesToDelete.AddRange(x.matchingRows.Select(y => y.entity));
rowsToAdd.Add(x.input.record);
}
}
我怀疑这将是最快的方法之一。
我正在编写一个小程序,它接受一个 .csv 文件作为输入,其中包含大约 45k 行。我正在尝试将此文件的内容与数据库上 table 的内容进行比较(SQL 服务器通过使用 Xrm.Sdk 的动态 CRM,如果它有所不同)。
在我当前的程序中(比较需要大约 25 分钟 - 文件和数据库在此处完全相同,均为 45k 行,没有差异),我将数据库中的所有现有记录都保存在 DataCollection<Entity>
它继承了 Collection<T>
和 IEnumerable<T>
在我下面的代码中,我使用 Where
方法进行过滤,然后根据匹配计数执行逻辑。 Where
似乎是这里的瓶颈。有比这更有效的方法吗?我绝不是 LINQ 专家。
foreach (var record in inputDataLines)
{
var fields = record.Split(',');
var fund = fields[0];
var bps = Convert.ToDecimal(fields[1]);
var withdrawalPct = Convert.ToDecimal(fields[2]);
var percentile = Convert.ToInt32(fields[3]);
var age = Convert.ToInt32(fields[4]);
var bombOutTerm = Convert.ToDecimal(fields[5]);
var matchingRows = existingRecords.Entities.Where(r => r["field_1"].ToString() == fund
&& Convert.ToDecimal(r["field_2"]) == bps
&& Convert.ToDecimal(r["field_3"]) == withdrawalPct
&& Convert.ToDecimal(r["field_4"]) == percentile
&& Convert.ToDecimal(r["field_5"]) == age);
entitiesFound.AddRange(matchingRows);
if (matchingRows.Count() == 0)
{
rowsToAdd.Add(record);
}
else if (matchingRows.Count() == 1)
{
if (Convert.ToDecimal(matchingRows.First()["field_6"]) != bombOutTerm)
{
rowsToUpdate.Add(record);
entitiesToUpdate.Add(matchingRows.First());
}
}
else
{
entitiesToDelete.AddRange(matchingRows);
rowsToAdd.Add(record);
}
}
编辑:我可以确认所有 existingRecords
在执行此代码之前都在内存中。上述循环中没有IO或DB访问。
在 Convert.ToDecimal(r["field_5"]) == age);
行后添加 ToList
以强制立即执行查询。
var matchingRows = existingRecords.Entities.Where(r => r["field_1"].ToString() == fund
&& Convert.ToDecimal(r["field_2"]) == bps
&& Convert.ToDecimal(r["field_3"]) == withdrawalPct
&& Convert.ToDecimal(r["field_4"]) == percentile
&& Convert.ToDecimal(r["field_5"]) == age)
.ToList();
Where
实际上并没有执行 您的查询,它只是准备 它。实际执行稍后以延迟的方式发生。在您的情况下,调用 Count
时会发生这种情况,它本身将迭代整个项目集合。但是,如果第一个条件失败,则会检查第二个条件,从而在调用 Count
时导致完整集合的第二次迭代。在这种情况下,您实际上在调用 matchingRows.First()
.
当强制立即执行时,你只执行一次查询,因此只迭代整个集合一次,这也会减少你的总时间。
Any
、Count
、AddRange
或任何方法再次执行查询。在您的代码中,查询可能在每次循环迭代中执行 5 次。
注意文档中的术语延迟执行。如果一个方法以这种方式实现,则意味着该方法可用于构造 LINQ 查询(因此您可以将它与其他方法链接起来,最后您有一个查询)。但只有不使用延迟执行的方法,如 Count
、Any
、ToList
(或普通的 foreach
)才会真正执行它。如果您不希望每次都执行整个查询并且必须多次访问此查询,最好将结果存储在一个集合中(.f.e 和 ToList
)。
但是,您可以使用一种更有效的方法,Lookup<TKey, TValue>
,它类似于字典,可以使用匿名类型作为键:
var lookup = existingRecords.Entities.ToLookup(r => new
{
fund = r["field_1"].ToString(),
bps = Convert.ToDecimal(r["field_2"]),
withdrawalPct = Convert.ToDecimal(r["field_3"]),
percentile = Convert.ToDecimal(r["field_4"]),
age = Convert.ToDecimal(r["field_5"])
});
现在您可以非常高效地在循环中访问此查找。
foreach (var record in inputDataLines)
{
var fields = record.Split(',');
var fund = fields[0];
var bps = Convert.ToDecimal(fields[1]);
var withdrawalPct = Convert.ToDecimal(fields[2]);
var percentile = Convert.ToInt32(fields[3]);
var age = Convert.ToInt32(fields[4]);
var bombOutTerm = Convert.ToDecimal(fields[5]);
var matchingRows = lookup[new {fund, bps, withdrawalPct, percentile, age}].ToList();
entitiesFound.AddRange(matchingRows);
if (matchingRows.Count() == 0)
{
rowsToAdd.Add(record);
}
else if (matchingRows.Count() == 1)
{
if (Convert.ToDecimal(matchingRows.First()["field_6"]) != bombOutTerm)
{
rowsToUpdate.Add(record);
entitiesToUpdate.Add(matchingRows.First());
}
}
else
{
entitiesToDelete.AddRange(matchingRows);
rowsToAdd.Add(record);
}
}
请注意,即使键不存在(返回空列表),这也会起作用。
另一个选项,基本上与其他答案相同,是先准备数据,这样你就不会重复调用 r["field_2"]
之类的东西(查找速度相对较慢).
这是 (1) 清理您的数据,(2) query/join 您的数据,(3) 处理您的数据的方法。
这样做:
(1)
var inputs =
inputDataLines
.Select(record =>
{
var fields = record.Split(',');
return new
{
fund = fields[0],
bps = Convert.ToDecimal(fields[1]),
withdrawalPct = Convert.ToDecimal(fields[2]),
percentile = Convert.ToInt32(fields[3]),
age = Convert.ToInt32(fields[4]),
bombOutTerm = Convert.ToDecimal(fields[5]),
record
};
})
.ToArray();
var entities =
existingRecords
.Entities
.Select(entity => new
{
fund = entity["field_1"].ToString(),
bps = Convert.ToDecimal(entity["field_2"]),
withdrawalPct = Convert.ToDecimal(entity["field_3"]),
percentile = Convert.ToInt32(entity["field_4"]),
age = Convert.ToInt32(entity["field_5"]),
bombOutTerm = Convert.ToDecimal(entity["field_6"]),
entity
})
.ToArray()
.GroupBy(x => new
{
x.fund,
x.bps,
x.withdrawalPct,
x.percentile,
x.age
}, x => new
{
x.bombOutTerm,
x.entity,
});
(2)
var query =
from i in inputs
join e in entities on new { i.fund, i.bps, i.withdrawalPct, i.percentile, i.age } equals e.Key
select new { input = i, matchingRows = e };
(3)
foreach (var x in query)
{
entitiesFound.AddRange(x.matchingRows.Select(y => y.entity));
if (x.matchingRows.Count() == 0)
{
rowsToAdd.Add(x.input.record);
}
else if (x.matchingRows.Count() == 1)
{
if (x.matchingRows.First().bombOutTerm != x.input.bombOutTerm)
{
rowsToUpdate.Add(x.input.record);
entitiesToUpdate.Add(x.matchingRows.First().entity);
}
}
else
{
entitiesToDelete.AddRange(x.matchingRows.Select(y => y.entity));
rowsToAdd.Add(x.input.record);
}
}
我怀疑这将是最快的方法之一。