【问题标题】:Improving performance of condtitional query inside LINQ enumerator提高 LINQ 枚举器中条件查询的性能
【发布时间】:2012-10-09 14:25:20
【问题描述】:

我有一些看起来像这样的代码,使用 .NET 3.5(客户要求):

void Process (ActionState state)
{
    var orderItemQuery = from OrderItem item in order.OrderItems
                           orderby item.OrderLineNumber ascending
                           select item;

    foreach (OrderItem item in orderItemQuery)
    {
        ActionData actionData;
        switch (state)
        {
            case ActionState.Prepare:
                actionData = (
                    from ActionData ad in db.ActionDataTable
                      where ad.ObjectId == item.ProductId
                      select ad
                ).First();

            case ActionState.QualityCheck:
                actionData = (
                    from ActionData ad in db.ActionDataTable
                      where ad.ObjectId == item.OrderItemId
                      select ad
                ).First();

            default:
                throw new InvalidOperationException();
        }

        // ...
    }
}

本质上,第一个查询的结果是迭代的,并且基于当前的ActionState,使用特定的外键从数据库中获取ActionData 记录。实际上,这里和那里有更多的嵌套和一些检查,但本质上是相同的。

这最初对测试数据库非常有效,但一位客户刚刚向我发送了一份他们的实时数据的副本,而且速度非常慢。运行整个批次大约需要 15 分钟,包括一些其他处理。通过性能测试器运行代码后,我发现整个过程中最慢的部分是每个案例的.First() 调用。

如果这是纯 SQL,我会根据 state 的值在循环外编译一个存储过程,然后使用它。既然我做不到,那还有什么办法呢?如何加快速度?

【问题讨论】:

  • 请注意,实际上并不是 .First() 是性能问题,而是 Linq 语句在 .First() 执行之前不会执行。所以实际的性能影响可能在 Linq 语句本身。
  • Linq 语句在 foreach 上执行。所以它向 sql server 发送了 100500 个请求(每个订单一个请求)。

标签: c# performance linq linq-to-sql


【解决方案1】:

您的问题是您的代码对 db 执行了 100500 个请求。你需要在一个请求中得到你想要的。为此,您需要编写正确的 linq 查询。像这样:

if(state!=ActionState.Prepare&&state!=ActionState.QualityCheck)
    throw new InvalidOperationException();
var orderSelector = state == ActionState.Prepare
                             ?o=>o.ProductId
                             :?o=>o.OrderItemId
var orderWithActionQuery  = order.OrderItems.Orderby(o => o.OrderLineNumber).GroupJoin(
        db.ActionDataTable,
        orderSelector,
        ad => ad.ObjectId,
        (x, y) => new { item = x, actionDatas = y })
        .Select(c => new {item = c.item, actionData = c.actionDatas.FirstOrDefault()});    
foreach(var orderWithActionData in orderWithActionQuery)
{
     //orderWithActionData.item is orderItem
     //orderWithActionData.actionData is ation data of this item
}

在此代码中,对数据库的请求将按照 foreach 发送。这将是单个请求,性能会很好

【讨论】:

  • 这看起来不错。唯一我不太明白的是为什么循环在actionData 上运行,因为每个OrderItem 只有一个ActionData 条目。你能解释一下这是如何工作的吗?
  • actionDataQuery 包含所有订单的 ActionData。相当于sql中的inner join
  • 但是我仍然需要在循环中查询订单记录......这不是把问题从里到外吗?
  • actionDataQuery 检索所有操作数据,每个订单项目一个。我认为这是一个好的方向,但是它缺少First() 部分,如果您只想要第一个并且每个订单项目可能有多个操作数据,那么该部分就丢失了。
  • 你只需要投影:select new { Item = item, Data = ad },假设你正在做两个表的连接。如果这是您的问题,我可以尝试写一个答案。
【解决方案2】:

您可能想要分析您的查询。我怀疑您遇到了 n+1 问题,因为每次调用 .First 时都会点击您的数据库。由于您在循环中执行此操作,因此您的数据库可能会看到过多的命中。评估循环外的状态,并在没有循环的情况下根据状态设置单独的查询。

【讨论】:

  • 是的,这就是我想做的,但我不知道如何在循环之外生成查询。
【解决方案3】:

您可能会发现这非常有效:

不要将您的 Select 放入 For 循环中,而是生成一个 OrderItemIds 数组并使用 Array.Contains();

void Process (ActionState state)
{
    var orderItemQuery = from OrderItem item in order.OrderItems
                           orderby item.OrderLineNumber ascending
                           select item;

    IEnumerable<ActionData> actionDatas = null;

    if (state ==  ActionState.Prepare)
    {
        var productIds = orderItemQuery.Select(o => o.ProductId).ToArray();
        actionDatas = db.ActionDataTable.Where(a => productIds.Contains(a.ObjectId));
    }
    else if(state == ActionState.QualityCheck)
    {
        var orderItemIds = orderItemQuery.Select(o => o.OrderItemId).ToArray();
        actionDatas = db.ActionDataTable.Where(a => orderItemIds.Contains(a.ObjectId));
    }
    else
    {
        throw new InvalidOperationException();
    }

        // Do stuff against the complete list of actionDatas without requerying.
    }
}

【讨论】:

  • 这看起来可以工作,但它依赖于将所有结果保存在内存中。对于某些客户,可能有几百万行。
  • 没有更多信息,我不知道如何提供帮助。也许 db View 会更有用。利用联接可以帮助缩小结果集的范围。如果您有其他条件,您可以通过遍历每个条件来缩小查询范围。如果您有上限和下限,则可以使用 Skip() 和 Take() 来限制每个查询中的结果数量。 HTH
【解决方案4】:

问题是您为循环中的每个项目调用数据库。
您应该将这些查询放在一个查询中。
这样,查询应该作为对数据库的一个大型查询执行,并且应该从 SQL 服务器随时获取结果。

您可以通过使用 SQL Profiler 轻松发现问题并查看大量 select x,y,z,... from ActionData 查询。

此选项将 OrderItem 和 ActionData 都选择为包含这两个值的临时匿名类型。

void Process (ActionState state)
{
    var orderItemQuery = from OrderItem item in order.OrderItems
                           orderby item.OrderLineNumber ascending
                           select item;
    var orderItemWithActionDataQuery = 
                from item in orderItemQuery
                select new{Item = item,
                           ActionData = (
                    from ActionData ad in db.ActionDataTable
                      where ad.ObjectId == item.ProductId
                      select ad
                ).First()};

        switch (state)
        {
            case ActionState.Prepare:
                // done
                break;

            case ActionState.QualityCheck:
                orderItemWithActionDataQuery = 
                from item in orderItemQuery
                select new{Item = item,
                           ActionData = (
                    from ActionData ad in db.ActionDataTable
                      where ad.ObjectId == item.OrderItemId
                      select ad
                ).First()};

            default:
                throw new InvalidOperationException();
        }



    foreach (var combinedItem in orderItemWithActionDataQuery)
    {
        OrderItem item = combinedItem.Item;
        ActionData actionData = combinedItem.ActionData;

        // ...
    }
}

【讨论】:

  • 我不明白这将如何工作。此代码无法编译。提供item 的循环在哪里?即使您包含循环,我们还是回到了在循环内切换查询的原始问题。
  • 抱歉,忘记了循环变量,现在修复了。查询在循环外切换。
猜你喜欢
  • 1970-01-01
  • 2012-07-09
  • 1970-01-01
  • 2012-08-12
  • 1970-01-01
  • 1970-01-01
  • 2016-01-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多