Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,19 @@ impl QueryProperties {
.add_filters(&self.dimensions_filters)
.add_filters(&self.segments)
.build(&self.all_used_measures()?)?;
MultiFactJoinGroups::try_new(self.query_tools.clone(), measures_join_hints)
// An ungrouped query returns raw rows rather than aggregates, so the
// replication the wider join tree introduces would reach the result
// directly. A pre-aggregation query describes the rollup to build, and
// matching compares its groups against the query's, so both sides have
// to be grouped the same way.
if self.ungrouped || self.pre_aggregation_query {
MultiFactJoinGroups::try_new(self.query_tools.clone(), measures_join_hints)
} else {
MultiFactJoinGroups::try_new_merging_nested(
self.query_tools.clone(),
measures_join_hints,
)
}
}

fn multi_fact_join_groups(&self) -> Result<&MultiFactJoinGroups, CubeError> {
Expand Down
14 changes: 14 additions & 0 deletions rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ pub struct JoinKey {
joins: Vec<JoinItemStatic>,
}

impl JoinKey {
/// Whether all of this key's joins appear in `other`, which walks from the
/// same root and holds strictly more of them.
///
/// Containment of the edge set, not of a path: `other` may extend this key
/// along a sibling branch rather than along the same walk. That is still a
/// tree this key's rows survive in, which is all the caller needs.
pub fn is_nested_in(&self, other: &JoinKey) -> bool {
self.root == other.root
&& self.joins.len() < other.joins.len()
&& self.joins.iter().all(|item| other.joins.contains(item))
}
}

pub struct QueryTools {
cube_evaluator: Rc<dyn CubeEvaluator>,
base_tools: Rc<dyn BaseTools>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ impl AggregationType {
matches!(self, Self::CountDistinct | Self::CountDistinctApprox)
}

/// Whether feeding a row more than once leaves the result unchanged.
///
/// A distinct count collapses repeats by definition, and a minimum or a
/// maximum does not move when a value it has already seen arrives again.
/// `sum`, `avg`, `count` and `numberAgg` all count every row they are
/// given, so a repeated row shows up in the answer.
///
/// Not the same question as `is_additive`, which asks whether partial
/// results can be rolled up further: `sum` is additive but sensitive to
/// repeats, `countDistinct` is insensitive to them but not additive.
pub fn is_duplicate_insensitive(&self) -> bool {
matches!(
self,
Self::Min | Self::Max | Self::CountDistinct | Self::CountDistinctApprox
)
}

pub fn as_str(&self) -> &'static str {
match self {
Self::Sum => "sum",
Expand Down Expand Up @@ -125,6 +142,17 @@ mod tests {
assert!(!AggregationType::NumberAgg.is_additive());
}

#[test]
fn test_is_duplicate_insensitive() {
assert!(AggregationType::Min.is_duplicate_insensitive());
assert!(AggregationType::Max.is_duplicate_insensitive());
assert!(AggregationType::CountDistinct.is_duplicate_insensitive());
assert!(AggregationType::CountDistinctApprox.is_duplicate_insensitive());
assert!(!AggregationType::Sum.is_duplicate_insensitive());
assert!(!AggregationType::Avg.is_duplicate_insensitive());
assert!(!AggregationType::NumberAgg.is_duplicate_insensitive());
}

#[test]
fn test_is_distinct() {
assert!(AggregationType::CountDistinct.is_distinct());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,24 @@ impl MeasureKind {
| Self::Rank => None,
}
}

/// Whether the value this kind computes is unchanged when the rows it
/// reads are each replicated some number of times — and computed by the
/// same SQL either way.
///
/// Neither narrower nor wider than `regular_in_multiplied`, which answers a
/// different question: a key-based count is safe under multiplication too,
/// but only once switched to the distinct `MultipliedCount` form, so it
/// does not qualify here; a minimum or a maximum needs no such switch and
/// qualifies, though that predicate turns it down.
pub fn survives_row_multiplication(&self) -> bool {
match self {
Self::Aggregated(a) | Self::AggregatedState(a) => {
a.agg_type().is_duplicate_insensitive()
}
Self::Count(_) | Self::MultipliedCount(_) | Self::Calculated(_) | Self::Rank => false,
}
}
}

impl SymbolDeps for MeasureKind {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
cubes:
- name: sites
sql: "SELECT * FROM sites"
joins:
- name: carts
relationship: one_to_many
sql: "{sites}.id = {carts.site_id}"
dimensions:
- name: id
type: number
sql: id
primary_key: true
- name: country
type: string
sql: country
measures:
- name: count
type: count

- name: carts
sql: "SELECT * FROM carts"
joins:
- name: checkouts
relationship: one_to_many
sql: "{carts}.id = {checkouts.cart_id}"
dimensions:
- name: id
type: number
sql: id
primary_key: true
- name: site_id
type: number
sql: site_id
- name: msid
type: string
sql: msid
measures:
- name: unique_msid
type: count_distinct
sql: msid
- name: count
type: count
- name: total_value
type: sum
sql: value
- name: max_value
type: max
sql: value
- name: min_value
type: min
sql: value
- name: avg_value
type: avg
sql: value

- name: checkouts
sql: "SELECT * FROM checkouts"
dimensions:
- name: id
type: number
sql: id
primary_key: true
- name: cart_id
type: number
sql: cart_id
- name: msid
type: string
sql: msid
measures:
- name: unique_msid
type: count_distinct
sql: msid
- name: total_amount
type: sum
sql: amount
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
cubes:
- name: sites
sql: "SELECT * FROM sites"
joins:
- name: carts
relationship: one_to_many
sql: "{sites}.id = {carts.site_id}"
dimensions:
- name: id
type: number
sql: id
primary_key: true
- name: country
type: string
sql: country
measures:
- name: count
type: count

- name: carts
sql: "SELECT * FROM carts"
pre_aggregations:
- name: carts_by_country
type: rollup
measures:
- unique_msid
dimensions:
- sites.country
joins:
- name: checkouts
relationship: one_to_many
sql: "{carts}.id = {checkouts.cart_id}"
dimensions:
- name: id
type: number
sql: id
primary_key: true
- name: site_id
type: number
sql: site_id
- name: msid
type: string
sql: msid
measures:
- name: unique_msid
type: count_distinct
sql: msid
- name: count
type: count
- name: total_value
type: sum
sql: value
- name: max_value
type: max
sql: value
- name: min_value
type: min
sql: value
- name: avg_value
type: avg
sql: value

- name: checkouts
sql: "SELECT * FROM checkouts"
pre_aggregations:
- name: checkouts_by_country
type: rollup
measures:
- unique_msid
dimensions:
- sites.country
dimensions:
- name: id
type: number
sql: id
primary_key: true
- name: cart_id
type: number
sql: cart_id
- name: msid
type: string
sql: msid
measures:
- name: unique_msid
type: count_distinct
sql: msid
- name: total_amount
type: sum
sql: amount
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
DROP TABLE IF EXISTS checkouts CASCADE;
DROP TABLE IF EXISTS carts CASCADE;
DROP TABLE IF EXISTS sites CASCADE;

CREATE TABLE sites (
id INTEGER PRIMARY KEY,
country TEXT NOT NULL
);

CREATE TABLE carts (
id INTEGER PRIMARY KEY,
site_id INTEGER NOT NULL REFERENCES sites(id),
msid TEXT NOT NULL,
value NUMERIC(10, 2) NOT NULL
);

CREATE TABLE checkouts (
id INTEGER PRIMARY KEY,
cart_id INTEGER NOT NULL REFERENCES carts(id),
msid TEXT NOT NULL,
amount NUMERIC(10, 2) NOT NULL
);

INSERT INTO sites (id, country) VALUES
(1, 'US'),
(2, 'US'),
(3, 'DE');

-- Two carts of site 1 share a msid, so a distinct count over them differs
-- from a plain count.
INSERT INTO carts (id, site_id, msid, value) VALUES
(1, 1, 'm1', 10),
(2, 1, 'm1', 20),
(3, 2, 'm2', 30),
(4, 3, 'm3', 40);

-- Cart 1 has two checkouts, so joining checkouts in splits its row. Carts 2
-- and 4 have none, so they only survive the join as NULL-extended rows.
INSERT INTO checkouts (id, cart_id, msid, amount) VALUES
(1, 1, 'x1', 100),
(2, 1, 'x2', 200),
(3, 3, 'x1', 300);
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod member_expressions;
mod modifiers;
mod multi_fact;
mod multi_stage;
mod nested_join_trees;
mod null_filters;
mod pre_aggregations;
mod propagate_subquery;
Expand Down
Loading
Loading