Skip to content

Commit 9f98324

Browse files
committed
Handle computed and distinct SUM outputs
1 parent 1429ff6 commit 9f98324

8 files changed

Lines changed: 413 additions & 12 deletions

File tree

src/core/ivm_view_classifier.cpp

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ static void BuildModelFeatures(DeltaViewModel &model, const PlanAnalysis &analys
353353
AddUnique(model.features, DeltaModelFeature::WINDOW_AFFECTED_PARTITION);
354354
}
355355
if (!model.HasFeature(DeltaModelFeature::FULL_ONLY)) {
356-
if (input.distinct_aux_candidate) {
356+
if (input.distinct_aux_candidate && !input.has_top_level_redundant_scalar_distinct) {
357357
AddUnique(model.features, DeltaModelFeature::DISTINCT_STATEFUL);
358358
}
359359
if (input.semi_anti_aux_candidate) {
@@ -559,10 +559,19 @@ static void SelectRefreshType(DeltaViewModel &model, const PlanAnalysis &analysi
559559
model.type = RefreshType::GROUP_RECOMPUTE;
560560
} else if (analysis.found_filtered_list) {
561561
model.type = RefreshType::FULL_REFRESH;
562+
} else if (input.has_computed_sum_aggregate_projection) {
563+
model.type = model.group_columns.empty() ? RefreshType::FULL_REFRESH : RefreshType::GROUP_RECOMPUTE;
564+
} else if (analysis.found_distinct && !analysis.found_union_distinct && model.distinct_at_top &&
565+
analysis.found_aggregation) {
566+
// DISTINCT over multiple aggregate result rows is a second non-linear
567+
// aggregation level. The outer DISTINCT key is an aggregate value, not
568+
// a source group key that affected-group recompute can recover.
569+
model.type = RefreshType::FULL_REFRESH;
562570
} else if (analysis.found_count_distinct && !model.group_columns.empty()) {
563571
model.type =
564572
input.count_distinct_aux_candidate ? RefreshType::COUNT_DISTINCT_INCREMENTAL : RefreshType::GROUP_RECOMPUTE;
565-
} else if (analysis.found_distinct && !model.distinct_at_top && analysis.found_aggregation) {
573+
} else if (analysis.found_distinct && !input.has_top_level_redundant_scalar_distinct && !model.distinct_at_top &&
574+
analysis.found_aggregation) {
566575
model.type = model.HasFeature(DeltaModelFeature::DISTINCT_STATEFUL) ? RefreshType::DISTINCT_INCREMENTAL
567576
: RefreshType::GROUP_RECOMPUTE;
568577
} else if (model.union_distinct_over_agg && !model.group_columns.empty()) {
@@ -878,6 +887,9 @@ DeltaViewModel BuildDeltaViewModel(const DeltaViewModelInput &input) {
878887

879888
model.has_minmax_metadata = analysis.found_minmax || analysis.found_count_distinct || analysis.found_list;
880889
model.distinct_at_top = IsDistinctAtTop(analysis, output_names);
890+
if (input.has_top_level_redundant_scalar_distinct) {
891+
model.distinct_at_top = false;
892+
}
881893
BuildGroupColumns(model, facts, output_names, input.visible_output_count);
882894

883895
if ((analysis.found_left_join || analysis.found_full_outer) && analysis.found_aggregation &&

src/core/parser.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ MaterializedViewParserExtension::PlanFunction(ParserExtensionInfo *info, ClientC
371371
bool pre_rewrite_has_aggregate_filter = false;
372372
bool has_hidden_minmax_having = false;
373373
bool has_computed_minmax_aggregate_projection = false;
374+
bool has_computed_sum_aggregate_projection = false;
374375
{
375376
auto select_parse_plan_start = create_profile_now();
376377
Parser select_parser;
@@ -411,6 +412,7 @@ MaterializedViewParserExtension::PlanFunction(ParserExtensionInfo *info, ClientC
411412
stored_query_has_aggregate_filter = post_rewrite_facts.has_filter_above_aggregate;
412413
has_hidden_minmax_having = post_rewrite_facts.has_hidden_minmax_having_column;
413414
has_computed_minmax_aggregate_projection = post_rewrite_facts.has_computed_minmax_aggregate_projection;
415+
has_computed_sum_aggregate_projection = post_rewrite_facts.has_computed_sum_aggregate_projection;
414416

415417
// Keep data tables unlimited/unordered; apply ORDER BY/LIMIT in the user-facing view.
416418
{
@@ -568,6 +570,8 @@ MaterializedViewParserExtension::PlanFunction(ParserExtensionInfo *info, ClientC
568570
model_input.stored_query_has_top_k = stored_query_retains_top_k;
569571
model_input.has_hidden_minmax_having = has_hidden_minmax_having;
570572
model_input.has_computed_minmax_aggregate_projection = has_computed_minmax_aggregate_projection;
573+
model_input.has_computed_sum_aggregate_projection = has_computed_sum_aggregate_projection;
574+
model_input.has_top_level_redundant_scalar_distinct = facts.has_top_level_redundant_scalar_distinct;
571575
model_input.has_ducklake_source = HasDuckLakeSourceForModel(facts, table_names, target_is_ducklake);
572576
const bool distinct_at_top = IsDistinctAtTop(analysis, output_names);
573577

src/core/parser_plan_helpers.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,49 @@ static bool ExpressionReferencesMinMaxAggregate(Expression &expr,
431431
return found;
432432
}
433433

434+
static bool ExpressionReferencesUserSumAggregate(Expression &expr, const CreateMVPlanFacts &facts,
435+
const unordered_map<idx_t, LogicalAggregate *> &aggregates,
436+
idx_t depth = 0) {
437+
if (depth > 16) {
438+
return false;
439+
}
440+
if (expr.expression_class == ExpressionClass::BOUND_COLUMN_REF) {
441+
auto &column_ref = expr.Cast<BoundColumnRefExpression>();
442+
auto aggregate_it = aggregates.find(column_ref.binding.table_index);
443+
if (aggregate_it != aggregates.end()) {
444+
auto &aggregate = *aggregate_it->second;
445+
if (column_ref.binding.column_index >= aggregate.expressions.size()) {
446+
return false;
447+
}
448+
auto &aggregate_expr = aggregate.expressions[column_ref.binding.column_index];
449+
if (aggregate_expr->expression_class != ExpressionClass::BOUND_AGGREGATE) {
450+
return false;
451+
}
452+
auto &bound_aggregate = aggregate_expr->Cast<BoundAggregateExpression>();
453+
return bound_aggregate.function.name == "sum" &&
454+
!IncrementalTableNames::IsInternalColumn(bound_aggregate.alias);
455+
}
456+
auto projection_it = facts.projections_by_index.find(column_ref.binding.table_index);
457+
if (projection_it == facts.projections_by_index.end()) {
458+
return false;
459+
}
460+
auto &projection = *projection_it->second;
461+
if (column_ref.binding.column_index >= projection.expressions.size()) {
462+
return false;
463+
}
464+
return ExpressionReferencesUserSumAggregate(*projection.expressions[column_ref.binding.column_index], facts,
465+
aggregates, depth + 1);
466+
}
467+
468+
bool found = false;
469+
ExpressionIterator::EnumerateChildren(expr, [&](Expression &child) {
470+
if (!found && ExpressionReferencesUserSumAggregate(child, facts, aggregates, depth + 1)) {
471+
found = true;
472+
}
473+
});
474+
return found;
475+
}
476+
434477
static void FinalizeCreateMVPlanFacts(CreateMVPlanFacts &facts) {
435478
unordered_map<idx_t, LogicalAggregate *> aggregates;
436479
for (auto *aggregate : facts.aggregates) {
@@ -441,6 +484,14 @@ static void FinalizeCreateMVPlanFacts(CreateMVPlanFacts &facts) {
441484
}
442485
for (auto *projection : facts.projections) {
443486
for (auto &expr : projection->expressions) {
487+
Expression *unwrapped = expr.get();
488+
while (unwrapped->expression_class == ExpressionClass::BOUND_CAST) {
489+
unwrapped = unwrapped->Cast<BoundCastExpression>().child.get();
490+
}
491+
if (unwrapped->expression_class != ExpressionClass::BOUND_COLUMN_REF &&
492+
ExpressionReferencesUserSumAggregate(*unwrapped, facts, aggregates)) {
493+
facts.has_computed_sum_aggregate_projection = true;
494+
}
444495
if (expr->expression_class == ExpressionClass::BOUND_COLUMN_REF && IsHiddenHavingColumn(expr->alias)) {
445496
auto &column_ref = expr->Cast<BoundColumnRefExpression>();
446497
if (IsMinMaxAggregateColumn(column_ref, aggregates)) {
@@ -901,10 +952,34 @@ static void AddJoinEdgesFromFacts(CreateMVPlanFacts &facts) {
901952
}
902953
}
903954

955+
bool ProducesAtMostOneRow(LogicalOperator &node) {
956+
LogicalOperator *current = &node;
957+
while ((current->type == LogicalOperatorType::LOGICAL_PROJECTION ||
958+
current->type == LogicalOperatorType::LOGICAL_FILTER) &&
959+
!current->children.empty()) {
960+
current = current->children[0].get();
961+
}
962+
if (current->type != LogicalOperatorType::LOGICAL_AGGREGATE_AND_GROUP_BY) {
963+
return false;
964+
}
965+
auto &aggregate = current->Cast<LogicalAggregate>();
966+
return aggregate.groups.empty() && aggregate.grouping_sets.size() <= 1;
967+
}
968+
904969
CreateMVPlanFacts BuildCreateMVPlanFacts(LogicalOperator *plan, const string &current_catalog) {
905970
CreateMVPlanFacts facts;
906971
facts.root = plan;
907972
facts.analysis = AnalyzePlan(plan);
973+
LogicalOperator *top = plan;
974+
while (top && top->children.size() == 1 &&
975+
(top->type == LogicalOperatorType::LOGICAL_CREATE_TABLE ||
976+
top->type == LogicalOperatorType::LOGICAL_PROJECTION || top->type == LogicalOperatorType::LOGICAL_FILTER ||
977+
top->type == LogicalOperatorType::LOGICAL_ORDER_BY || top->type == LogicalOperatorType::LOGICAL_LIMIT ||
978+
top->type == LogicalOperatorType::LOGICAL_TOP_N)) {
979+
top = top->children[0].get();
980+
}
981+
facts.has_top_level_redundant_scalar_distinct = top && top->type == LogicalOperatorType::LOGICAL_DISTINCT &&
982+
!top->children.empty() && ProducesAtMostOneRow(*top->children[0]);
908983
unordered_map<string, idx_t> next_occurrence;
909984
CollectCreateMVPlanFacts(plan, current_catalog, facts, next_occurrence, false, false);
910985
FinalizeCreateMVPlanFacts(facts);

src/core/plan_rewrite.cpp

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include "core/openivm_constants.hpp"
44
#include "core/openivm_debug.hpp"
5+
#include "core/parser_plan_helpers.hpp"
56
#include "core/plan_rewrite_internal.hpp"
67
#include "duckdb/catalog/catalog.hpp"
78
#include "duckdb/catalog/catalog_entry/aggregate_function_catalog_entry.hpp"
@@ -39,18 +40,30 @@
3940
namespace duckdb {
4041

4142
/// Replace LOGICAL_DISTINCT with LOGICAL_AGGREGATE + COUNT(*).
42-
static void RewriteDistinct(ClientContext &context, Binder &binder, unique_ptr<LogicalOperator> &node) {
43+
/// Returns whether a DISTINCT state aggregate was added at the top level.
44+
static bool RewriteDistinct(ClientContext &context, Binder &binder, unique_ptr<LogicalOperator> &node,
45+
bool is_top_level) {
4346
if (node->type != LogicalOperatorType::LOGICAL_DISTINCT) {
44-
for (auto &child : node->children) {
45-
RewriteDistinct(context, binder, child);
47+
bool added_top_level_state = false;
48+
for (idx_t child_idx = 0; child_idx < node->children.size(); child_idx++) {
49+
bool child_is_top_level =
50+
is_top_level && node->type == LogicalOperatorType::LOGICAL_PROJECTION && child_idx == 0;
51+
added_top_level_state = RewriteDistinct(context, binder, node->children[child_idx], child_is_top_level) ||
52+
added_top_level_state;
4653
}
47-
return;
54+
return added_top_level_state;
4855
}
4956

5057
auto &distinct = node->Cast<LogicalDistinct>();
5158
if (node->children.empty()) {
5259
OPENIVM_DEBUG_PRINT("[PlanRewrite] DISTINCT has no children — skipping\n");
53-
return;
60+
return false;
61+
}
62+
if (ProducesAtMostOneRow(*node->children[0])) {
63+
OPENIVM_DEBUG_PRINT("[PlanRewrite] Removed redundant DISTINCT over scalar aggregate\n");
64+
node = std::move(node->children[0]);
65+
RewriteDistinct(context, binder, node, false);
66+
return false;
5467
}
5568
auto &child = node->children[0];
5669
child->ResolveOperatorTypes();
@@ -86,6 +99,7 @@ static void RewriteDistinct(ClientContext &context, Binder &binder, unique_ptr<L
8699

87100
OPENIVM_DEBUG_PRINT("[PlanRewrite] DISTINCT → AGGREGATE + COUNT(*), %zu groups\n", agg_node->groups.size());
88101
node = std::move(agg_node);
102+
return is_top_level;
89103
}
90104

91105
static bool IsSemiAntiJoinType(JoinType join_type) {
@@ -1347,9 +1361,9 @@ static void RewritePassAggregateFilters(PlanRewriteContext &rewrite_context) {
13471361
}
13481362

13491363
static void RewritePassDistinct(PlanRewriteContext &rewrite_context) {
1350-
bool had_distinct = HasTopLevelDistinct(rewrite_context.plan);
1351-
RewriteDistinct(rewrite_context.context, rewrite_context.binder, rewrite_context.plan);
1352-
if (had_distinct) {
1364+
bool added_top_level_state = RewriteDistinct(rewrite_context.context, rewrite_context.binder, rewrite_context.plan,
1365+
HasTopLevelDistinct(rewrite_context.plan));
1366+
if (added_top_level_state) {
13531367
rewrite_context.planner_names.push_back(openivm::DISTINCT_COUNT_COL);
13541368
}
13551369
}

src/include/core/ivm_view_classifier.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ struct DeltaViewModelInput {
163163
bool stored_query_has_top_k = false;
164164
bool has_hidden_minmax_having = false;
165165
bool has_computed_minmax_aggregate_projection = false;
166+
bool has_computed_sum_aggregate_projection = false;
167+
bool has_top_level_redundant_scalar_distinct = false;
166168
bool has_ducklake_source = false;
167169
};
168170

src/include/core/parser_plan_helpers.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ struct CreateMVPlanFacts {
8585
bool has_bound_aggregate_filter = false;
8686
bool has_hidden_minmax_having_column = false;
8787
bool has_computed_minmax_aggregate_projection = false;
88+
bool has_computed_sum_aggregate_projection = false;
89+
bool has_top_level_redundant_scalar_distinct = false;
8890
};
8991

9092
string BuildTopKSuffix(const vector<BoundOrderByNode> &orders, idx_t limit_val, idx_t offset_val,
@@ -94,6 +96,7 @@ string QualifyCreateSourceTable(const string &table_name, const string &current_
9496
const string &default_db);
9597
string ExplainInitialLoadQuery(Connection &con, const string &label, const string &query);
9698
CreateMVPlanFacts BuildCreateMVPlanFacts(LogicalOperator *plan, const string &current_catalog);
99+
bool ProducesAtMostOneRow(LogicalOperator &node);
97100
void AddJoinKeyColumn(const unique_ptr<Expression> &expr, unordered_map<idx_t, unordered_set<idx_t>> &join_key_cols);
98101
bool OuterJoinAggregateNeedsRecompute(const CreateMVPlanFacts &facts, idx_t group_index);
99102
bool RelationExists(Connection &con, const string &qualified_name);

0 commit comments

Comments
 (0)