Simplify NOT#34142
Conversation
|
This is based on top of and continues the same approach. |
roji
left a comment
There was a problem hiding this comment.
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.
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.
will this also work for null? 🤔
I guess I could handle it separately
There was a problem hiding this comment.
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.
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.
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.
nit: if we don't inline, move to be a local function of Not()
There was a problem hiding this comment.
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.
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.
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.
Can this optimization be inside MakeBinary() like the others?
There was a problem hiding this comment.
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
| 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.
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.
IIUC the pipeline generates a correlationPredicate here
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.
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
Notexpressions, perform some basic local simplifications.These mostly match
OptimizeNonNullableNotExpression, but they are written to to be generally applicable, even for nullable expressions.