枚举类型搜索的最有效方法

3
我正在编写一个小程序,它以.csv文件作为输入,大约有45k行。我试图将此文件的内容与数据库中表格的内容进行比较(通过Xrm.Sdk使用SQL Server通过动态CRM,如果有区别)。
在我的当前程序中(需要约25分钟进行比较-文件和数据库都是完全相同的,都有45k行没有差异),我在一个继承Collection和IEnumerable的DataCollection中拥有所有现有记录。
在下面的代码中,我使用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访问。


3
看起来你正在进行45k * 45k个比较,这确实很多。不过,25分钟似乎太长了。你能否确认existingRecords是内存中的,而不需要任何文件(数据库)访问? - H H
@HenkHolterman 是的,我可以确认在执行此代码之前,所有现有记录都在内存中。上述循环中没有IO或DB访问。 - Ben
在这种情况下,@HenkHolterman,我回答的假设是错误的。 - Ipsit Gaur
是的,这就是我问的原因。 - H H
3个回答

5

Himbrombeere 是正确的,你应该先执行查询并将结果放入集合中,然后再使用 AnyCountAddRange 或其他会再次执行查询的方法。在你的代码中,每次循环迭代可能会执行 5 次查询。

注意文档中的术语 延迟执行。如果一个方法是以这种方式实现的,那么它意味着这个方法可以用于构建一个 LINQ 查询(因此你可以将其与其他方法链接起来,并最终得到一个查询)。但只有不使用延迟执行的方法,例如 CountAnyToList(或纯粹的 foreach)才会实际执行它。如果你不想每次都执行整个查询,并且你需要多次访问这个查询,那么最好将结果存储在集合中(例如使用 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);
    }
}

请注意,即使键不存在(返回空列表),此方法仍然有效。

挑剔一点,但是无法编译,因为在“lookup”中的匿名类型与您传递给其索引器的类型不同。 - Evk
1
使用如下所示的查找类型大大减少了处理时间。发布的代码需要约25分钟才能完成。使用查找后,时间缩短到20秒左右。 - Ben

4
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()时实际上会执行该查询第三次。

当强制进行立即执行时,您只需执行一次查询,因此也仅需遍历整个集合一次,这将减少总体时间。


2
是的,这将把原来每个输入行执行6次的次数减少到一次。但是算法仍然是O(N * M),即较慢的。 - Ivan Stoev
我之前不知道这个...... 我添加了它后,性能从之前的25分钟急剧提高到了目前的9分钟。这是我可以用这个算法达到的最佳性能吗? - Ben
@Ben,从数据库一次性获取所有行到内存中,然后在内存中执行搜索(通过字典或类似方式)可能会更快。 - Evk
@Ben:看一下MSDN中的LINQ方法文档。在每个不执行但构建查询的方法中,你会发现术语“延迟”。这些方法可以链接而不立即执行每个单独的方法,但它们有一个“缺点”。如果你忘记执行它们,每当你使用它们时(与不使用延迟执行的方法,如CountAnyToList或简单的foreach循环),你将总是执行它们。这就是这里发生的事情。 - Tim Schmelter
@Ben:OP已经提到所有记录都已经在内存中了。但这并没有改变任何事情,因为他在循环的每次迭代中执行(内存中的)查询多达5次。 - Tim Schmelter
@TimSchmelter 将所有内容放入字典中(以字符串 r["field_1"].ToString() + r["field_2"] 为键等等)可能会有帮助。 - Evk

0
另一个选项,基本上与其他答案相同,是首先准备好您的数据,这样您就不会反复调用像r [“field_2”] (查找速度相对较慢)之类的东西。
这是一种(1)清理数据,(2)查询/连接数据,(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);
    }
}

我认为这将是提出的最快方法之一。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接