【问题标题】:Select Distinct Count is really slowSelect Distinct Count 真的很慢
【发布时间】:2023-03-15 04:46:01
【问题描述】:

我有一个包含大约 7000 个对象的循环,在循环内我需要获取结构列表的不同计数。目前我正在使用 -

foreach (var product in productsToSearch)
{
    Console.WriteLine("Time elapsed: {0} start", stopwatch.Elapsed);
    var cumulativeCount = 0;
    productStore.Add(product);
    var orderLinesList = totalOrderLines
        .Where(myRows => productStore.Contains(myRows.Sku))
        .Select(myRows => new OrderLineStruct
        {
            OrderId = myRows.OrderId,
            Sku = myRows.Sku
        });
    var differences = totalOrderLines.Except(orderLinesList);
    cumulativeCount = totalOrderLinsCount - differences.Select(x => x.OrderId).Distinct().Count();
    cumulativeStoreTable.Rows.Add(product, cumulativeCount);      
    Console.WriteLine("Time elapsed: {0} end", stopwatch.Elapsed);
}

public struct OrderLineStruct
{
    public string OrderId { get; set; }
    public string Sku { get; set; }
}

获取不同计数时这非常慢。有人知道这样做的更有效方法吗?我曾尝试使用 MoreLinq,它有一个用于 Linq 的 DisctintBy 方法,但它并不高效,因为我已经计时了。我玩过 PLinq,但我有点不确定在哪里并行化这个查询。

所以循环的每次迭代都在 -
经过时间:00:00:37.1142047 开始
经过时间:00:00:37.8310148 结束

= 0.7168101 秒 * 7000 = 5017.6707(83.627845 分钟)

它的 Distinct() Count() 行处理时间最长(大约 0.5 秒)。变量差异有几十万个 OrderLineStruct,因此对此进行任何 linq 查询都很慢。

更新

我稍微修改了循环,现在它运行大约 10 分钟而不是超过 1 小时

foreach (var product in productsToSearch)
{
    var cumulativeCount = 0;
    productStore.Add(product);
    var orderLinesList = totalOrderLines
        .Join(productStore, myRows => myRows.Sku, p => p, (myRows, p) => myRows)
        .Select(myRows => new OrderLineStruct
        {
            OrderId = myRows.OrderId,
            Sku = myRows.Sku
        });
    totalOrderLines = totalOrderLines.Except(orderLinesList).ToList();
    cumulativeCount = totalOrderLinesCount - totalOrderLines.Select(x => x.OrderId).Distinct().Count();
    cumulativeStoreTable.Rows.Add(product, cumulativeCount);
}

在 except 上有一个 .ToList() 似乎有所不同,现在我在每次迭代后删除已经处理的订单,这提高了每次迭代的性能。

【问题讨论】:

  • 那么结构是什么样的? minimal reproducible example 真的很有帮助——我们真的对目前发生的事情没有太多线索。
  • 你没有使用product,那么你为什么要在循环中这样做呢?所以我的建议是:把它移出循环,它已经快了 7000 倍。
  • @JamesDev 不,你没有使用product,你使用的是productStore。为什么不将所有产品添加到 productstore,并将 linq 移到 foreach 之外?
  • 在您调用 Count 之前,您的任何 LINQ(包括在 Where 中包含嵌套 Contains 的第一个查询)实际上都不会被评估 - 你 确定 @ 987654330@是瓶颈?
  • @Kote var productStore = new HashSet();

标签: c# linq plinq morelinq


【解决方案1】:

你在错误的地方寻找问题。

orderLinesListdifferencesdifferences.Select(x => x.OrderId).Distinct() 只是带​​有 延迟执行 的 LINQ to Objects 链接 query 方法,Count() 方法正在执行它们全部。

您的处理算法非常低效。瓶颈是orderLinesList 查询,它为每个product 迭代整个totalOrderLines 列表,这被链接(包括)在ExceptDistinct 等中 - 再次,在循环内部,即 7000+次。

这是一个 IMO 执行相同操作的示例高效算法:

Console.WriteLine("Time elapsed: {0} start", stopwatch.Elapsed);
var productInfo =
(
    from product in productsToSearch
    join line in totalOrderLines on product equals line.Sku into orderLines
    select new { Product = product, OrderLines = orderLines }
).ToList();
var lastIndexByOrderId = new Dictionary<string, int>();
for (int i = 0; i < productInfo.Count; i++)
{
    foreach (var line in productInfo[i].OrderLines)
        lastIndexByOrderId[line.OrderId] = i; // Last wins
}
int cumulativeCount = 0;
for (int i = 0; i < productInfo.Count; i++)
{
    var product = productInfo[i].Product;
    foreach (var line in productInfo[i].OrderLines)
    {
        int lastIndex;
        if (lastIndexByOrderId.TryGetValue(line.OrderId, out lastIndex) && lastIndex == i)
        {
            cumulativeCount++;
            lastIndexByOrderId.Remove(line.OrderId);
        }
    }
    cumulativeStoreTable.Rows.Add(item.Product, cumulativeCount);
    // Remove the next if it was just to support your processing
    productStore.Add(item.Product);
}
Console.WriteLine("Time elapsed: {0} end", stopwatch.Elapsed);

【讨论】:

  • 谢谢伊万,但这不是我想要的,而是沿着正确的路线。所以我有一个包含 7000 个产品的产品列表,我需要逐个遍历这个列表,并将它们添加到一些存储(哈希集)中。每次迭代后,我使用哈希集来查找总订单线,例如,我将在第一次迭代后搜索产品 1,然后在第二次迭代后搜索产品 1 和产品 2。然后我需要从产品中找到订单线,然后找到订单线和总订单线之间的差异(按订单 ID 区分)。希望这是有道理的?
  • 这正是我的算法所做的,只是方式不同。在您的示例中,不是 DistinctOrderCount(all items) - DistinctOrderCount(all items except (product 1 + product2) 它执行 DistinctCount(product 1 + product 2)。这不是产生相同的结果吗?
  • 不幸的是它不是正确的输出。所以原因是,例如在第一次迭代中,如果订单 1 有产品 1 和产品 2。您的算法匹配产品 1 并将其视为有效,但我的算法不会,因为商店中缺少产品 2。希望这是有道理的。
  • 我明白了。不得不多想一点。但我想你明白了,最初的 group join 做了必要的分离,然后关键是要以某种方式消除(替换为)循环内的过度操作。我认为如果我们创建productInfo 列表(通过调用ToList())然后按索引对其进行迭代,则不需要Except,因此 differences* 将是当前索引之后的所有项目.所以棘手的部分是如何消除DistinctCount(如果可能的话)。
  • 我看到你接受了答案。同时,我仍然快速更新了它,希望这次是正确的逻辑。请检查一下,让我知道它是否适合您。
【解决方案2】:

在您的情况下,正如 Jon Hanna 所提到的,瓶颈是 Except 方法。
DistinctCount 具有第二优先级。
您可以通过对方法的每个部分执行枚举并放置秒表来验证这一点。

foreach (var product in productsToSearch)
{
    var cumulativeCount = 0;
    productStore.Add(product);

    olSw.Start();
    var orderLinesList = totalOrderLines
        .Where(myRows => productStore.Contains(myRows.Sku))
        .Select(myRows => new OrderLineStruct
        {
            OrderId = myRows.OrderId,
            Sku = myRows.Sku
        }).ToList();
    olSw.Stop();

    exSw.Start();
    var differences = totalOrderLines.Except(orderLinesList).ToList();
    exSw.Stop();

    dcSw.Start();
    cumulativeCount = totalOrderLinsCount - differences.Select(x => x.OrderId).Distinct().Count();
    dcSw.Stop();
}

测量值:
productsToSearch count 100
totalOrderLines count 300 000

Total olSw time: 00:00:01.3583340
Total exSw time: 00:00:14.3304959
Total dcSw time: 00:00:04.1986018

exSw 时间可以通过在OrderLineStruct 显式实现GetHashCode 来减少

带有明确的GetHashCode

Total olSw time: 00:00:01.4045676
Total exSw time: 00:00:08.4691066
Total dcSw time: 00:00:03.9439711

没有冗余枚举的总时间变化:
默认GetHashCode Total time: 00:00:18.9649790
显式GetHashCodeTotal time: 00:00:12.7736320

更新:
您也可以通过更改方法逻辑来优化它。

例如,您可以从 totalOrderLines 创建HashSet,然后从中删除项目。

var orderLinesList = totalOrderLines
    ... 
    .ToList();

orderLinesList.ForEach(item => totalOrderLines.Remove(item));

cumulativeCount = totalOrderLinsCount - totalOrderLines.Select(x => x.OrderId).Distinct().Count();

就我而言,它将总时间减少到 7 秒。
Total time: 00:00:07.0851111

在这种情况下,通过TotalOrderLinesDictinct 枚举是一个瓶颈,但它需要O(N) 时间,这没关系。

【讨论】:

  • 您有比使用 except 更快的方法来比较两个列表并选择差异吗?
  • 恐怕不行。根据这个答案stackoverflow.com/a/2799543/3872935Except 方法已经足够好了。
  • 您可以从totalOrderLines 创建HashSet,然后从集合orderLinesList.ForEach(item =&gt; totalOrderLines.Remove(item)); 中删除项目,而不是使用Except。它会工作得更快,但这是一个合乎逻辑的变化。
【解决方案3】:

我建议更改 LINQ 查询的这一部分

totalOrderLines.Where(myRows => productStore.Contains(myRows.Sku))

到一个加入这样阅读:

totalOrderLines.Join(productStore, myRows => myRows.Sku, p => p, (myRows, p) => myRows)

这样您只需支付一次费用,而不是让 Contains 遍历您的产品商店列表 7,000 次,这是非常低效的。此外,如果可以使您的 id 成为整数数据类型(int、long)而不是字符串,那么您也应该有更快的搜索和比较。但我猜你的模型的结构已经设置好了。

【讨论】:

    【解决方案4】:

    totalOrderLines 从何而来?一个 MSSQL 数据库可能吗?如果是这样,您必须在 OrderId 列上有一个索引。在此列上执行不带索引的 Distinct() 会强制数据库引擎遍历所有行以识别不同的值。

    【讨论】:

    • totalOrderLines 是一个 List,它是从数据库查询创建的,因此它与数据库之间没有链接。
    猜你喜欢
    • 2012-10-13
    • 2015-01-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-19
    • 2020-05-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多