-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
sql: fix the subquery limit optimization #34801
Conversation
@RaduBerinde @andy-kimball can you make a suggestion as to how/where I can introduce the same limit in the CBO? I kinda half-guess this can be introduced super-early (as soon as |
Yeah, I would try to do it in |
0f248e8
to
58c4cac
Compare
58c4cac
to
eb29d13
Compare
@RaduBerinde @andy-kimball I have introduced the rules, with a property to prevent double application as you recommended. I am happy to see that the limit propagates, however I found this interesting case:
Notice how the limit does not get propagated down in that case. I think the reason is the ordering of the rules:
Of course I can't simply swap the select and limit elimination rules, because then we get in the opposite problem. What do you think? Would this warrant moving the limit and select elimination to xforms? |
eb29d13
to
14cce3d
Compare
There are existing cases like that already (see limit file in execbuilder testdata). The reason is that opt doesn't have a scan+select operator. The filter gets pushed in the scannode during execbuild. We could probably have special code to push the limit in too, but there is little benefit - it's all the same when it gets converted to distsql. |
does the physical planner also push limits down to scans? |
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.
Reviewable status: complete! 0 of 0 LGTMs obtained (waiting on @jordanlewis, @knz, and @RaduBerinde)
pkg/sql/opt/norm/custom_funcs.go, line 1256 at r1 (raw file):
// applying twice. func (c *CustomFuncs) SetSubqueryLimited(sub *memo.SubqueryPrivate) *memo.SubqueryPrivate { sub.SubqueryLimited = true
We never mutate private structs once they've been constructed, because it interferes with interning. It's similar to updating an object's key after putting it into a hash table. opt
trees must always be immutable after construction.
Instead, follow this pattern (used elsewhere):
func (c *CustomFuncs) MakeLimitedSubquery(sub *memo.SubqueryPrivate) *memo.SubqueryPrivate {
newSub := *sub
newSub.SubqueryLimited = true
return &newSub
}
pkg/sql/opt/norm/rules/limit.opt, line 16 at r1 (raw file):
# operator is supported - in that case it can be definitely worthwhile # pushing down a LIMIT 2 to limit the amount of work done on every row.) [Max1RowLimitScan, Normalize]
I think this rule is unnecessary and we should remove. If there is more than 1 row, then it's an error. So there's no need to optimize that code path. In the non-error case, the returned result will always have at most 1 row. Adding a Limit
may even slow down execution a bit.
pkg/sql/opt/norm/rules/scalar.opt, line 178 at r1 (raw file):
# operator is supported - in that case it can be definitely worthwhile # pushing down a LIMIT 1 to limit the amount of work done on every row.) [ExistsLimit, Normalize]
NIT: Call this rule IntroduceExistsLimit
, or similar, since all our rule names begin with a verb.
Also, can you add commentary explaining how/why you're marking the private to avoid double pushdown?
pkg/sql/opt/norm/rules/scalar.opt, line 183 at r1 (raw file):
$subqueryPrivate:* & ^(IsSubqueryLimited $subqueryPrivate)) => ((OpName)
NIT: Our convention is to only use (OpName)
when there's more than one choice. So this should just be Exists
.
pkg/sql/opt/ops/scalar.opt, line 40 at r1 (raw file):
[Private] define SubqueryPrivate { OriginalExpr Subquery
NIT: Do you mind converting spaces to tabs in this declaration since you're touching this anyway?
pkg/sql/opt/ops/scalar.opt, line 51 at r1 (raw file):
Cmp Operator SubqueryLimited bool
This could use comment, since it's not obvious what it's for. This establishes a new pattern that we want to have documented.
NIT: Call this WasLimited
, and we can use that naming as a convention if we follow this pattern elsewhere.
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.
Thanks for diving into this. I'm coming around to the idea of using the "mark bit" to avoid double pushdown. In more complex cases, RuleProps
are needed instead, but in simple cases like this where we always want to push down, and always want to do that exactly once, then this is easy way to do that.
Reviewable status: complete! 0 of 0 LGTMs obtained (waiting on @jordanlewis, @knz, and @RaduBerinde)
The physical planner effectively pushes limits into any processor (all processors support Limit as part of post-processing). |
14cce3d
to
9ae1253
Compare
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.
Reviewable status: complete! 0 of 0 LGTMs obtained (waiting on @andy-kimball, @jordanlewis, and @RaduBerinde)
pkg/sql/opt/norm/custom_funcs.go, line 1256 at r1 (raw file):
Previously, andy-kimball (Andy Kimball) wrote…
We never mutate private structs once they've been constructed, because it interferes with interning. It's similar to updating an object's key after putting it into a hash table.
opt
trees must always be immutable after construction.Instead, follow this pattern (used elsewhere):
func (c *CustomFuncs) MakeLimitedSubquery(sub *memo.SubqueryPrivate) *memo.SubqueryPrivate { newSub := *sub newSub.SubqueryLimited = true return &newSub }
Done.
pkg/sql/opt/norm/rules/limit.opt, line 16 at r1 (raw file):
Previously, andy-kimball (Andy Kimball) wrote…
I think this rule is unnecessary and we should remove. If there is more than 1 row, then it's an error. So there's no need to optimize that code path. In the non-error case, the returned result will always have at most 1 row. Adding a
Limit
may even slow down execution a bit.
Done.
pkg/sql/opt/norm/rules/scalar.opt, line 183 at r1 (raw file):
Previously, andy-kimball (Andy Kimball) wrote…
NIT: Our convention is to only use
(OpName)
when there's more than one choice. So this should just beExists
.
Done.
pkg/sql/opt/ops/scalar.opt, line 40 at r1 (raw file):
Previously, andy-kimball (Andy Kimball) wrote…
NIT: Do you mind converting spaces to tabs in this declaration since you're touching this anyway?
Done.
pkg/sql/opt/ops/scalar.opt, line 51 at r1 (raw file):
Previously, andy-kimball (Andy Kimball) wrote…
This could use comment, since it's not obvious what it's for. This establishes a new pattern that we want to have documented.
NIT: Call this
WasLimited
, and we can use that naming as a convention if we follow this pattern elsewhere.
Done.
36c9c00
to
71a772e
Compare
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.
Reviewable status: complete! 0 of 0 LGTMs obtained (waiting on @andy-kimball, @jordanlewis, @knz, and @RaduBerinde)
pkg/sql/opt/norm/rules/limit.opt, line 16 at r1 (raw file):
Previously, knz (kena) wrote…
Done.
NIT: You can just revert all changes to this file.
pkg/sql/opt/norm/rules/scalar.opt, line 183 at r1 (raw file):
Previously, knz (kena) wrote…
Done.
NIT: The trailing paren should go on a separate line. Don't worry about this if you're already passing CI, though.
pkg/sql/opt/ops/scalar.opt, line 51 at r1 (raw file):
Previously, knz (kena) wrote…
Done.
NIT: ndicates => indicates
71a772e
to
eab6b3f
Compare
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.
Reviewable status: complete! 0 of 0 LGTMs obtained (waiting on @andy-kimball, @jordanlewis, and @RaduBerinde)
pkg/sql/opt/norm/rules/limit.opt, line 16 at r1 (raw file):
Previously, andy-kimball (Andy Kimball) wrote…
NIT: You can just revert all changes to this file.
Done.
pkg/sql/opt/ops/scalar.opt, line 51 at r1 (raw file):
Previously, andy-kimball (Andy Kimball) wrote…
NIT: ndicates => indicates
Done.
eab6b3f
to
74c4d7d
Compare
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.
Reviewable status: complete! 0 of 0 LGTMs obtained (waiting on @andy-kimball, @jordanlewis, and @RaduBerinde)
pkg/sql/opt/norm/rules/scalar.opt, line 183 at r1 (raw file):
Previously, andy-kimball (Andy Kimball) wrote…
NIT: The trailing paren should go on a separate line. Don't worry about this if you're already passing CI, though.
Done.
74c4d7d
to
c399c88
Compare
TFYRs! bors r+ |
Build failed |
A while ago the HP was equipped with an optimization: when a subquery is planned for EXISTS or "max 1 row" (scalar context), a LIMIT is applied on its data source. This ensures that the data source does not fetch more rows than strictly necessary to determine the subquery result: - for EXISTS, only 0 or 1 row are needed to decide the boolean; - for scalar contexts, only 0, 1 or 2 rows are needed to decide the outcome 0 or 2 yield an error, only 1 gets a valid result. This optimization was temporarily broken for the scalar case when `max1row` was introduced (when local exec was subsumed by distsql), because the limit was remaining "on top" of `max1row` and not propagated down. This patch places it "under" so it gets propagated again. Release note (performance improvement): subqueries used with EXISTS or as a scalar value now avoid fetching more rows than needed to decide the outcome.
c399c88
to
462d3aa
Compare
That was a merge skew with a concurrently introduced test. Retrying. bors r+ |
34801: sql: fix the subquery limit optimization r=knz a=knz Found this while investigating #32054. A while ago the HP was equipped with an optimization: when a subquery is planned for EXISTS or "max 1 row" (scalar context), a LIMIT is applied on its data source. This ensures that the data source does not fetch more rows than strictly necessary to determine the subquery result: - for EXISTS, only 0 or 1 row are needed to decide the boolean; - for scalar contexts, only 0, 1 or 2 rows are needed to decide the outcome 0 or 2 yield an error, only 1 gets a valid result. This optimization was temporarily broken for the scalar case when `max1row` was introduced (when local exec was subsumed by distsql), because the limit was remaining "on top" of `max1row` and not propagated down. This patch places it "under" so it gets propagated again. Release note (performance improvement): subqueries used with EXISTS or as a scalar value now avoid fetching more rows than needed to decide the outcome. Co-authored-by: Raphael 'kena' Poss <[email protected]>
Build succeeded |
Found this while investigating #32054.
A while ago the HP was equipped with an optimization: when a subquery
is planned for EXISTS or "max 1 row" (scalar context), a LIMIT is
applied on its data source. This ensures that the data source does not
fetch more rows than strictly necessary to determine the subquery
result:
outcome 0 or 2 yield an error, only 1 gets a valid result.
This optimization was temporarily broken for the scalar case when
max1row
was introduced (when local exec was subsumed by distsql),because the limit was remaining "on top" of
max1row
and notpropagated down. This patch places it "under" so it gets propagated
again.
Release note (performance improvement): subqueries used with EXISTS or
as a scalar value now avoid fetching more rows than needed to decide
the outcome.