-
Notifications
You must be signed in to change notification settings - Fork 2.1k
fix(tesseract): keep time_shift when a pre-aggregation serves the query #11599
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
Changes from all commits
17afb92
1ffbf21
2f86135
f5744b6
a9078f4
628e116
e7e3970
4c2c63b
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 |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ use super::*; | |
| use crate::logical_plan::visitor::{LogicalPlanRewriter, NodeRewriteResult}; | ||
| use crate::logical_plan::*; | ||
| use crate::planner::collectors::{collect_cube_names_from_symbols, has_multi_stage_members}; | ||
| use crate::planner::filter::typed_filter::resolve_base_symbol; | ||
| use crate::planner::filter::FilterItem; | ||
| use crate::planner::filter::FilterOp; | ||
| use crate::planner::join_hints::JoinHints; | ||
|
|
@@ -140,9 +141,13 @@ impl PreAggregationOptimizer { | |
| let external = pre_aggregation.external.unwrap_or(false); | ||
| let date_range = | ||
| Self::extract_date_range(&query.filter(), &self.query_tools, time_shifts, external); | ||
| if let Some(rewritten) = | ||
| self.try_rewrite_simple_query(query, pre_aggregation, date_range, is_user_query)? | ||
| { | ||
| if let Some(rewritten) = self.try_rewrite_simple_query( | ||
| query, | ||
| pre_aggregation, | ||
| date_range, | ||
| is_user_query, | ||
| time_shifts, | ||
| )? { | ||
| return Ok(Some(rewritten)); | ||
| } | ||
| } | ||
|
|
@@ -156,6 +161,7 @@ impl PreAggregationOptimizer { | |
| pre_aggregation: &Rc<CompiledPreAggregation>, | ||
| date_range: Option<(String, String)>, | ||
| is_user_query: bool, | ||
| time_shifts: &TimeShiftState, | ||
| ) -> Result<Option<Rc<Query>>, CubeError> { | ||
| // Row identity for an ungrouped read is judged against the join this | ||
| // very node will render, taken from the node itself rather than | ||
|
|
@@ -174,6 +180,14 @@ impl PreAggregationOptimizer { | |
| pre_aggregation, | ||
| row_grain, | ||
| )? { | ||
| if !Self::can_carry_time_shifts( | ||
| pre_aggregation, | ||
| &matched_measures, | ||
| &Self::read_member_names(&query.schema(), &query.filter()), | ||
| time_shifts, | ||
| ) { | ||
| return Ok(None); | ||
| } | ||
| let source = | ||
| self.make_pre_aggregation_source(pre_aggregation, &matched_measures, date_range)?; | ||
| let new_query = Query::builder() | ||
|
|
@@ -477,6 +491,88 @@ impl PreAggregationOptimizer { | |
| } | ||
| } | ||
|
|
||
| // A stored member is shifted by offsetting its column as a whole, which | ||
| // only reproduces the shifted values when the shift can be attributed to | ||
| // that column. A column built from several members of which just some are | ||
| // shifted has no such offset — moving it would carry along rows the shift | ||
| // must leave in place — and the lookup cannot attribute a shift to it | ||
| // either, so the two agree: whenever a shift is involved but cannot be | ||
| // attributed, the pre-aggregation cannot serve the shifted leaf. | ||
| // | ||
| // Grouping members — time dimensions, dimensions and segments — are | ||
| // substituted by column, so a pre-aggregation can only serve a shifted | ||
| // leaf when every stored member a shift reaches is one whose column can | ||
| // carry that shift. `shift_for_substituted_column` decides that, and the | ||
| // rendering node asks it too, so a member admitted here is one that will | ||
| // actually be offset. | ||
| // | ||
| // A measure column holds an aggregate, and a shift changes which rows | ||
| // feed it rather than the value itself, so no offset applies at all. A | ||
| // stored measure reading a shifted member is therefore always unusable, | ||
| // however cleanly the shift could be attributed to it. Only the measures | ||
| // matching consumed are examined, since the rest are never read. | ||
| // Resolved names of every member the query reads, so a stored member no | ||
| // one reads cannot decide anything. | ||
| fn read_member_names(schema: &LogicalSchema, filter: &LogicalFilter) -> HashSet<String> { | ||
|
Contributor
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. Inserting Moving |
||
| let mut symbols: Vec<Rc<MemberSymbol>> = schema | ||
| .dimensions | ||
| .iter() | ||
| .chain(schema.time_dimensions.iter()) | ||
| .chain(schema.measures.iter()) | ||
| .cloned() | ||
| .collect(); | ||
| for item in filter | ||
| .dimensions_filters | ||
| .iter() | ||
| .chain(filter.time_dimensions_filters.iter()) | ||
| .chain(filter.segments.iter()) | ||
| { | ||
| item.find_all_member_evaluators(&mut symbols); | ||
| } | ||
| symbols | ||
| .into_iter() | ||
| .map(|symbol| { | ||
| resolve_base_symbol(&symbol) | ||
| .resolve_reference_chain() | ||
| .full_name() | ||
| }) | ||
| .collect() | ||
| } | ||
|
|
||
| fn can_carry_time_shifts( | ||
| pre_aggregation: &CompiledPreAggregation, | ||
| matched_measures: &HashSet<String>, | ||
| read_members: &HashSet<String>, | ||
| time_shifts: &TimeShiftState, | ||
| ) -> bool { | ||
| if time_shifts.is_empty() { | ||
| return true; | ||
| } | ||
| let is_read = |member: &Rc<MemberSymbol>| { | ||
| read_members.contains( | ||
| &resolve_base_symbol(member) | ||
| .resolve_reference_chain() | ||
| .full_name(), | ||
| ) | ||
| }; | ||
| let grouping_members_carry_shift = pre_aggregation | ||
| .time_dimensions | ||
| .iter() | ||
| .chain(pre_aggregation.dimensions.iter()) | ||
| .chain(pre_aggregation.segments.iter()) | ||
|
claude[bot] marked this conversation as resolved.
|
||
| .filter(|member| is_read(member)) | ||
| .all(|member| { | ||
| !time_shifts.has_shift_under(member) | ||
| || time_shifts.shift_for_substituted_column(member).is_some() | ||
| }); | ||
| grouping_members_carry_shift | ||
| && pre_aggregation | ||
| .measures | ||
| .iter() | ||
| .filter(|measure| matched_measures.contains(&measure.full_name())) | ||
| .all(|measure| !time_shifts.has_shift_under(measure)) | ||
| } | ||
|
|
||
| fn extract_date_range( | ||
| filter: &LogicalFilter, | ||
| query_tools: &Rc<State>, | ||
|
|
@@ -496,8 +592,7 @@ impl PreAggregationOptimizer { | |
| // Apply time shift for this dimension if present. | ||
| // SQL renders `column + interval`, so actual data range is `date - interval`. | ||
| if let Some(interval) = time_shifts | ||
| .dimensions_shifts | ||
| .get(&base_filter.member_name()) | ||
| .get_for_symbol(base_filter.raw_member_evaluator_ref()) | ||
| .and_then(|s| s.interval.as_ref()) | ||
| { | ||
| let tz = query_tools.timezone(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -287,7 +287,11 @@ impl SqlNodesFactory { | |
| }; | ||
|
|
||
| let input = if !self.time_shifts.is_empty() { | ||
| TimeShiftSqlNode::new(self.time_shifts.clone(), input) | ||
| TimeShiftSqlNode::new( | ||
| self.time_shifts.clone(), | ||
| self.pre_aggregation_dimensions_references.clone(), | ||
| input, | ||
| ) | ||
| } else { | ||
| input | ||
| }; | ||
|
Comment on lines
289
to
297
Contributor
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.
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| cubes: | ||
| - name: pa_customers | ||
| sql: "SELECT * FROM pa_customers" | ||
| joins: | ||
| - name: pa_returns | ||
| relationship: one_to_many | ||
| sql: "{pa_customers}.id = {pa_returns.customer_id}" | ||
| dimensions: | ||
| - name: id | ||
| type: number | ||
| sql: id | ||
| primary_key: true | ||
|
|
||
| # Time dimension derived from another cube's time dimension: | ||
| # not owned by its cube and not a plain reference, so its shift | ||
| # is keyed by the owned member it wraps. | ||
| - name: return_day | ||
| type: time | ||
| sql: "DATE_TRUNC('day', {pa_returns.created_at})" | ||
| measures: | ||
| - name: total_value | ||
| type: sum | ||
| sql: lifetime_value | ||
|
|
||
| - name: total_value_prev_month | ||
| type: number | ||
| sql: "{CUBE.total_value}" | ||
| multi_stage: true | ||
| time_shift: | ||
| - interval: "1 month" | ||
| type: prior | ||
| timeDimension: pa_customers.return_day | ||
|
|
||
| pre_aggregations: | ||
| - name: value_by_return_day_month | ||
| type: rollup | ||
| measures: | ||
| - total_value | ||
| time_dimension: pa_customers.return_day | ||
| granularity: month | ||
|
|
||
| - name: pa_returns | ||
| sql: "SELECT * FROM pa_returns" | ||
| dimensions: | ||
| - name: id | ||
| type: number | ||
| sql: id | ||
| primary_key: true | ||
| - name: customer_id | ||
| type: number | ||
| sql: customer_id | ||
| - name: created_at | ||
| type: time | ||
| sql: created_at | ||
| measures: | ||
| - name: count | ||
| type: count |
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.
read_member_namesis an eagerly-evaluated argument, so it runs beforecan_carry_time_shiftsgets to itstime_shifts.is_empty()early return. Every successful pre-aggregation match therefore walks the full schema plus every filter's member evaluators, callsresolve_base_symbol().resolve_reference_chain()on each, and allocates aStringper member into a freshHashSet— then discards all of it, becausetime_shiftsis empty for essentially every non-multi-stage query and for the whole-query rewrite path at line 104 (&TimeShiftState::default()).Not hot enough to be a real problem (once per match, not per candidate), but it's free to avoid:
(the
is_empty()check insidecan_carry_time_shiftscan stay as the invariant for direct callers).