Skip to content

Prevent Repetitive To String calls in push_projection_dedupl - #76

Merged
xudong963 merged 2 commits into
branch-55from
wyatt/push-projection-dedupl-improv
Sep 11, 2026
Merged

xudong963 merged 2 commits into
branch-55from
wyatt/push-projection-dedupl-improv

Conversation

@wyatt-herkamp

@wyatt-herkamp wyatt-herkamp commented Sep 5, 2026 •

Copy link
Copy Markdown

Which issue does this PR close?

N/A — performance fix found while profiling UNNEST planning.

Rationale for this change

Planning a query with UNNEST and a wide select list is disproportionately slow.

The unnest rewrite builds an inner projection and calls push_projection_dedupl
for every expression it pushes. That function decided whether an expression was
already present by formatting both sides with Expr::schema_name().to_string()
on every comparison:

let schema_name = expr.schema_name().to_string();
if !projection.iter().any(|e| e.schema_name().to_string() == schema_name) {
    projection.push(expr);
}

So pushing n expressions formats and allocates O(n^2) strings, and each
schema_name() render walks the whole expression tree. For a 1000-column select
list that dominates planning time.

Benchmarks (sql_planner_extended, added in this PR):

Benchmark Before After Change
logical_unnest_plus_200_columns 4.70 ms 1.04 ms -77.9%
logical_unnest_plus_1000_columns 109.2 ms 17.1 ms -84.3%
logical_unnest_in_200_exprs 11.34 ms 3.38 ms -70.2%

What changes are included in this PR?

datafusion/sql/src/utils.rs

  • Add a private SchemaNameKey<'a> that compares equal exactly when two
    expressions would render the same Expr::schema_name(), without formatting or
    allocating. It mirrors the SchemaDisplay rules:
    • Expr::Alias and Expr::Column render as [relation.]name, so they compare
      as a (relation, name) pair — the aliased/inner expression is not compared,
      matching the rendering.
    • Expr::Cast / Expr::TryCast render as their input, so the key unwraps them.
    • Everything else renders structurally, so it compares structurally (Expr: PartialEq).
  • push_projection_dedupl now compares keys instead of formatted strings. The
    comparison count is unchanged (still a linear scan); what goes away is the
    per-comparison tree walk and String allocation.

The one behavioral divergence is deliberate: two structurally different
expressions that happen to render to the same schema name (e.g. foo(a) and
foo(CAST(a AS BIGINT))) are now both kept, where the string comparison silently
dropped the second. Projections containing such a pair are rejected by
validate_unique_names either way, so the user-visible result is the same error
rather than a silently dropped expression.

datafusion/core/benches/sql_planner_extended.rs

  • Add register_list_table (N Int32 columns + one List<Int32> column) and a
    logical_plan helper that only parses and plans, isolating SQL planner cost.
  • Add three benchmarks: logical_unnest_plus_200_columns,
    logical_unnest_plus_1000_columns (bare columns alongside an unnest) and
    logical_unnest_in_200_exprs (columns inside expressions containing an unnest,
    which also exercises the repeated unnest-placeholder alias path).
  • Bump the slow benchmark group from sample_size(5) to sample_size(10)
    (group renamed accordingly) — 5 samples was too noisy to read the change.

Are these changes tested?

Yes, two new unit tests in datafusion/sql/src/utils.rs:

  • test_schema_name_key_matches_schema_name — cross-products a list of
    expressions (columns with and without a relation, aliases sharing a name,
    CAST/TRY_CAST, binary exprs, aggregates, literals, an unnest placeholder
    alias) and asserts SchemaNameKey equality agrees with comparing formatted
    schema_name() for every pair.
  • test_push_projection_dedupl — asserts the resulting projection and its field
    names for a sequence of pushes covering duplicates, relation-qualified vs bare
    columns, same-name aliases and a cast of an already-present column.

Existing UNNEST coverage (sqllogictests and the SQL planner tests) exercises
the rewrite end to end.

Are there any user-facing changes?

No public API changes — SchemaNameKey is private to datafusion::sql::utils.
Faster planning for UNNEST queries with wide select lists. The only observable
change is the divergence described above, where a projection that previously had
an expression silently dropped now surfaces the duplicate-name error from
validate_unique_names.

@wyatt-herkamp wyatt-herkamp changed the title Remove to string comparison in push_projection_dedupl Remove to to_string comparison in push_projection_dedupl Sep 6, 2026
Comment thread datafusion/sql/src/utils.rs Outdated
name: r_name,
},
) => l_name == r_name && l_relation == r_relation,
(Self::Other(l), Self::Other(r)) => l == r,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SchemaNameKey only unwraps top-level casts and otherwise compares expressions structurally. However, schema_name() also hides casts nested inside expressions. This causes the following valid query to fail:

SELECT DISTINCT ON (1 + 1)
    UNNEST([1, 2]) AS item,
    COUNT(*) OVER () AS n
ORDER BY CAST(1 AS BIGINT) + 1;

Here, DISTINCT ON and ORDER BY share the inner projection created by the UNNEST rewrite. The new comparison retains both expressions despite their identical schema names. The final output names, item and n, are unique.

I suggest preserving schema-name-based deduplication while computing each incoming expression’s name once and caching it in a HashSet, then adding this query and its TRY_CAST variant as regression tests.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ok went with the hash set approach. I was hoping to prevent any to_string calls. However, the HashSet seems to give almost the same improvements.

@wyatt-herkamp
wyatt-herkamp marked this pull request as ready for review September 7, 2026 15:40
@wyatt-herkamp wyatt-herkamp changed the title Remove to to_string comparison in push_projection_dedupl Prevent Repetitive To String calls in push_projection_dedupl Sep 7, 2026
@xudong963
xudong963 merged commit 35e7e10 into branch-55 Sep 11, 2026
70 checks passed
@xudong963
xudong963 deleted the wyatt/push-projection-dedupl-improv branch September 11, 2026 02:43
MassivePizza pushed a commit that referenced this pull request Sep 30, 2026
* Remove to string comparison in push_projection_dedupl

* Use HahSet
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants