Skip to content
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

Merged
merged 4 commits into from
Jul 3, 2024
Merged

Simplify NOT #34142

merged 4 commits into from
Jul 3, 2024

Conversation

ranma42
Copy link
Contributor

@ranma42 ranma42 commented Jul 2, 2024

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.

@ranma42
Copy link
Contributor Author

ranma42 commented Jul 2, 2024

This is based on top of

and continues the same approach.

@ranma42 ranma42 marked this pull request as ready for review July 2, 2024 21:54
Copy link
Member

@roji roji left a 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
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.)

Copy link
Contributor Author

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

Copy link
Member

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).

Copy link
Contributor Author

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,
Copy link
Member

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)
Copy link
Member

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()

Copy link
Contributor Author

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
Copy link
Member

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);
Copy link
Member

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))
Copy link
Member

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?

Copy link
Contributor Author

@ranma42 ranma42 Jul 3, 2024

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

Copy link
Member

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 (
Copy link
Member

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.

Copy link
Contributor Author

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

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)));
that has the form 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).

Copy link
Member

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.
@roji roji merged commit 97d2365 into dotnet:main Jul 3, 2024
7 checks passed
@ranma42 ranma42 deleted the simplify-not branch July 3, 2024 15:47
@ChrisJollyAU
Copy link
Contributor

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

@roji
Copy link
Member

roji commented Jul 25, 2024

Great to hear @ChrisJollyAU! Hopefully we'll have more like these in the future as the query pipeline improves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants