-
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
Handle IN optimizer transformation of ordering to constant (take 2) #16422
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
// 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; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Linq.Expressions; | ||
|
@@ -15,12 +16,15 @@ private class InExpressionValuesExpandingExpressionVisitor : ExpressionVisitor | |
{ | ||
private readonly ISqlExpressionFactory _sqlExpressionFactory; | ||
private IReadOnlyDictionary<string, object> _parametersValues; | ||
private ConstantExpressionIdentifyingExpressionVisitor _constantIdentifyingVisitor; | ||
private bool _innerExpressionChangedToConstant; | ||
|
||
public InExpressionValuesExpandingExpressionVisitor( | ||
ISqlExpressionFactory sqlExpressionFactory, IReadOnlyDictionary<string, object> parametersValues) | ||
{ | ||
_sqlExpressionFactory = sqlExpressionFactory; | ||
_parametersValues = parametersValues; | ||
_constantIdentifyingVisitor = new ConstantExpressionIdentifyingExpressionVisitor(); | ||
} | ||
|
||
public override Expression Visit(Expression expression) | ||
|
@@ -34,6 +38,7 @@ public override Expression Visit(Expression expression) | |
|
||
switch (inExpression.Values) | ||
{ | ||
// TODO: This shouldn't be here - should be part of compilation instead (#16375) | ||
case SqlConstantExpression sqlConstant: | ||
{ | ||
typeMapping = sqlConstant.TypeMapping; | ||
|
@@ -91,6 +96,7 @@ public override Expression Visit(Expression expression) | |
|
||
if (updatedInExpression == null && nullCheckExpression == null) | ||
{ | ||
_innerExpressionChangedToConstant = true; | ||
return _sqlExpressionFactory.Equal(_sqlExpressionFactory.Constant(true), _sqlExpressionFactory.Constant(inExpression.Negated)); | ||
} | ||
|
||
|
@@ -99,6 +105,56 @@ public override Expression Visit(Expression expression) | |
|
||
return base.Visit(expression); | ||
} | ||
|
||
protected override Expression VisitExtension(Expression extensionExpression) | ||
{ | ||
var visited = base.VisitExtension(extensionExpression); | ||
if (_innerExpressionChangedToConstant && visited is OrderingExpression orderingExpression) | ||
{ | ||
// Our rewriting of InExpression above may have left an ordering which contains only constants - | ||
// invalid in SqlServer. If so, rewrite the entire ordering to a single constant expression which | ||
// QuerySqlGenerator will recognize and render as (SELECT 1). | ||
_innerExpressionChangedToConstant = false; | ||
_constantIdentifyingVisitor.Reset(); | ||
roji marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_constantIdentifyingVisitor.Visit(orderingExpression); | ||
return _constantIdentifyingVisitor.WasConstant | ||
? new OrderingExpression( | ||
new SqlConstantExpression(Expression.Constant(1), _sqlExpressionFactory.FindMapping(typeof(int))), | ||
true) | ||
: orderingExpression; | ||
} | ||
|
||
return visited; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Searches an expression tree for the first occurrence of a node meeting the given criteria, and returns that node. | ||
/// </summary> | ||
private class ConstantExpressionIdentifyingExpressionVisitor : ExpressionVisitor | ||
{ | ||
public bool WasConstant { get; private set; } | ||
|
||
/// <summary> | ||
/// Resets the visitor, making it ready to run again. | ||
/// </summary> | ||
public void Reset() => WasConstant = true; | ||
|
||
public override Expression Visit(Expression node) | ||
{ | ||
// TODO: can be optimized by immediately returning null when a matching node is found (but must be handled everywhere...) | ||
|
||
switch (node) | ||
{ | ||
case ColumnExpression _: | ||
case SqlParameterExpression _: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SqlServer does not allow ordering by parameter either. This may work in current tests but seems like a hacky solution.
Let's do it right way by simplifying expression. Or we can leave the PR unmerged for now and revisit it later closer to preview8 deadline. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, so: 😖 In any case, thinking about it some more, I think the previous logic in #16380 may have actually been more correct. Ordering by COUNT(*) is very similar to ordering by a constant - if an ordering doesn't reference the current row in any way, it is basically meaningless, isn't it? In fact, QuerySqlGenerator.GenerateOrderings already elides SqlParameterExpression just like SqlConstantExpression. So why not have the same logic here, and always simplify an ordering that contains a ParameterExpression (but no ColumnExpression) to (We should try hard to avoid simplifying the expression unless absolutely necessary, as this will be both non-trivial to write and also have to run in runtime). Note that COUNT() over a related entity (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Not when you have a Group by clause.
Let's not do premature optimization here. I would prefer to be on the side of being correct than being fast. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good point - but I don't think it's a fundamental thing here. An OrderBy() after GroupBy() still must refer to the columns of the table in some way - and that can be our trigger for not reducing it to constant. I just did a quick test, and this actually works well with the current state of this PR, since OrderBy() is generated with an SqlFragment for the star in COUNT(*): var list = new List<string>();
return AssertQueryScalar<Order>(isAsync, os => os
.GroupBy(o => o.CustomerID)
.OrderBy(g => (list.Contains(g.Key) ? 1 : 0) + g.Count())
.Select(g => g.Count())); SELECT COUNT(*)
FROM [Orders] AS [o]
GROUP BY [o].[CustomerID]
ORDER BY CASE
WHEN CAST(1 AS bit) = CAST(0 AS bit) THEN 1
ELSE 0
END + COUNT(*) So unless I'm mistaken, the approach of looking for references to the row's columns should be OK - let me know if you see another problematic case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no good logical reason why SqlFragmentExpression is allow-able like ColumnExpression. Yes it works for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The point is that we don't know, and will never be able to know, what is inside an SqlFragment - no matter how complete an analysis we do. So either we:
The only theoretical alternative is if we start to somehow transfer additional information with the SqlFragment, which allows us to understand what's in it. That simply seems unjustified for just these edge cases. To my mind that is good logical reasoning (I actually excepted SqlFragment before looking at |
||
case SqlFragmentExpression _: // May or may not be constant, but better to error than to give wrong results | ||
WasConstant = false; | ||
break; | ||
} | ||
|
||
return base.Visit(node); | ||
} | ||
} | ||
} | ||
} |
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.
Few design things (no need to address right now)
visited.Equals(extensionExpression)
. If it is changed, it will be different and reconstructed.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.
The idea here was to limit the check to only the case where when we rewrite the expression into a constant - not all changes inside the ordering mean that we need to perform this (relatively expensive) check. This might be a somewhat unneeded optimization, let me know what you think.
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.
Possibly un-needed. We can also do reference equality due to immutability and reconstruction. It should give same result.
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 may be misunderstanding, but once again there are rewritings that this visitor performs which do not require our check. For example, if there's a null in the values list, the visitor adds null semantics - if we do a reference equality check it will show as modified, although there's no need to check whether it was converted to constant.
It may not be important to skip these cases perf-wise, but then again the stateful flag approach seems easy and safe enough, and allows us to check only when we know we've performed a "dangerous" rewrite.
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.
States are dangerous. No need of premature optimization.
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.
We have state in a lot of visitors around the pipeline, this is outside of the compilation phase (so perf-sensitive), and the statefulness is extremely limited.
But if you're deadset against it I can remove it.