Skip to content

Commit

Permalink
Optimize SQL based on parameter nullability
Browse files Browse the repository at this point in the history
This is part of #17543 - Queries really slow due to null checks

Problem was that at the time we performed null semantics and related optimizations we didn't know the value of parameters, so we always had to assume they can be nullable, which resulted in additional IS NULL checks being created.
However, later in the pipeline we know the parameter values and we can optimize out those checks for parameters whose values are not null.
  • Loading branch information
maumar committed Oct 22, 2019
1 parent 0ad9d67 commit 069d334
Show file tree
Hide file tree
Showing 25 changed files with 578 additions and 415 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal
{
public class SqlExpressionOptimizingExpressionVisitor : ExpressionVisitor
{
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly bool _useRelationalNulls;

private static bool TryNegate(ExpressionType expressionType, out ExpressionType result)
Expand All @@ -26,15 +25,18 @@ private static bool TryNegate(ExpressionType expressionType, out ExpressionType
};

result = negated ?? default;

return negated.HasValue;
}

public SqlExpressionOptimizingExpressionVisitor(ISqlExpressionFactory sqlExpressionFactory, bool useRelationalNulls)
{
_sqlExpressionFactory = sqlExpressionFactory;
SqlExpressionFactory = sqlExpressionFactory;
_useRelationalNulls = useRelationalNulls;
}

protected virtual ISqlExpressionFactory SqlExpressionFactory { get; }

protected override Expression VisitExtension(Expression extensionExpression)
=> extensionExpression switch
{
Expand All @@ -43,7 +45,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
_ => base.VisitExtension(extensionExpression),
};

private Expression VisitSqlUnaryExpression(SqlUnaryExpression sqlUnaryExpression)
protected virtual Expression VisitSqlUnaryExpression(SqlUnaryExpression sqlUnaryExpression)
{
if (sqlUnaryExpression.OperatorType == ExpressionType.Not)
{
Expand All @@ -55,15 +57,15 @@ private Expression VisitSqlUnaryExpression(SqlUnaryExpression sqlUnaryExpression
if (sqlUnaryExpression.OperatorType == ExpressionType.Equal
&& sqlUnaryExpression.Operand is SqlConstantExpression innerConstantNull1)
{
return _sqlExpressionFactory.Constant(innerConstantNull1.Value == null, sqlUnaryExpression.TypeMapping);
return SqlExpressionFactory.Constant(innerConstantNull1.Value == null, sqlUnaryExpression.TypeMapping);
}

// NULL IS NOT NULL -> false
// non_nullable_constant IS NOT NULL -> true
if (sqlUnaryExpression.OperatorType == ExpressionType.NotEqual
&& sqlUnaryExpression.Operand is SqlConstantExpression innerConstantNull2)
{
return _sqlExpressionFactory.Constant(innerConstantNull2.Value != null, sqlUnaryExpression.TypeMapping);
return SqlExpressionFactory.Constant(innerConstantNull2.Value != null, sqlUnaryExpression.TypeMapping);
}

if (sqlUnaryExpression.Operand is SqlUnaryExpression innerUnary)
Expand All @@ -72,14 +74,14 @@ private Expression VisitSqlUnaryExpression(SqlUnaryExpression sqlUnaryExpression
if (sqlUnaryExpression.OperatorType == ExpressionType.Equal
&& innerUnary.OperatorType == ExpressionType.Not)
{
return Visit(_sqlExpressionFactory.IsNull(innerUnary.Operand));
return Visit(SqlExpressionFactory.IsNull(innerUnary.Operand));
}

// (!a) IS NOT NULL <==> a IS NOT NULL
if (sqlUnaryExpression.OperatorType == ExpressionType.NotEqual
&& innerUnary.OperatorType == ExpressionType.Not)
{
return Visit(_sqlExpressionFactory.IsNotNull(innerUnary.Operand));
return Visit(SqlExpressionFactory.IsNotNull(innerUnary.Operand));
}
}

Expand All @@ -95,7 +97,7 @@ private Expression VisitNot(SqlUnaryExpression sqlUnaryExpression)
if (sqlUnaryExpression.Operand is SqlConstantExpression innerConstantBool
&& innerConstantBool.Value is bool value)
{
return _sqlExpressionFactory.Constant(!value, sqlUnaryExpression.TypeMapping);
return SqlExpressionFactory.Constant(!value, sqlUnaryExpression.TypeMapping);
}

if (sqlUnaryExpression.Operand is InExpression inExpression)
Expand All @@ -114,13 +116,13 @@ private Expression VisitNot(SqlUnaryExpression sqlUnaryExpression)
if (innerUnary.OperatorType == ExpressionType.Equal)
{
//!(a IS NULL) -> a IS NOT NULL
return Visit(_sqlExpressionFactory.IsNotNull(innerUnary.Operand));
return Visit(SqlExpressionFactory.IsNotNull(innerUnary.Operand));
}

//!(a IS NOT NULL) -> a IS NULL
if (innerUnary.OperatorType == ExpressionType.NotEqual)
{
return Visit(_sqlExpressionFactory.IsNull(innerUnary.Operand));
return Visit(SqlExpressionFactory.IsNull(innerUnary.Operand));
}
}

Expand All @@ -130,12 +132,12 @@ private Expression VisitNot(SqlUnaryExpression sqlUnaryExpression)
if (innerBinary.OperatorType == ExpressionType.AndAlso
|| innerBinary.OperatorType == ExpressionType.OrElse)
{
var newLeft = (SqlExpression)Visit(_sqlExpressionFactory.Not(innerBinary.Left));
var newRight = (SqlExpression)Visit(_sqlExpressionFactory.Not(innerBinary.Right));
var newLeft = (SqlExpression)Visit(SqlExpressionFactory.Not(innerBinary.Left));
var newRight = (SqlExpression)Visit(SqlExpressionFactory.Not(innerBinary.Right));

return innerBinary.OperatorType == ExpressionType.AndAlso
? _sqlExpressionFactory.OrElse(newLeft, newRight)
: _sqlExpressionFactory.AndAlso(newLeft, newRight);
? SqlExpressionFactory.OrElse(newLeft, newRight)
: SqlExpressionFactory.AndAlso(newLeft, newRight);
}

// those optimizations are only valid in 2-value logic
Expand All @@ -145,7 +147,7 @@ private Expression VisitNot(SqlUnaryExpression sqlUnaryExpression)
if (!_useRelationalNulls && TryNegate(innerBinary.OperatorType, out var negated))
{
return Visit(
_sqlExpressionFactory.MakeBinary(
SqlExpressionFactory.MakeBinary(
negated,
innerBinary.Left,
innerBinary.Right,
Expand Down Expand Up @@ -215,7 +217,7 @@ private Expression VisitSqlBinaryExpression(SqlBinaryExpression sqlBinaryExpress
{
return (bool)constant.Value == (sqlBinaryExpression.OperatorType == ExpressionType.Equal)
? binary
: _sqlExpressionFactory.MakeBinary(
: SqlExpressionFactory.MakeBinary(
negated,
sqlBinaryExpression.Left,
sqlBinaryExpression.Right,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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 Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.Query
{
public partial class RelationalShapedQueryCompilingExpressionVisitor
{
private class ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor : SqlExpressionOptimizingExpressionVisitor
{
private readonly IReadOnlyDictionary<string, object> _parametersValues;

public ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor(
ISqlExpressionFactory sqlExpressionFactory,
bool useRelationalNulls,
IReadOnlyDictionary<string, object> parametersValues)
: base(sqlExpressionFactory, useRelationalNulls)
{
_parametersValues = parametersValues;
}

protected override Expression VisitExtension(Expression extensionExpression)
{
if (extensionExpression is SelectExpression selectExpression)
{
var newSelectExpression = (SelectExpression)base.VisitExtension(extensionExpression);

return newSelectExpression.Predicate is SqlConstantExpression newSelectPredicateConstant
&& !(selectExpression.Predicate is SqlConstantExpression)
? newSelectExpression.Update(
newSelectExpression.Projection.ToList(),
newSelectExpression.Tables.ToList(),
SqlExpressionFactory.Equal(
newSelectPredicateConstant,
SqlExpressionFactory.Constant(true, newSelectPredicateConstant.TypeMapping)),
newSelectExpression.GroupBy.ToList(),
newSelectExpression.Having,
newSelectExpression.Orderings.ToList(),
newSelectExpression.Limit,
newSelectExpression.Offset,
newSelectExpression.IsDistinct,
newSelectExpression.Alias)
: newSelectExpression;
}

return base.VisitExtension(extensionExpression);
}

protected override Expression VisitSqlUnaryExpression(SqlUnaryExpression sqlUnaryExpression)
{
var result = base.VisitSqlUnaryExpression(sqlUnaryExpression);
if (result is SqlUnaryExpression newUnaryExpresion
&& newUnaryExpresion.Operand is SqlParameterExpression parameterOperand)
{
var parameterValue = _parametersValues[parameterOperand.Name];
if (sqlUnaryExpression.OperatorType == ExpressionType.Equal)
{
return SqlExpressionFactory.Constant(parameterValue == null, sqlUnaryExpression.TypeMapping);
}

if (sqlUnaryExpression.OperatorType == ExpressionType.NotEqual)
{
return SqlExpressionFactory.Constant(parameterValue != null, sqlUnaryExpression.TypeMapping);
}
}

return result;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,44 @@ private class ParameterValueBasedSelectExpressionOptimizer
{
private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly IParameterNameGeneratorFactory _parameterNameGeneratorFactory;
private readonly bool _useRelationalNulls;

public ParameterValueBasedSelectExpressionOptimizer(
ISqlExpressionFactory sqlExpressionFactory,
IParameterNameGeneratorFactory parameterNameGeneratorFactory)
IParameterNameGeneratorFactory parameterNameGeneratorFactory,
bool useRelationalNulls)
{
_sqlExpressionFactory = sqlExpressionFactory;
_parameterNameGeneratorFactory = parameterNameGeneratorFactory;
_useRelationalNulls = useRelationalNulls;
}

public SelectExpression Optimize(SelectExpression selectExpression, IReadOnlyDictionary<string, object> parametersValues)
public (SelectExpression selectExpression, bool canCache) Optimize(SelectExpression selectExpression, IReadOnlyDictionary<string, object> parametersValues)
{
var query = new InExpressionValuesExpandingExpressionVisitor(
var canCache = true;

var inExpressionOptimized = new InExpressionValuesExpandingExpressionVisitor(
_sqlExpressionFactory, parametersValues).Visit(selectExpression);

query = new FromSqlParameterApplyingExpressionVisitor(
if (!ReferenceEquals(selectExpression, inExpressionOptimized))
{
canCache = false;
}

var nullParametersOptimized = new ParameterNullabilityBasedSqlExpressionOptimizingExpressionVisitor(
_sqlExpressionFactory, _useRelationalNulls, parametersValues).Visit(inExpressionOptimized);

var fromSqlParameterOptimized = new FromSqlParameterApplyingExpressionVisitor(
_sqlExpressionFactory,
_parameterNameGeneratorFactory.Create(),
parametersValues).Visit(query);
parametersValues).Visit(nullParametersOptimized);

if (!ReferenceEquals(nullParametersOptimized, fromSqlParameterOptimized))
{
canCache = false;
}

return (SelectExpression)query;
return (selectExpression: (SelectExpression)fromSqlParameterOptimized, canCache);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,19 @@ public RelationalCommandCache(
ISqlExpressionFactory sqlExpressionFactory,
IParameterNameGeneratorFactory parameterNameGeneratorFactory,
IQuerySqlGeneratorFactory querySqlGeneratorFactory,
bool useRelationalNulls,
SelectExpression selectExpression)
{
_memoryCache = memoryCache;
_sqlExpressionFactory = sqlExpressionFactory;
_parameterNameGeneratorFactory = parameterNameGeneratorFactory;
_querySqlGeneratorFactory = querySqlGeneratorFactory;
_selectExpression = selectExpression;

_parameterValueBasedSelectExpressionOptimizer = new ParameterValueBasedSelectExpressionOptimizer(
_sqlExpressionFactory,
_parameterNameGeneratorFactory);
_parameterNameGeneratorFactory,
useRelationalNulls);
}

public virtual IRelationalCommand GetRelationalCommand(IReadOnlyDictionary<string, object> parameters)
Expand All @@ -55,10 +58,11 @@ public virtual IRelationalCommand GetRelationalCommand(IReadOnlyDictionary<strin

try
{
var selectExpression = _parameterValueBasedSelectExpressionOptimizer.Optimize(_selectExpression, parameters);
relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(selectExpression);
var optimizeResult = _parameterValueBasedSelectExpressionOptimizer.Optimize(_selectExpression, parameters);
var selectExpression =
relationalCommand = _querySqlGeneratorFactory.Create().GetCommand(optimizeResult.selectExpression);

if (ReferenceEquals(selectExpression, _selectExpression))
if (optimizeResult.canCache)
{
_memoryCache.Set(cacheKey, relationalCommand, new MemoryCacheEntryOptions { Size = 10 });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace Microsoft.EntityFrameworkCore.Query
Expand All @@ -16,6 +17,7 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQue
private readonly Type _contextType;
private readonly IDiagnosticsLogger<DbLoggerCategory.Query> _logger;
private readonly ISet<string> _tags;
private readonly bool _useRelationalNulls;

public RelationalShapedQueryCompilingExpressionVisitor(
ShapedQueryCompilingExpressionVisitorDependencies dependencies,
Expand All @@ -28,6 +30,7 @@ public RelationalShapedQueryCompilingExpressionVisitor(
_contextType = queryCompilationContext.ContextType;
_logger = queryCompilationContext.Logger;
_tags = queryCompilationContext.Tags;
_useRelationalNulls = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).UseRelationalNulls;
}

protected virtual RelationalShapedQueryCompilingExpressionVisitorDependencies RelationalDependencies { get; }
Expand Down Expand Up @@ -68,6 +71,7 @@ protected override Expression VisitShapedQueryExpression(ShapedQueryExpression s
RelationalDependencies.SqlExpressionFactory,
RelationalDependencies.ParameterNameGeneratorFactory,
RelationalDependencies.QuerySqlGeneratorFactory,
_useRelationalNulls,
selectExpression);

var shaperLambda = (LambdaExpression)shaper;
Expand Down
57 changes: 57 additions & 0 deletions test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,63 @@ await AssertQuery(
elementSorter: e => e.Id);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Select_null_parameter(bool isAsync)
{
AmmunitionType? ammunitionType = AmmunitionType.Cartridge;

await AssertQuery(
isAsync,
ss => ss.Set<Weapon>()
.Select(
w => new
{
w.Id,
AmmoType = ammunitionType
}),
elementSorter: e => e.Id);

ammunitionType = null;

await AssertQuery(
isAsync,
ss => ss.Set<Weapon>()
.Select(
w => new
{
w.Id,
AmmoType = ammunitionType
}),
elementSorter: e => e.Id);

ammunitionType = AmmunitionType.Shell;

await AssertQuery(
isAsync,
ss => ss.Set<Weapon>()
.Select(
w => new
{
w.Id,
AmmoType = ammunitionType
}),
elementSorter: e => e.Id);

ammunitionType = null;

await AssertQuery(
isAsync,
ss => ss.Set<Weapon>()
.Select(
w => new
{
w.Id,
AmmoType = ammunitionType
}),
elementSorter: e => e.Id);
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Select_ternary_operation_with_boolean(bool isAsync)
Expand Down
Loading

0 comments on commit 069d334

Please sign in to comment.