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

TransformUp is now sensitive to tree modifications #867

Merged
merged 13 commits into from
Apr 1, 2022

Conversation

max-hoffman
Copy link
Contributor

@max-hoffman max-hoffman commented Mar 12, 2022

TransformUp and related node/expression DFS helper functions expect the
visit functions to return an additional boolean parameter indicating
whether the visit changed the node:

type TransformNodeFunc func(Node) (Node, bool, error)
type TransformExprFunc func(Expression) (Expression, bool, error)
type Transformer func(TransformContext) (sql.Node, bool, error)

TransformUp's implementation uses the modification information to avoid
re-creating a node with identical children:

BenchmarkTransformOld
BenchmarkTransformOld-12          	  396544	      2782 ns/op	    3000 B/op	      51 allocs/op
BenchmarkTransformOldNoEdit
BenchmarkTransformOldNoEdit-12    	  407797	      2731 ns/op	    2936 B/op	      50 allocs/op
BenchmarkTransformNew
BenchmarkTransformNew-12          	 4584258	       254.1 ns/op	      96 B/op	       5 allocs/op
BenchmarkTransformNewNoEdit
BenchmarkTransformNewNoEdit-12       	 4782098	       237.8 ns/op	      96 B/op	       5 allocs/op

We use plan.InspectUp when possible, and then plan.TransformUp
where possible, resorting to the more expensive plan.TransformUpCtx
and plan.TransformUpCtxSchema only when necessary.

@max-hoffman max-hoffman changed the title Minimize TransformUp mallocs TransformUp is now sensitive to tree modifications Mar 14, 2022
@max-hoffman max-hoffman marked this pull request as ready for review March 14, 2022 15:59
@max-hoffman max-hoffman requested a review from zachmu March 14, 2022 15:59
Copy link
Member

@zachmu zachmu left a comment

Choose a reason for hiding this comment

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

Love the performance results, happy you did this work.

How mad would you be if I made you rewrite this to return a sentinel error type instead of changing the return signature of Transform? I think that's a better pattern because a) fewer return params, b) makes it harder to mistakenly under-analyze because a buggy rule returned the wrong boolean. Over-analyzing means we take longer than we should, under-analyzing means we return incorrect results. Basically I think the signal "I made no changes" should be more explicit and harder to send by accident.

See related comments about helper methods being exported inappropriately.

e, ok := n.(sql.Expressioner)
if !ok {
return n, nil
func TransformUpHelper(node sql.Node, f sql.TransformNodeFunc) (sql.Node, bool, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Why exported?

if _, ok := paramNames[strings.ToLower(e.Name())]; ok {
return expression.NewProcedureParam(e.Name()), nil
func resolveProcedureParamsTransform(ctx *sql.Context, paramNames map[string]struct{}, n sql.Node) (sql.Node, bool, error) {
return plan.TransformUpHelper(n, func(n sql.Node) (sql.Node, bool, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Big red flag here

The question you should be asking is: why doesn't TransformExpressionsUpWithNode work here on its own?

cur = n
for i := 0; i < maxCteDepth && !nodesEqual(prev, cur); i++ {
for i := 0; i < maxCteDepth && mod; i++ {
Copy link
Member

Choose a reason for hiding this comment

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

Not sure about this, it makes it possible for an analyzer rule to misbehave and claim it didn't change anything when it actually did.

On the other hand, not calling DeepEquals on every node is probably a huge performance savings.

I think I would be more comfortable getting rid of DeepEquals here if a rule had to opt-in to saying it changed nothing, rather than opt-in to saying something changed.

`TransformUp` and related node/expression DFS helper functions expect the
visit function to return an additional parameter indicating
whether the visit changed the node: `sql.SameTree` or `sql.NewTree`.

We use `plan.InspectUp` when possible, and then `plan.TransformUp` where
possible, resorting to the more expensive `plan.TransformUpCtx` and
`plan.TransformUpCtxSchema` only when necessary.
@zachmu
Copy link
Member

zachmu commented Mar 28, 2022

This getting close to ready for review?

Needs conflicts resolved

@max-hoffman
Copy link
Contributor Author

@zachmu it's next on my list after prepared statements. I spent 4/5 days last week on index issues, this keeps getting deprioritized

@max-hoffman max-hoffman requested a review from zachmu March 31, 2022 16:59
Copy link
Member

@zachmu zachmu left a comment

Choose a reason for hiding this comment

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

This looks really good in general, a lot of comments but overall sound

sql/analyzer/aggregations.go Outdated Show resolved Hide resolved
sql/analyzer/analyzer.go Show resolved Hide resolved
return nil, err
return nil, transform.SameTree, err
}
if same {
Copy link
Member

Choose a reason for hiding this comment

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

A lot of places you have this block of code could be simplified:

return node, same, err

No need to special-case same, or the err from WithExpressions or WithChildren, just return all of it

Copy link
Contributor Author

@max-hoffman max-hoffman Apr 1, 2022

Choose a reason for hiding this comment

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

if we're always creating a new node, doesn't that defeat the purpose of minimizing allocs?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah on second thought this is a dumb comment in the general case

There are a couple spots you might apply it, but not here. Don't call a With method if you don't have to.

}

// pushdownIndexToTable attempts to push the index given down to the table given, if it implements
// sql.IndexAddressableTable
func pushdownIndexToTable(a *Analyzer, tableNode NameableNode, index sql.Index, keyExpr []sql.Expression) (sql.Node, error) {
return plan.TransformUp(tableNode, func(n sql.Node) (sql.Node, error) {
func pushdownIndexToTable(a *Analyzer, tableNode NameableNode, index sql.Index, keyExpr []sql.Expression) (sql.Node, transform.TreeIdentity, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Why is this here instead of in pushdown.go?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the function is only referenced in this file. it's not filter pushdown, just applying an index

sql/analyzer/assign_info_schema.go Outdated Show resolved Hide resolved
sql/transform/node.go Show resolved Hide resolved
sql/transform/node.go Outdated Show resolved Hide resolved
sql/transform/node.go Show resolved Hide resolved
sql/transform/node.go Outdated Show resolved Hide resolved
sql/transform/node.go Show resolved Hide resolved
@zachmu
Copy link
Member

zachmu commented Apr 1, 2022

Also I forgot to mention this last night, but this would make a really good blog post.

@max-hoffman max-hoffman merged commit 24320fe into main Apr 1, 2022
@max-hoffman max-hoffman deleted the max/reduce-transform-mallocs branch April 1, 2022 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants