从IQueryable<T>中删除OrderBy

13

我有一个分页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,所以我可以在它到达SQL服务器之前操作IQueryable。

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

例如:totalRecordCount = queryable.NoOrder.Count();

如果不能,也没关系。 我看到很多关于如何OrderBy的问题,但没有涉及从Linq表达式中删除OrderBy的。

谢谢!


2
你能发更多的代码吗?特别是我对分配给queryable的查询代码感兴趣。 - Mark Byers
你可以始终解析表达式树,然后从中删除orderby。 - dbarnes
6个回答

10

所以,以下代码是针对内存数组的一个探针。使用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创建一个很好的扩展方法。我已经进行了重构,但这也可能是解决这个问题的一个可能答案。从技术上讲,这比我们上面所做的变通方法更能回答这个问题。还有其他人同意吗?我还没有测试过它。 - TravisWhidden

6

这里不仅有不必要的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的外观。但是没错...它确实很丑陋。 - TravisWhidden
马克说这个查询没有计算任何东西。你确定这是正确的查询吗?它返回大于1的计数吗? - usr
用户是的 - 仍然返回1作为记录集。我并不争论生成的linq2sql代码是否性感。我只是在寻找一种解决方案,在.Count()之前从IQueryable中删除OrderBy。 - TravisWhidden
1
@TravisWhidden:你想要计算什么?你是想要计算你刚刚得到的结果数量吗?还是如果你获取了所有结果而不仅仅是前n个,你将会得到的结果数量?如果你想要后者,请使用我在答案中发布的代码。如果你想要前者,那么既然你已经从数据库中获取了结果,最简单(且性能最佳)的方法就是在内存中对它们进行计数,而不是向数据库发送新的查询。 - Mark Byers
@MarkByers 这似乎是一个可以接受的答案。这正是我想做的,但我想看看是否有更优雅的方法。到目前为止,使用简单的调用(如usr)似乎不可能实现。我会再等一会儿,以防有人有更好的解决方案,但肯定会点赞。感谢您的帮助。 - TravisWhidden
显示剩余2条评论

3

恐怕没有简单的方法可以从可查询对象中删除OrderBy操作符。

然而,您可以基于重写 queryable.Expression参见此处)获取的新表达式来重新创建 IQueryable,省略OrderBy调用。


2

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

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

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


我尝试了一下,但没有成功。我希望它能够从IQueryable中删除OrderBy和OrderByDescending表达式。count = queryResults.OrderBy(x => 0).OrderByDescending(x => 0).Count();但结果仍然包含Order By子句。通过传递OrderBy,它应该替换现有的OrderBy,而不是使它们错开,对吧? - TravisWhidden
1
我认为添加新的“OrderBy”子句不会使旧子句消失。“.OrderBy(x = > x.Foo).OrderBy(x => x.Bar)”和“.OrderBy(x => x.Bar).ThenBy(y => y.Foo)”给出了相同的结果。此外,您没有这样的情况,“.OrderBy(x => x.Foo).Take(1).OrderBy(x => 0)”。 - Mark Byers
是的,Linq2Sql确实省略了x => 0,但保留了原始的orderby。我认为将额外的OrderBy添加到表达式中必须使其类似于ThenBy。尽管这种解决方案听起来很优雅/巧妙,但它并没有起作用:( - TravisWhidden
这确实会使order-by语句错位,但查询优化器会将它们删除。查看查询计划以确认此操作。添加10个order-by语句,您将不会看到10个排序。这种一般性的方法有时是解决L2S限制的好方法。只需依靠优化器来清除垃圾即可。 - usr
我有一个工厂类,返回一个IQueryable<T>对象,在这个工厂方法中,我提供了一个默认的排序方式(finalQuery = finalQuery.OrderByDescending(dataEvent => dataEvent.DataTimeStamp))。在调用工厂方法后,我添加了额外的排序条件,比如(x => 0),但最终的SQL输出仍然保留了原始的OrderByDescending表达式。我通过SQL Server Profiler进行验证,可以看出它没有将它们从查询计划中移除。在移除所有的OrderBy之前,查询执行时间为12秒,而在移除之后,只需300毫秒。 - TravisWhidden
+1 for "look at the query plan". TravisWhidden: http://msdn.microsoft.com/en-us/library/ms191194.aspx+1 为“查看查询计划”。TravisWhidden:http://msdn.microsoft.com/en-us/library/ms191194.aspx - Amy B

0

我知道这不完全是你要找的,但在[DataOwnerID]上建立索引并包含DataTimeStamp可能会使你的查询更加经济高效。


0

我认为你的分页代码实现有误。实际上,你需要查询数据库两次,一次是为了获取分页数据源,另一次是为了获取总行数。以下是正确的设置方式。

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 ...;
}

使用动态 Linq 库进行筛选和排序。


感谢输入。实际上,我查询两次。Count() 是用于页面计数的,但我在结果集中使用同样的可查询对象。之后当我执行 Skip、Take 时也会使用它:var queryResultsList = orderdResults.Skip((result.ReturnValue.CurrentPage) * result.ReturnValue.RecordsPerPage).Take(result.ReturnValue.RecordsPerPage).ToList(); - TravisWhidden
从你的问题生成的 SQL 代码来看,似乎是在 OrderByskip/take 应用之后对 IQuerable 对象进行了 Count() - Magnus
计数是在排序之后,跳过/取出之前执行的。这就是我想删除排序的主要原因,但是我按照上面建议的方法创建了两个IQueryable,一个带排序,一个不带排序。 - TravisWhidden
@TravisWhidden 一个类似于这样的Linq语句 tblUsers.OrderBy (u => u.fkCompanyID).Count() Linq会从生成的SQL代码中删除orderby:SELECT COUNT(*) AS [value] FROM [dbo].[tblUser] AS [t0] - Magnus
我昨天说的话是错误的。我忘记了查询中有一个编码的“Max”可能结果限制。也就是说,返回的记录数可以是最大值“x”,也可以更少,但绝不会更多。计数发生在(Take)之后,因此页面会被正确地计算。这就是为什么它会导致如此丑陋的SQL语句。 - TravisWhidden
显示剩余3条评论

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