Skip to content

Commit

Permalink
Fix to #9282 - Query: improve include pipeline so it can be reused fo…
Browse files Browse the repository at this point in the history
…r queries projecting composed collection navigations (filters and projections)

This feature optimizes a number of queries that project correlated collections. Previously those would produce N+1 queries. Now, we rewrite queries similarly to how Include pipeline does it, producing only two queries and correlating them on the client.
To enable the feature the inner subquery needs to be wrapped around ToList() or ToArray() call.

Current limitations:
- only works for sync queries,
- child entities are not being tracked,
- no fixup between parent and child,
- doesn't work if the parent query results in a CROSS JOIN,
- doesn't work with result operators (i.e. Skip/Take/Distinct)
- doesn't work if outer query needs client evaluation anywhere outside projection (e.g. order by or filter by NonMapped property)
- doesn't work if inner query is correlated with query two (or more) levels up, (e.g. customers.Select(c => c.Orders.Select(o => o.OrderDetails.Where(od => od.Name == c.Name).ToList()).ToList())
- doesn't work in nested scenarios where the outer collection is streaming (e.g. customers.Select(c => c.Orders.Select(o => o.OrderDetails.Where(od => od.Name != "Foo").ToList())) - to make it work, outer collection must also be wrapped in ToList(). However it is OK to stream inner collection - in that case outer collection will take advantage of the optimization.

Optimization process:

original query:

from c in ctx.Customers
where c.CustomerID != "ALFKI"
orderby c.City descending
select (from o in c.Orders
        where o.OrderID > 100
        orderby o.EmployeeID
        select new { o.OrderID, o.CustomerID }).ToList()

nav rewrite converts it to:

from c in customers
where c.CustomerID != "ALFKI"
order by c.City descending
select
   (from o in orders
    where o.OrderID > 100
    order by o.EmployeeID
    where c.CustomerID ?= o.CustomerID
    select new { o.OrderID, o.CustomerID }).ToList()

which gets rewritten to (simplified):

from c in customers
where c.CustomerID != "ALFKI"
order by c.City desc, c.CustomerID asc
select CorrelateSubquery(
    outerKey: new { c.CustomerID },
    correlationPredicate: (outer, inner) => outer.GetValue(0) == null || inner.GetValue(0) == null ? false : outer.GetValue(0) == inner.GetValue(0)
    correlatedCollectionFactory: () =>
        from o in orders
        where o.OrderID > 100
        join _c in
            from c in customers
            where c.CustomerID != "ALFKI"
            select new { c.City, c.CustomerID }
        on o.CustomerID equals _c.GetValue(1)
        order by _c.GetValue(0) descending, _c.GetValue(1), o.EmployeeID
        select new
        {
            InnerResult = new { o.OrderID, o.CustomerID }
            InnerKey = new { o.CustomerID },
            OriginKey = new { _c.GetValue(1) }
        }).ToList()

CorrelateSubquery is the method that combines results of outer and inner queries. Because order for both queries is the same we can perform only one pass thru inner query.
We use correlation predicate (between outerKey parameter passed to CorrelateSubquery and InnerKey which is part of the final result) to determine whether giver result of the inner query belongs to the outer.
We also remember latest origin key (i.e. PK of the outer, which is not always the same as outer key). If the origin key changes, it means that all inners for that outer have already been encountered.
  • Loading branch information
maumar committed Jan 10, 2018
1 parent 7ee6e3c commit b177872
Show file tree
Hide file tree
Showing 29 changed files with 3,527 additions and 180 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ protected override Expression VisitNew(NewExpression newExpression)
{
Check.NotNull(newExpression, nameof(newExpression));

if (newExpression.Type == typeof(AnonymousObject))
if (newExpression.Type == typeof(AnonymousObject)
|| newExpression.Type == typeof(MaterializedAnonymousObject))
{
var propertyCallExpressions
= ((NewArrayExpression)newExpression.Arguments.Single()).Expressions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,8 @@ var boundExpression
}
}

if (AnonymousObject.IsGetValueExpression(methodCallExpression, out var querySourceReferenceExpression))
if (AnonymousObject.IsGetValueExpression(methodCallExpression, out var querySourceReferenceExpression)
|| MaterializedAnonymousObject.IsGetValueExpression(methodCallExpression, out querySourceReferenceExpression))
{
var selectExpression
= _queryModelVisitor.TryGetQuery(querySourceReferenceExpression.ReferencedQuerySource);
Expand Down Expand Up @@ -863,7 +864,8 @@ var memberBindings
return Expression.Constant(memberBindings);
}
}
else if (expression.Type == typeof(AnonymousObject))
else if (expression.Type == typeof(AnonymousObject)
|| expression.Type == typeof(MaterializedAnonymousObject))
{
var propertyCallExpressions
= ((NewArrayExpression)expression.Arguments.Single()).Expressions;
Expand Down
17 changes: 10 additions & 7 deletions src/EFCore.Relational/Query/Expressions/SelectExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query.Expressions.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.Sql;
using Microsoft.EntityFrameworkCore.Storage;
Expand Down Expand Up @@ -910,13 +911,10 @@ var existingOrdering

private bool OrderingExpressionComparison(Ordering ordering, Expression expressionToMatch)
{
return _expressionEqualityComparer.Equals(ordering.Expression, expressionToMatch)
|| _expressionEqualityComparer.Equals(
UnwrapNullableExpression(ordering.Expression.RemoveConvert()).RemoveConvert(),
expressionToMatch)
|| _expressionEqualityComparer.Equals(
UnwrapNullableExpression(expressionToMatch.RemoveConvert()).RemoveConvert(),
ordering.Expression);
var unwrappedOrderingExpression = UnwrapNullableExpression(ordering.Expression.RemoveConvert()).RemoveConvert();
var unwrappedExpressionToMatch = UnwrapNullableExpression(expressionToMatch.RemoveConvert()).RemoveConvert();

return _expressionEqualityComparer.Equals(unwrappedOrderingExpression, unwrappedExpressionToMatch);
}

private Expression UnwrapNullableExpression(Expression expression)
Expand All @@ -926,6 +924,11 @@ private Expression UnwrapNullableExpression(Expression expression)
return nullableExpression.Operand;
}

if (expression is NullConditionalExpression nullConditionalExpression)
{
return nullConditionalExpression.AccessOperation;
}

return expression;
}

Expand Down
49 changes: 49 additions & 0 deletions src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,54 @@ var sqlOrderingExpression
}
}

/// <summary>
/// Determines whether correlated collections (if any) can be optimized.
/// </summary>
/// <returns>True if optimization is allowed, false otherwise.</returns>
protected override bool CanOptimizeCorrelatedCollections()
{
if (!base.CanOptimizeCorrelatedCollections())
{
return false;
}

if (RequiresClientEval
|| RequiresClientFilter
|| RequiresClientJoin
|| RequiresClientOrderBy
|| RequiresClientSelectMany)
{
return false;
}

var injectParametersFinder = new InjectParametersFindingVisitor(QueryCompilationContext.QueryMethodProvider.InjectParametersMethod);
injectParametersFinder.Visit(Expression);

return !injectParametersFinder.InjectParametersFound;
}

private class InjectParametersFindingVisitor : ExpressionVisitorBase
{
private MethodInfo _injectParametersMethod;

public InjectParametersFindingVisitor(MethodInfo injectParametersMethod)
{
_injectParametersMethod = injectParametersMethod;
}

public bool InjectParametersFound { get; private set; }

protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.MethodIsClosedFormOf(_injectParametersMethod))
{
InjectParametersFound = true;
}

return base.VisitMethodCall(node);
}
}

/// <summary>
/// Visits <see cref="SelectClause" /> nodes.
/// </summary>
Expand Down Expand Up @@ -1449,6 +1497,7 @@ var predicate

var projection
= QueryCompilationContext.QuerySourceRequiresMaterialization(joinClause)
//|| joinClause.ItemType == typeof(MaterializedAnonymousObject)
? innerSelectExpression.Projection
: Enumerable.Empty<Expression>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3516,7 +3516,7 @@ public virtual void Project_collection_navigation_composed()
AssertQuery<Level1>(
l1s => from l1 in l1s
where l1.Id < 3
select new { l1.Id, collection = l1.OneToMany_Optional.Where(l2 => l2.Name != "Foo") },
select new { l1.Id, collection = l1.OneToMany_Optional.Where(l2 => l2.Name != "Foo").ToList() },
elementSorter: e => e.Id,
elementAsserter: (e, a) =>
{
Expand Down
Loading

0 comments on commit b177872

Please sign in to comment.