Skip to content

Commit

Permalink
Query: Lift more subqueries in the QueryOptimizer
Browse files Browse the repository at this point in the history
Query models following a simple pattern of selecting from a subquery
only to create a projection can often be flattened by lifting the
subquery. These changes do that, which results in more efficient
queries for such cases, and in the case of Relational, more compact
SQL statements.
  • Loading branch information
tuespetre committed Mar 10, 2017
1 parent 952af89 commit 4cd141d
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 219 deletions.
3 changes: 3 additions & 0 deletions src/EFCore/Query/EntityQueryModelVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,9 @@ protected virtual void OptimizeQueryModel(

_queryOptimizer.Optimize(QueryCompilationContext.QueryAnnotations, queryModel);

new MainFromClauseFlatteningQueryModelVisitor(QueryCompilationContext.QueryAnnotations)
.VisitQueryModel(queryModel);

var entityEqualityRewritingExpressionVisitor
= new EntityEqualityRewritingExpressionVisitor(QueryCompilationContext.Model);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.ResultOperators;
using Remotion.Linq;
using Remotion.Linq.Clauses;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Clauses.ResultOperators;
using Remotion.Linq.Parsing;

namespace Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal
{
/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public class MainFromClauseFlatteningQueryModelVisitor : QueryModelVisitorBase
{
private readonly IEnumerable<IQueryAnnotation> _queryAnnotations;
private readonly SubQueryExpressionVisitor _subQueryExpressionVisitor;

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public MainFromClauseFlatteningQueryModelVisitor([NotNull] IEnumerable<IQueryAnnotation> queryAnnotations)
{
_queryAnnotations = queryAnnotations;
_subQueryExpressionVisitor = new SubQueryExpressionVisitor(this);
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public override void VisitQueryModel([NotNull] QueryModel queryModel)
{
queryModel.TransformExpressions(_subQueryExpressionVisitor.Visit);

base.VisitQueryModel(queryModel);
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public override void VisitMainFromClause([NotNull] MainFromClause fromClause, [NotNull] QueryModel queryModel)
{
if (fromClause.FromExpression is SubQueryExpression subQueryExpression)
{
var subQueryModel = subQueryExpression.QueryModel;

if (subQueryModel.SelectClause.Selector is QuerySourceReferenceExpression
&& !subQueryModel.ResultOperators.OfType<GroupResultOperator>().Any()
&& !queryModel.BodyClauses.Any())
{
queryModel.UpdateQuerySourceMapping(
queryModel.MainFromClause,
subQueryModel.SelectClause.Selector,
_queryAnnotations);

queryModel.MainFromClause = subQueryModel.MainFromClause;

foreach (var bodyClause in subQueryModel.BodyClauses)
{
queryModel.BodyClauses.Add(bodyClause);
}

foreach (var resultOperator in subQueryModel.ResultOperators.Reverse())
{
queryModel.ResultOperators.Insert(0, resultOperator);
}

return;
}
}
}

private class SubQueryExpressionVisitor : RelinqExpressionVisitor
{
private readonly MainFromClauseFlatteningQueryModelVisitor _queryModelVisitor;

public SubQueryExpressionVisitor(MainFromClauseFlatteningQueryModelVisitor queryModelVisitor)
{
_queryModelVisitor = queryModelVisitor;
}

protected override Expression VisitSubQuery(SubQueryExpression expression)
{
_queryModelVisitor.VisitQueryModel(expression.QueryModel);

return expression;
}
}
}
}
48 changes: 48 additions & 0 deletions src/EFCore/Query/Internal/QueryModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors;
using Microsoft.EntityFrameworkCore.Query.ResultOperators;
using Remotion.Linq;
using Remotion.Linq.Clauses;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Clauses.ExpressionVisitors;

namespace Microsoft.EntityFrameworkCore.Query.Internal
{
Expand All @@ -24,6 +26,52 @@ public static class QueryModelExtensions
public static string Print([NotNull] this QueryModel queryModel, bool removeFormatting = false, int? characterLimit = null)
=> new QueryModelPrinter().Print(queryModel, removeFormatting, characterLimit);

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static void UpdateQuerySourceMapping(
[NotNull] this QueryModel queryModel,
[NotNull] IQuerySource oldQuerySource,
[NotNull] Expression newExpression,
[NotNull] IEnumerable<IQueryAnnotation> queryAnnotations)
{
var querySourceMapping = new QuerySourceMapping();
querySourceMapping.AddMapping(oldQuerySource, newExpression);

queryModel.TransformExpressions(expression =>
ReferenceReplacingExpressionVisitor.ReplaceClauseReferences(
expression,
querySourceMapping,
throwOnUnmappedReferences: false));

if (newExpression is QuerySourceReferenceExpression qsre)
{
foreach (var queryAnnotation in queryAnnotations)
{
if (queryAnnotation.QuerySource == oldQuerySource)
{
queryAnnotation.QuerySource = qsre.ReferencedQuerySource;
queryAnnotation.QueryModel = queryModel;
}
}
}
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
/// </summary>
public static void UpdateQuerySourceMapping(
[NotNull] this QueryModel queryModel,
[NotNull] IQuerySource oldQuerySource,
[NotNull] IQuerySource newQuerySource,
[NotNull] IEnumerable<IQueryAnnotation> queryAnnotations)
=> queryModel.UpdateQuerySourceMapping(
oldQuerySource,
new QuerySourceReferenceExpression(newQuerySource),
queryAnnotations);

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
/// directly from your code. This API may change or be removed in future releases.
Expand Down
101 changes: 33 additions & 68 deletions src/EFCore/Query/Internal/QueryOptimizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using Remotion.Linq;
using Remotion.Linq.Clauses;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Clauses.ExpressionVisitors;
using Remotion.Linq.Clauses.ResultOperators;
using Remotion.Linq.Transformations;

Expand Down Expand Up @@ -102,9 +101,7 @@ public override void VisitJoinClause(JoinClause joinClause, QueryModel queryMode

private void TryFlattenJoin(JoinClause joinClause, QueryModel queryModel)
{
var subQueryExpression = joinClause.InnerSequence as SubQueryExpression;

if (subQueryExpression != null)
if (joinClause.InnerSequence is SubQueryExpression subQueryExpression)
{
VisitQueryModel(subQueryExpression.QueryModel);

Expand Down Expand Up @@ -135,29 +132,21 @@ public override void VisitGroupJoinClause(GroupJoinClause groupJoinClause, Query

// Attempts to rewrite GroupJoin/SelectMany to regular join

var additionalFromClause
= queryModel.BodyClauses.ElementAtOrDefault(index + 1)
as AdditionalFromClause;

var querySourceReferenceExpression
= additionalFromClause?.FromExpression as QuerySourceReferenceExpression;

if (querySourceReferenceExpression != null
&& querySourceReferenceExpression.ReferencedQuerySource == groupJoinClause)
if (queryModel.BodyClauses.ElementAtOrDefault(index + 1) is AdditionalFromClause additionalFromClause
&& additionalFromClause.FromExpression is QuerySourceReferenceExpression qsre
&& qsre.ReferencedQuerySource == groupJoinClause
&& queryModel.CountQuerySourceReferences(groupJoinClause) == 1)
{
if (queryModel.CountQuerySourceReferences(groupJoinClause) == 1)
{
// GroupJoin/SelectMany can be rewritten to regular Join.
// GroupJoin/SelectMany can be rewritten to regular Join.

queryModel.BodyClauses.RemoveAt(index + 1);
queryModel.BodyClauses.RemoveAt(index);
queryModel.BodyClauses.Insert(index, groupJoinClause.JoinClause);
queryModel.BodyClauses.RemoveAt(index + 1);
queryModel.BodyClauses.RemoveAt(index);
queryModel.BodyClauses.Insert(index, groupJoinClause.JoinClause);

UpdateQuerySourceMapping(
queryModel,
additionalFromClause,
new QuerySourceReferenceExpression(groupJoinClause.JoinClause));
}
queryModel.UpdateQuerySourceMapping(
additionalFromClause,
groupJoinClause.JoinClause,
_queryAnnotations);
}
}

Expand All @@ -175,11 +164,10 @@ protected override void FlattenSubQuery(

VisitQueryModel(subQueryModel);

if (subQueryModel.ResultOperators
.All(ro => ro is CastResultOperator)
&& !subQueryModel.BodyClauses.Any(bc => bc is OrderByClause)
|| queryModel.IsIdentityQuery()
&& !queryModel.ResultOperators.Any())
if ((subQueryModel.ResultOperators.All(ro => ro is CastResultOperator)
&& !subQueryModel.BodyClauses.Any(bc => bc is OrderByClause))
|| (queryModel.IsIdentityQuery()
&& !queryModel.ResultOperators.Any()))
{
string itemName;

Expand All @@ -202,10 +190,10 @@ var fromClauseData

fromClause.CopyFromSource(fromClauseData);

UpdateQuerySourceMapping(
queryModel,
queryModel.UpdateQuerySourceMapping(
fromClause,
subQueryExpression.QueryModel.SelectClause.Selector);
subQueryExpression.QueryModel.SelectClause.Selector,
_queryAnnotations);

InsertBodyClauses(subQueryExpression.QueryModel.BodyClauses, queryModel, destinationIndex);

Expand All @@ -214,10 +202,10 @@ var fromClauseData
queryModel.ResultOperators.Insert(0, resultOperator);
}

UpdateQuerySourceMapping(
queryModel,
queryModel.UpdateQuerySourceMapping(
innerMainFromClause,
new QuerySourceReferenceExpression(fromClause));
new QuerySourceReferenceExpression(fromClause),
_queryAnnotations);
}
}

Expand All @@ -240,25 +228,25 @@ public override void VisitResultOperator(ResultOperatorBase resultOperator, Quer
}
}
}

var ofTypeOperator = resultOperator as OfTypeResultOperator;
if (ofTypeOperator != null)

if (resultOperator is OfTypeResultOperator ofTypeOperator)
{
var searchedItemType = ofTypeOperator.SearchedItemType;
if (searchedItemType == queryModel.MainFromClause.ItemType)
if (ofTypeOperator.SearchedItemType == queryModel.MainFromClause.ItemType)
{
queryModel.ResultOperators.RemoveAt(index);
}
else
{
var entityType = _model.FindEntityType(searchedItemType);
var entityType = _model.FindEntityType(ofTypeOperator.SearchedItemType);

if (entityType != null)
{
var oldQuerySource = queryModel.MainFromClause;

var entityQueryProvider
= ((oldQuerySource.FromExpression as ConstantExpression)?.Value as IQueryable)?.Provider as IAsyncQueryProvider;
= ((oldQuerySource.FromExpression as ConstantExpression)
?.Value as IQueryable)
?.Provider as IAsyncQueryProvider;

if (entityQueryProvider != null)
{
Expand All @@ -272,39 +260,16 @@ var newMainFromClause

queryModel.MainFromClause = newMainFromClause;

UpdateQuerySourceMapping(queryModel,
queryModel.UpdateQuerySourceMapping(
oldQuerySource,
new QuerySourceReferenceExpression(newMainFromClause));
newMainFromClause,
_queryAnnotations);
}
}
}
}

base.VisitResultOperator(resultOperator, queryModel, index);
}

private void UpdateQuerySourceMapping(
QueryModel queryModel,
IQuerySource oldQuerySource,
Expression newExpression)
{
var querySourceMapping = new QuerySourceMapping();
querySourceMapping.AddMapping(oldQuerySource, newExpression);

queryModel.TransformExpressions(e =>
ReferenceReplacingExpressionVisitor
.ReplaceClauseReferences(e, querySourceMapping, throwOnUnmappedReferences: false));

var qsre = newExpression as QuerySourceReferenceExpression;
if (qsre != null)
{
var newQuerySource = qsre.ReferencedQuerySource;
foreach (var queryAnnotation in _queryAnnotations.Where(qa => qa.QuerySource == oldQuerySource))
{
queryAnnotation.QuerySource = newQuerySource;
queryAnnotation.QueryModel = queryModel;
}
}
}
}
}
Loading

0 comments on commit 4cd141d

Please sign in to comment.