Skip to content

Commit

Permalink
Query: Optimize certain queries by deduplicating SubQueryExpressions
Browse files Browse the repository at this point in the history
  • Loading branch information
tuespetre committed Mar 13, 2017
1 parent a281331 commit 58a7319
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ private static Expression HandleDefaultIfEmpty(HandlerContext handlerContext)

selectExpression.PushDownSubquery();
selectExpression.ExplodeStarProjection();
selectExpression.ClearOrderBy();

var subquery = selectExpression.Tables.Single();

Expand Down
16 changes: 16 additions & 0 deletions src/EFCore.Specification.Tests/QueryTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7007,6 +7007,22 @@ public virtual void Contains_with_subquery_involving_join_binds_to_correct_table
.Contains(o.OrderID)));
}

[ConditionalFact]
public virtual void Range_variable_subquery_with_FirstOrDefault_is_deduplicated()
{
AssertQuery<Order, OrderDetail>(
(os, ods) =>
from o in os
let d = ods.OrderByDescending(x => x.ProductID).FirstOrDefault(x => x.OrderID == o.OrderID)
select new
{
o.OrderID,
d.ProductID,
d.Quantity,
d.UnitPrice
});
}

private static IEnumerable<TElement> ClientDefaultIfEmpty<TElement>(IEnumerable<TElement> source)
{
return source?.Count() == 0 ? new[] { default(TElement) } : source;
Expand Down
2 changes: 2 additions & 0 deletions src/EFCore/Query/EntityQueryModelVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ protected virtual void OptimizeQueryModel(

_queryOptimizer.Optimize(QueryCompilationContext.QueryAnnotations, queryModel);

new SubQueryDeduplicatingQueryModelVisitor().VisitQueryModel(queryModel);

var entityEqualityRewritingExpressionVisitor
= new EntityEqualityRewritingExpressionVisitor(QueryCompilationContext.Model);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// 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 Remotion.Linq;
using Remotion.Linq.Clauses;
using Remotion.Linq.Clauses.Expressions;
using Remotion.Linq.Clauses.ResultOperators;

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 SubQueryDeduplicatingQueryModelVisitor : QueryModelVisitorBase
{
/// <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(new TransformingQueryModelExpressionVisitor<SubQueryDeduplicatingQueryModelVisitor>(this).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 VisitSelectClause(SelectClause selectClause, QueryModel queryModel)
{
var duplicateSubQueryVisitor = new DuplicateSubQueryExpressionFindingExpressionVisitor();
duplicateSubQueryVisitor.Visit(selectClause.Selector);

var duplicateSubQueryExpressions
= duplicateSubQueryVisitor.SubQueryExpressions
.Where(p => p.Value > 1)
.Select(p => p.Key)
.ToArray();

foreach (var subQueryExpression in duplicateSubQueryExpressions)
{
var finalResultOperator = subQueryExpression.QueryModel.ResultOperators.LastOrDefault();

if (finalResultOperator is FirstResultOperator firstResultOperator
&& firstResultOperator.ReturnDefaultWhenEmpty == true)
{
var newSubQueryModel = subQueryExpression.QueryModel.Clone();

newSubQueryModel.ResultOperators.Remove(newSubQueryModel.ResultOperators.Last());
newSubQueryModel.ResultOperators.Add(new TakeResultOperator(Expression.Constant(1)));
newSubQueryModel.ResultOperators.Add(new DefaultIfEmptyResultOperator(null));
newSubQueryModel.ResultTypeOverride = null;

var newSubQueryExpression = new SubQueryExpression(newSubQueryModel);

var newAdditionalFromClause
= new AdditionalFromClause(
queryModel.GetUniqueIdentfierGenerator().GetUniqueIdentifier("<range>_"),
subQueryExpression.Type,
newSubQueryExpression);

queryModel.BodyClauses.Add(newAdditionalFromClause);

var replacingVisitor
= new RecursiveExpressionReplacingExpressionVisitor(
subQueryExpression,
new QuerySourceReferenceExpression(newAdditionalFromClause));

queryModel.SelectClause.TransformExpressions(replacingVisitor.Visit);
}
}
}

private class DuplicateSubQueryExpressionFindingExpressionVisitor : ExpressionVisitorBase
{
public Dictionary<SubQueryExpression, int> SubQueryExpressions { get; }
= new Dictionary<SubQueryExpression, int>();

public override Expression Visit(Expression node)
{
return base.Visit(node);
}

protected override Expression VisitSubQuery(SubQueryExpression expression)
{
if (SubQueryExpressions.ContainsKey(expression))
{
SubQueryExpressions[expression]++;
}
else
{
SubQueryExpressions[expression] = 1;
}

expression.QueryModel.TransformExpressions(Visit);

return base.VisitSubQuery(expression);
}
}

private class RecursiveExpressionReplacingExpressionVisitor : ExpressionVisitorBase
{
private readonly Expression _targetExpression;
private readonly Expression _replacementExpression;

public RecursiveExpressionReplacingExpressionVisitor(Expression targetExpression, Expression replacementExpression)
{
_targetExpression = targetExpression;
_replacementExpression = replacementExpression;
}

public override Expression Visit(Expression node)
{
if (node == _targetExpression)
{
return _replacementExpression;
}

return base.Visit(node);
}

protected override Expression VisitSubQuery(SubQueryExpression expression)
{
expression.QueryModel.TransformExpressions(Visit);

return base.VisitSubQuery(expression);
}
}
}
}
22 changes: 22 additions & 0 deletions test/EFCore.SqlServer.FunctionalTests/QuerySqlServerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7157,6 +7157,28 @@ FROM [Order Details] AS [od]
Sql);
}

public override void Range_variable_subquery_with_FirstOrDefault_is_deduplicated()
{
base.Range_variable_subquery_with_FirstOrDefault_is_deduplicated();

Assert.StartsWith(
@"SELECT [o].[OrderID], [t0].[ProductID], [t0].[Quantity], [t0].[UnitPrice]
FROM [Orders] AS [o]
CROSS APPLY (
SELECT [t].*
FROM (
SELECT NULL AS [empty]
) AS [empty]
LEFT JOIN (
SELECT TOP(1) [x].*
FROM [Order Details] AS [x]
WHERE [x].[OrderID] = [o].[OrderID]
ORDER BY [x].[ProductID] DESC
) AS [t] ON 1 = 1
) AS [t0]",
Sql);
}

private const string FileLineEnding = @"
";

Expand Down

0 comments on commit 58a7319

Please sign in to comment.