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

feat(Optimizer): eliminate common sub-expressions #659

Closed
wants to merge 3 commits into from

Conversation

lokax
Copy link
Member

@lokax lokax commented Jun 6, 2022

Signed-off-by: lokax [email protected]

Implement this feature #658 for projection and aggregate.

I removed projection merge rule in LogicalProjection::new(), because we will need a projection below the current projection in some case.

like this:

> explain select l_extendedprice * (1 - l_discount), l_extendedprice * (1 - l_discount) * (1 + l_tax) from lineitem;
PhysicalProjection:
    InputRef #0
    (InputRef #0 * (1 + InputRef #1))
  PhysicalProjection:
      (InputRef #0 * (1 - InputRef #1))
      InputRef #2
    PhysicalTableScan:
        table #64,
        columns [5, 6, 7],
        with_row_handler: false,
        is_sorted: false,
        expr: None

In the above example, it will evaluate (1 - l_discount) 2 times and l_extendedprice * A 2 times before optimization. This would not be a problem as long as the expression itself is not expensive. However, evaluating a expensive scalar function multiple times could incur a performance overhead. After optimization, common sub-expressions are evaluated only once.

@TennyZhuang
Copy link
Collaborator

@st1page PTAL

@TennyZhuang TennyZhuang requested review from st1page and xxchan June 6, 2022 17:04
Comment on lines +14 to +21
impl Rule for ProjectEliminateCSE {
fn apply(&self, plan: PlanRef) -> Result<PlanRef, ()> {
let projection = plan.as_logical_projection()?;
let mut cse_eliminator = CSEEliminator::new();
let mut proj_exprs = projection.project_expressions().to_vec();
// Search Phase
Copy link
Collaborator

Choose a reason for hiding this comment

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

it might be better if we add the projection merge rule here. and we can merge the nest projection input in AggregateEliminateCSE too.

Copy link
Member Author

Choose a reason for hiding this comment

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

it might be better if we add the projection merge rule here. and we can merge the nest projection input in AggregateEliminateCSE too.

Sorry, I don't know what you want to tell me. For simple implementation, it needs multiple consecutive projection operators which play the role of holding temporary variables. In addtition, it seems that consecutive and identical projection operators are produced during column pruning.
Maybe we could:
column pruning --> projection merge(remove identical projection) --> eliminate cse

Copy link
Member

Choose a reason for hiding this comment

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

I think dummy projections (with only InputRefs) are necessary to support CSE for now. To eliminated them, we have to introduce local reference inside operator, or output_indices. cc @st1page

Comment on lines 78 to 79
exprs_maps: Vec<BoundExpr>, // TODO: HashMap
counts: Vec<(usize, bool)>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

please add some comments on the fields too.
And why they are not in the same data structure Vec<(BoundExpr, usize, bool)>? I think even when we use hashmap it should be in the same data structure too, like

struct SubExpr{
  count: usize.
  new_projectionindex: Option<usize>
}

Copy link
Member

Choose a reason for hiding this comment

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

also some comments on the struct to discribe the general idea of the implementation plz!

Copy link
Member Author

Choose a reason for hiding this comment

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

also some comments on the struct to discribe the general idea of the implementation plz!

ok 😇

please add some comments on the fields too. And why they are not in the same data structure Vec<(BoundExpr, usize, bool)>? I think even when we use hashmap it should be in the same data structure too, like

struct SubExpr{
  count: usize.
  new_projectionindex: Option<usize>
}

It looks better :neckbeard:

Comment on lines +70 to +81
enum EliminatorState {
Search,
Collect,
Replace,
End,
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could you please add some comments here? just similar to the comments on the corresponding functions. and also please remember to use /// instead // as rustdoc

Copy link
Member

Choose a reason for hiding this comment

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

I think State seems not necessaray. What about using different structs to do different phases instead of state transition?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think State seems not necessaray. What about using different structs to do different phases instead of state transition?

It seems that expr_maps: HashMap<BoundExpr, SubExpr> will be accessed during the search phase and collect phase. Using diffrent structs to access and modify expr_maps maybe look likes wierd? But Replace state and End state can be removed, they are just to avoid me doing something wrong.

Copy link
Member

Choose a reason for hiding this comment

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

It seems that expr_maps: HashMap<BoundExpr, SubExpr> will be accessed during the search phase and collect phase. Using diffrent structs to access and modify expr_maps maybe look likes wierd?

IMHO it makes sense for each phase to process some data and then pass data to the next phase. Using state machine to model the processing pipeline makes the logic of different tasks mixed together and hard to read (And I didn't fully understand it yet) :(

What about:

/// output balabala, modify balabala
fn eliminate(exprs: &mut Vec<BoundExpr>) -> Option<Vec<BoundExpr>> {
	let expr_maps = search(exprs.iter())?;
	let child_projection_exprs = collect(exprs.iter(), expr_maps);
	rewrite(exprs, child_projection_exprs);
	Some(child_projection_exprs)
}

/// do balabala, output balabala
fn search(exprs: &Vec<BoundExpr>) -> Option<HashMap<BoundExpr, SubExpr>> {
	struct Visitor { expr_maps }
	impl ExprVisitor for Visitor {...}
	let visitor = ...;
	...
    visitor.expr_maps
}

/// do balabala
fn collect(exprs: &Vec<BoundExpr>, subexprs: ...) ->  Vec<BoundExpr> {
	struct Visitor {...}
	impl ExprVisitor for Visitor {...}
	...
}

/// do balabala
fn rewrite(exprs: &mut Vec<BoundExpr>, child_projection_exprs) {
	struct Rewriter {...}
	...
}

}

struct CSEEliminator {
exprs_maps: Vec<BoundExpr>, // TODO: HashMap
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe the hash map is necessary because the matching algorithm seems O(n^2) now. we can do it in future PRs later.

Copy link
Member

Choose a reason for hiding this comment

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

+1 and I think Vec is not simpler?

src/optimizer/rules/eliminate_cse_rule.rs Outdated Show resolved Hide resolved
Comment on lines +70 to +81
enum EliminatorState {
Search,
Collect,
Replace,
End,
}
Copy link
Member

Choose a reason for hiding this comment

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

I think State seems not necessaray. What about using different structs to do different phases instead of state transition?

}

struct CSEEliminator {
exprs_maps: Vec<BoundExpr>, // TODO: HashMap
Copy link
Member

Choose a reason for hiding this comment

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

+1 and I think Vec is not simpler?

Copy link
Member

@xxchan xxchan left a comment

Choose a reason for hiding this comment

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

We need #593! 🥵

@skyzh
Copy link
Member

skyzh commented Jun 7, 2022

We need #593! 🥵

Next time for sure!

@skyzh
Copy link
Member

skyzh commented Jun 9, 2022

I'm working on a new project https://github.com/risinglightdb/sqlplannertest-rs. With planner test, we can merge such optimizer PRs with more confidence!

@skyzh
Copy link
Member

skyzh commented Jun 9, 2022

Hopefully I can finish it this weekend :)

@skyzh
Copy link
Member

skyzh commented Jun 9, 2022

It's here! #661

@skyzh
Copy link
Member

skyzh commented Jun 9, 2022

Please add planner test cases for affected queries after #661 gets merged :)

@lokax
Copy link
Member Author

lokax commented Jun 15, 2022

It's here! #661

Thanks. 😇😇

@lokax
Copy link
Member Author

lokax commented Aug 24, 2022

Some things have been modified.

@lokax lokax requested review from xxchan and st1page August 24, 2022 12:59
Copy link
Member

@xxchan xxchan left a comment

Choose a reason for hiding this comment

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

Please add some planner tests ❤️

Comment on lines +14 to +21
impl Rule for ProjectEliminateCSE {
fn apply(&self, plan: PlanRef) -> Result<PlanRef, ()> {
let projection = plan.as_logical_projection()?;
let mut cse_eliminator = CSEEliminator::new();
let mut proj_exprs = projection.project_expressions().to_vec();
// Search Phase
Copy link
Member

Choose a reason for hiding this comment

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

I think dummy projections (with only InputRefs) are necessary to support CSE for now. To eliminated them, we have to introduce local reference inside operator, or output_indices. cc @st1page


/// This is a helper struct for eliminate common sub-expressions
/// Does not eliminate common aggregate functions
struct CSEEliminator {
Copy link
Member

Choose a reason for hiding this comment

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

Eliminate CS, not Eliminate CSE 😄

Comment on lines +70 to +81
enum EliminatorState {
Search,
Collect,
Replace,
End,
}
Copy link
Member

Choose a reason for hiding this comment

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

It seems that expr_maps: HashMap<BoundExpr, SubExpr> will be accessed during the search phase and collect phase. Using diffrent structs to access and modify expr_maps maybe look likes wierd?

IMHO it makes sense for each phase to process some data and then pass data to the next phase. Using state machine to model the processing pipeline makes the logic of different tasks mixed together and hard to read (And I didn't fully understand it yet) :(

What about:

/// output balabala, modify balabala
fn eliminate(exprs: &mut Vec<BoundExpr>) -> Option<Vec<BoundExpr>> {
	let expr_maps = search(exprs.iter())?;
	let child_projection_exprs = collect(exprs.iter(), expr_maps);
	rewrite(exprs, child_projection_exprs);
	Some(child_projection_exprs)
}

/// do balabala, output balabala
fn search(exprs: &Vec<BoundExpr>) -> Option<HashMap<BoundExpr, SubExpr>> {
	struct Visitor { expr_maps }
	impl ExprVisitor for Visitor {...}
	let visitor = ...;
	...
    visitor.expr_maps
}

/// do balabala
fn collect(exprs: &Vec<BoundExpr>, subexprs: ...) ->  Vec<BoundExpr> {
	struct Visitor {...}
	impl ExprVisitor for Visitor {...}
	...
}

/// do balabala
fn rewrite(exprs: &mut Vec<BoundExpr>, child_projection_exprs) {
	struct Rewriter {...}
	...
}

Comment on lines +59 to +60
-- keep short circuit
EXPLAIN SELECT x + 5 < y AND x + y < 3, x + y FROM test;
Copy link
Member

Choose a reason for hiding this comment

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

Could you elaborate a bit what is "short circuit" here? I think x + y should always be computed, so there's no short circuit?

@lokax
Copy link
Member Author

lokax commented Feb 1, 2023

Thanks for the review. I have no passion to finish it. And the optimizer has been refactored. So I closed this pr.

@lokax lokax closed this Feb 1, 2023
@wangrunji0408
Copy link
Member

Feel free to retry it on the new optimizer if you are interested. It would be easier than this one in my view.
Thank you anyway! 🥰

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.

6 participants