-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simplify NOT
#34142
Simplify NOT
#34142
Conversation
This is based on top of and continues the same approach. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really, really nice improvement - I'm especially excited about how this unlocks translating via JOIN instead of OUTER/CROSS APPLY in various scenarios... A really nice demonstration of the power of early as opposed to late optimization/simplification.
See mainly nits below, other than that LGTM.
// !(true) -> false | ||
// !(false) -> true | ||
// !(null) -> null | ||
SqlConstantExpression sqlConstantOperand when sqlConstantOperand.Type == typeof(bool) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
SqlConstantExpression sqlConstantOperand when sqlConstantOperand.Type == typeof(bool) | |
SqlConstantExpression { Value: bool boolValue, Type: var type } when type == typeof(bool) | |
=> Constant(!boolValue, typeof(bool), operand.TypeMapping), |
(which is similar to how the check was originally in SqlNullabilityProcessor - any reason you made the change here?)
(in fact, we may want to just remove the check on the Type - if the constant expression's Value is a bool (as is pattern matched), the simplification is probably good regardless of the node's static Type.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will this also work for null
? 🤔
I guess I could handle it separately
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah right, I see. Yeah, I'd do two checks here, one for a constant with a bool value (where the Type doesn't matter), and one for the null case (where the Type obviously does).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah right, I see. Yeah, I'd do two checks here, one for a constant with a bool value (where the Type doesn't matter), and one for the null case (where the Type obviously does).
Fun fact: Type is not relevant for null! 🥳
We can optimize both !null
(boolean negation) and ~null
(binary complement) to null
, as long as we preserve the correct type and typemapping.
|
||
// !(!a) -> a | ||
// ~(~a) -> a (bitwise negation) | ||
SqlUnaryExpression { OperatorType: ExpressionType.Not } sqlUnaryOperand => sqlUnaryOperand.Operand, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'd just use short names like unary, binary (I'm moving in that direction in general)
}; | ||
|
||
// negate all of the results of a CaseExpression | ||
private SqlExpression NotCase(CaseExpression caseExpression) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if we don't inline, move to be a local function of Not()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I unified the Case
methods, which made inlining this much easier
// !(a != b) -> a == b | ||
SqlBinaryExpression { OperatorType: ExpressionType.NotEqual } sqlBinaryOperand => Equal(sqlBinaryOperand.Left, sqlBinaryOperand.Right), | ||
|
||
CaseExpression caseExpression |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment to explain the cases this simplifies?
|
||
return caseExpression.Operand is null | ||
? Case(clauses, newElseResult) | ||
: Case(caseExpression.Operand, clauses, newElseResult); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's some messiness in the Case() methods: the version that accepts operand accepts it as nullable SqlExpression?
, but then immediately proceeds to assume it's not null with the null-forgiving operator.
We could simply allow the operand to be null, at which point it produces a CASE WHEN (just like the overload that doesn't accept an operand at all). At that point we should be able to collapse this conditional expression and inline everything into the switch above (i.e. no need for a function).
// !(a < b) -> a >= b | ||
// !(a <= b) -> a > b | ||
if (sqlUnaryExpression.Operand is SqlBinaryExpression sqlBinaryOperand | ||
&& TryNegate(sqlBinaryOperand.OperatorType, out var negated)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this optimization be inside MakeBinary() like the others?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, it is not valid in C# semantics :(
In SQL !(a < b)
evaluates the same as a >= b
(regardless of nulls)
In C# !(null < b)
evaluates to true
while null >= b
evaluates to false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, thanks!
WHERE [l].[Id] = [l0].[Level1_Optional_Id] AND [l0].[Id] > 0 | ||
ORDER BY [l0].[Id] | ||
) AS [l1] | ||
LEFT JOIN ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting change from OUTER APPLY to LEFT JOIN (and CROSS APPLY to INNER JOIN below) - do you have a clear idea of why this happens?
In general, any change from OUTER/CROSS APPLY to regular JOIN is a significant improvement, since OUTER/CROSS APPLY is correlated and must be calculated for each row, whereas JOIN does not. But am just interested in the exact change here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC the pipeline generates a correlationPredicate
here
efcore/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs
Lines 684 to 694 in 0337960
var correlationPredicate = ReplacingExpressionVisitor.Replace( | |
outerKeySelector.Parameters[0], | |
resultSelector.Parameters[0], | |
Expression.AndAlso( | |
ExpressionExtensions.CreateEqualsExpression( | |
outerKeySelector.Body, | |
Expression.Constant(null), | |
negated: true), | |
ExpressionExtensions.CreateEqualsExpression( | |
outerKeySelector.Body, | |
innerKeySelector.Body))); |
NOT(foo == NULL) AND foo == bar
, which was previously optimized to foo == bar
by the SqlNullabilityProcessor
.
The early optimization transforms the predicate into foo <> NULL AND foo == bar
, which is simplified to foo == bar
by SelectExpression.RemoveRedundantNullChecks()
. I believe that this affects how the apply/join decision is made (but I have not yet delved deep in that direction).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense - and the details are indeed not super important in this context. It's great to see this unlocking optimizations like this.
This makes it easier to have unified paths in transformations.
When constructing `Not` expressions, perform some basic local simplifications. These mostly match `OptimizeNonNullableNotExpression`, but they are written to to be generally applicable, even for nullable expressions.
Most of the optimizations are eagerly performed by the `SqlExpressionFactory`. The remaining ones can now be safely applied regardless of the nullability of the expression.
A quick thanks for this optimization. Especially for those databases that don't support OUTER/CROSS APPLY (Microsoft Access for one), I am seeing a bunch of tests passing that previously didn't |
Great to hear @ChrisJollyAU! Hopefully we'll have more like these in the future as the query pipeline improves. |
When constructing
Not
expressions, perform some basic local simplifications.These mostly match
OptimizeNonNullableNotExpression
, but they are written to to be generally applicable, even for nullable expressions.