Search code examples
linqoptimizationlinq-to-objects

Optimizing away OrderBy() when using Any()


So I have a fairly standard LINQ-to-Object setup.

var query = expensiveSrc.Where(x=> x.HasFoo)
                        .OrderBy(y => y.Bar.Count())
                        .Select(z => z.FrobberName);    

// ...

if (!condition && !query.Any())
 return; // seems to enumerate and sort entire enumerable 

// ...

foreach (var item in query)
   // ...

This enumerates everything twice. Which is bad.

var queryFiltered = expensiveSrc.Where(x=> x.HasFoo);

var query = queryFiltered.OrderBy(y => y.Bar.Count())
                         .Select(z => z.FrobberName); 

if (!condition && !queryFiltered.Any())
   return;

// ...

foreach (var item in query)
   // ...

Works, but is there a better way?

Would there be any non-insane way to "enlighten" Any() to bypass the non-required operations? I think I remember this sort of optimisation going into EduLinq.


Solution

  • There is not much information that can be extracted from an enumerable, so maybe it's better to turn the query into an IQueryable? This Any extension method walks down its expression tree skipping all irrelevant operations, then it turns the important branch into a delegate that can be called to obtain an optimized IQueryable. Standard Any method applied to it explicitly to avoid recursion. Not sure about corner cases, and maybe it makes sense to cache compiled queries, but with simple queries like yours it seems to work.

    static class QueryableHelper {
        public static bool Any<T>(this IQueryable<T> source) {
            var e = source.Expression;
            while (e is MethodCallExpression) {
                var mce = e as MethodCallExpression;
                switch (mce.Method.Name) {
                    case "Select":
                    case "OrderBy":
                    case "ThenBy": break;
                    default: goto dun;
                }
                e = mce.Arguments.First();
            }
            dun:
            var d = Expression.Lambda<Func<IQueryable<T>>>(e).Compile();
            return Queryable.Any(d());
        }
    }
    

    Queries themselves must be modified like this:

    var query = expensiveSrc.AsQueryable()
                            .Where(x=> x.HasFoo)
                            .OrderBy(y => y.Bar.Count())
                            .Select(z => z.FrobberName);