Удалить OrderBy из IQueryable <T>

У меня есть пейджинговый API, который возвращает строки, запрашиваемые пользователем, но только так много за один раз, а не всю коллекцию. API работает так, как планировалось, но мне нужно рассчитать общее количество доступных записей (для правильных расчётов страниц). В API я использую Linq2Sql, и я много работаю с IQueryable, прежде чем я, наконец, сделаю свои запросы. Когда я иду, чтобы получить счет, я вызываю что-то вроде: totalRecordCount = queryable.Count();

Получающийся SQL интересен тем не менее, но он также добавляет ненужный порядок, который делает запрос очень дорогостоящим.

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.

Мой вопрос: если у меня уже есть IQueryable с OrderBy в нем, можно ли удалить этот OrderBy, прежде чем я вызову Count()?

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

Если нет, не biggie. Я вижу много вопросов, как OrderBy, но не связанных с удалением OrderBy из выражения Linq.

Спасибо!

Ответ 1

Существует не только ненужный 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. Вы уверены, что не написали .Take(1) или что-то подобное в своем запросе, прежде чем называть .Count()?

Это неправильно:

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();

Ответ 2

Таким образом, приведенный ниже код является всплеском против массива в памяти. Там могут быть некоторые препятствия, чтобы получить эту работу с 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]);
    }
}

Ответ 3

Если вы не можете устранить основную причину, вот обходной путь:

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

Оптимизатор запросов SQL Server удалит этот бесполезный порядок. Он не будет иметь затраты времени исполнения.

Ответ 4

Я боюсь, что нет простого способа удалить оператор OrderBy из запроса.

Однако вы можете воссоздать IQueryable на основе нового выражения, полученного при перезаписи queryable.Expression (см. здесь), опустив вызов OrderBy.

Ответ 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 ...;
}

Фильтрация и сортировка выполняются с помощью динамической библиотеки linq

Ответ 6

Я знаю, что это не совсем то, что вы ищете, но индекс на [DataOwnerID] с включением DataTimeStamp может сделать ваш запрос менее дорогостоящим.