【问题标题】:Remove OrderBy from an IQueryable<T>从 IQueryable<T> 中删除 OrderBy
【发布时间】:2012-05-22 09:22:39
【问题描述】:

我有一个分页 API,它返回用户请求的行,但一次只能返回这么多行,而不是整个集合。 API 按设计工作,但我必须计算可用记录的总数(用于正确的页面计算)。在 API 中,我使用 Linq2Sql,并且在最终提出请求之前,我使用 IQueryable 做了很多工作。当我去计数时,我会调用类似:totalRecordCount = queryable.Count();

生成的 SQL 仍然很有趣,但它也添加了不必要的 Order By,这使得查询非常昂贵。

exec sp_executesql N'SELECT COUNT(*) AS [value]
FROM (
    SELECT TOP (1) NULL AS [EMPTY]
    FROM [dbo].[JournalEventsView] AS [t0]
    WHERE [t0].[DataOwnerID] = @p0
    ORDER BY [t0].[DataTimeStamp] DESC
    ) AS [t1]',N'@p0 int',@p0=1

因为我使用的是 IQueryable,所以我可以在 IQueryable 进入 SQL 服务器之前对其进行操作。

我的问题是,如果我已经有一个带有 OrderBy 的 IQueryable,是否可以在调用 Count() 之前删除该 OrderBy?

like: totalRecordCount = queryable.NoOrder.Count();

如果不是,那也没什么大不了的。我看到很多关于如何 OrderBy 的问题,但没有任何涉及从 Linq 表达式中删除 OrderBy 的问题。

谢谢!

【问题讨论】:

  • 你能发布更多你的代码吗?特别是我对您分配给queryable 的查询代码感兴趣。
  • 你总是可以解析表达式树然后从那里删除 orderby

标签: c# .net linq linq-to-sql iqueryable


【解决方案1】:

因此,下面的代码是针对内存数组的尖峰。使用 Entity Framework(或其他一些任意的 IQueryProvider 实现)可能会遇到一些障碍。基本上,我们要做的是访问表达式树并查找任何 Ordering 方法调用,然后将其从树中删除。希望这能为您指明正确的方向。

class Program
{
    static void Main(string[] args)
    {
        var seq = new[] { 1, 3, 5, 7, 9, 2, 4, 6, 8 };

        var query = seq.OrderBy(x => x);

        Console.WriteLine("Print out in reverse order.");
        foreach (var item in query)
        {
            Console.WriteLine(item);
        }

        Console.WriteLine("Prints out in original order");
        var queryExpression = seq.AsQueryable().OrderBy(x => x).ThenByDescending(x => x).Expression;

        var queryDelegate = Expression.Lambda<Func<IEnumerable<int>>>(new OrderByRemover().Visit(queryExpression)).Compile();

        foreach (var item in queryDelegate())
        {
            Console.WriteLine(item);
        }


        Console.ReadLine();
    }
}

public class OrderByRemover : ExpressionVisitor
{
    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        if (node.Method.DeclaringType != typeof(Enumerable) && node.Method.DeclaringType != typeof(Queryable))
            return base.VisitMethodCall(node);

        if (node.Method.Name != "OrderBy" && node.Method.Name != "OrderByDescending" && node.Method.Name != "ThenBy" && node.Method.Name != "ThenByDescending")
            return base.VisitMethodCall(node);

        //eliminate the method call from the expression tree by returning the object of the call.
        return base.Visit(node.Arguments[0]);
    }
}

【讨论】:

  • 仅举个例子,这绝对值得一票。技术上可以为 Linq 创建一个很好的扩展方法。我已经做了我的重构,但这也可能是一个可能的答案。从技术上讲,这比我们上面所做的工作更能回答这个问题。还有人同意吗?我还没有测试过。
【解决方案2】:

不仅有一个不需要的 ORDER BY,还有一个虚假的 TOP(1)。

SELECT TOP (1) NULL AS [EMPTY] ...

该子选择将仅返回 0 或 1 行。事实上,如果没有 TOP,在子选择中使用 ORDER BY 是不合法的。

ORDER BY 子句在视图、内联函数、派生表、子查询和公用表表达式中无效,除非还指定了 TOP 或 FOR XML。:SELECT COUNT(*) FROM (SELECT * FROM Table1 ORDER BY foo)

sqlfiddle

我认为您的 LINQ 可能做错了什么。在调用.Count() 之前,您确定没有在查询中的某处写上.Take(1) 或类似内容吗?

这是错误的:

IQueryable<Foo> foo = (...).OrderBy(x => x.Foo).Take(1);
int count = foo.Count();

你应该这样做:

IQueryable<Foo> foo = (...);
Iqueryable<Foo> topOne = foo.OrderBy(x => x.Foo).Take(1);
int count = foo.Count();

【讨论】:

  • 完全明白你在说什么。我知道它会产生一些奇怪的 sql。但实际上更多的是在寻找问题的答案,而不是关于 SQL 的样子。但是是的......它非常丑陋。
  • 马克说这个查询没有计算任何东西。你确定这是正确的查询吗?它会返回大于 1 的计数吗?
  • 澄清一下,通常没有 Top(1),但在这个例子中,我只从我们的 api 请求了一条记录,它仍然会调用 .Count(),即使计数可以是 1 或 0。通常有一个前 100000 个。Count() 似乎包装了实际的查询结果,但 order by 并不是真正需要的,而且查询成本很高。在计数之前删除 OrderBy 将大大降低 SQL 成本,同时仍然准确地返回要返回的记录总数。希望这可以解决问题,因为乍一看,我会喜欢“wtf是那个人在做”
  • Usr yes -- 仍然返回 1 作为记录集。不争论生成的 linq2sql 代码是很性感的。我只是在寻找一种解决方案,在 .Count() 之前从 IQueryable 中删除 OrderBy。
  • @TravisWhidden:你想数什么?您是否要计算刚刚获得的结果数量?或者如果您获取所有结果而不是仅获取第一个 n,您将获得的结果数量?如果您想要后者,请使用我在答案中发布的代码。如果您想要前者,那么既然您已经从数据库中获取结果,那么最简单(并且性能最好)的方法就是在内存中对它们进行计数,而不是向数据库发送新查询。
【解决方案3】:

恐怕没有简单的方法可以从可查询中删除 OrderBy 运算符。

但是,您可以做的是根据重写 queryable.Expression(see here) 获得的新表达式重新创建 IQueryable,省略 OrderBy 调用。

【讨论】:

    【解决方案4】:

    如果您无法消除根本原因,这里有一个解决方法:

    totalRecordCount = queryable.OrderBy(x => 0).Count();
    

    SQL Server 的查询优化器将删除这种无用的排序。它不会产生运行时成本。

    【讨论】:

    • 我试了一下,但没有运气。我希望它会删除 IQueryable 的 OrderBy 和 OrderByDescending 表达式。 count = queryResults.OrderBy(x => 0).OrderByDescending(x => 0).Count();但结果仍然包含 Order By 子句。通过传递一个 OrderBy,它应该替换现有的 OrderBy 权限,而不是错开它们吧?
    • 我认为添加新的OrderBy 子句不会让旧的子句消失。 .OrderBy(x =&gt; x.Foo).OrderBy(x =&gt; x.Bar) 给出与.OrderBy(x =&gt; x.Bar).ThenBy(y =&gt; y.Foo) 相同的结果。此外,你没有那个,你有.OrderBy(x =&gt; x.Foo).Take(1).OrderBy(x =&gt; 0)
    • 是的,Linq2Sql 确实省略了 x => 0 但保留了原来的 orderby。我认为在表达式中添加额外的 OrderBy 必须使其类似于 ThenBy。这个解决方案听起来很优雅/很老套,但它没有用:(
    • 这确实会错开排序,但查询优化器会删除它们。查看查询计划以确认这一点。添加 10 个 order-bys,您将看不到 10 个排序。这种通用方法有时是解决 L2S 限制的好方法。只需依靠优化器来消除废话。
    • 我有一个返回 IQueryable 的工厂类,并且在该工厂方法中我提供了默认排序顺序 (finalQuery = finalQuery.OrderByDescending(dataEvent => dataEvent.DataTimeStamp))。在调用工厂方法然后添加额外的排序后,例如 (x => 0),它仍然在最终的 sql 输出中保持原来的 OrderByDescending 表达式。我正在使用 SQL Server Profiler 进行验证。我可以说它没有将它们从查询计划中删除。之前执行查询需要 12 秒,在我删除所有 OrderBy 之后需要 300 毫秒。
    【解决方案5】:

    我认为您错误地实现了分页代码。您实际上需要查询数据库两次,一次用于分页数据源,一次用于总行数。这就是设置的外观。

    public IList<MyObj> GetPagedData(string filter, string sort, int skip, int take)
    {
       using(var db = new DataContext())
       {
          var q = GetDataInternal(db);
          if(!String.IsNullOrEmpty(filter))
             q = q.Where(filter); //Using Dynamic linq
    
          if(!String.IsNullOrEmpty(sort))
             q = q.OrderBy(sort); //And here
    
          return q.Skip(skip).Take(take).ToList();
       }
    }
    
    public int GetTotalCount(string filter)
    {
        using(var db = new DataContext())
        {
           var q = GetDataInternal(db);
           if(!String.IsNullOrEmpty(filter))
             q = q.Where(filter); //Using Dynamic linq
    
           return q.Count(); //Without ordering and paging.
        }
    }
    
    private static IQuerable<MyObj> GetDataInternal(DataContext db)
    {
       return 
            from x in db.JournalEventsView 
            where ...
            select new ...;
    }
    

    过滤和排序是使用Dynamic linq library完成的

    【讨论】:

    • 感谢您的意见。我确实查询了两次。 Count() 用于页数,但我对结果集使用相同的可查询。我稍后在执行 Skip, Take 时使用它: var queryResultsList = orderdResults.Skip((result.ReturnValue.CurrentPage) * result.ReturnValue.RecordsPerPage).Take(result.ReturnValue.RecordsPerPage).ToList();
    • 从您问题中生成的 sql 代码看来,您正在对 IQuerable 对象执行 Count() 之后 OrderByskip/take 被应用跨度>
    • 不,计数是在 orderby 之后但在 skip/take 之前执行的。这是我想删除 orderby 的主要原因,但我只是按照上面的建议创建了两个 IQueryable,一个有,一个没有排序。
    • @TravisWhidden 类似于tblUsers.OrderBy (u =&gt; u.fkCompanyID).Count() linq 的 linq 语句只会从生成的 SQL 代码中删除 orderby:SELECT COUNT(*) AS [value] FROM [dbo].[tblUser] AS [t0]
    • 我昨天说错了。我忘记了查询允许的编码“最大”可能结果。也就是说,最多可以返回“x”条记录或更少,但永远不会更多。计数发生在 (Take) 之后,因此会有正确计算的页面。这就是为什么它导致了如此丑陋的 sql。
    【解决方案6】:

    我知道这不是您要查找的内容,但是包含 DataTimeStamp 的 [DataOwnerID] 上的索引可以降低您的查询成本。

    【讨论】:

      猜你喜欢
      • 2023-03-14
      • 2011-02-25
      • 2010-09-07
      • 2012-01-22
      • 2020-12-22
      • 2010-10-17
      • 2017-05-27
      相关资源
      最近更新 更多