Repository navigation
Prevent Repetitive To String calls in push_projection_dedupl - #76
Conversation
| name: r_name, | ||
| }, | ||
| ) => l_name == r_name && l_relation == r_relation, | ||
| (Self::Other(l), Self::Other(r)) => l == r, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
* Remove to string comparison in push_projection_dedupl * Use HahSet
Which issue does this PR close?
N/A — performance fix found while profiling
UNNESTplanning.Rationale for this change
Planning a query with
UNNESTand a wide select list is disproportionately slow.The unnest rewrite builds an inner projection and calls
push_projection_deduplfor 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:
So pushing
nexpressions formats and allocatesO(n^2)strings, and eachschema_name()render walks the whole expression tree. For a 1000-column selectlist that dominates planning time.
Benchmarks (
sql_planner_extended, added in this PR):What changes are included in this PR?
datafusion/sql/src/utils.rsSchemaNameKey<'a>that compares equal exactly when twoexpressions would render the same
Expr::schema_name(), without formatting orallocating. It mirrors the
SchemaDisplayrules:Expr::AliasandExpr::Columnrender as[relation.]name, so they compareas a
(relation, name)pair — the aliased/inner expression is not compared,matching the rendering.
Expr::Cast/Expr::TryCastrender as their input, so the key unwraps them.Expr: PartialEq).push_projection_deduplnow compares keys instead of formatted strings. Thecomparison count is unchanged (still a linear scan); what goes away is the
per-comparison tree walk and
Stringallocation.The one behavioral divergence is deliberate: two structurally different
expressions that happen to render to the same schema name (e.g.
foo(a)andfoo(CAST(a AS BIGINT))) are now both kept, where the string comparison silentlydropped the second. Projections containing such a pair are rejected by
validate_unique_nameseither way, so the user-visible result is the same errorrather than a silently dropped expression.
datafusion/core/benches/sql_planner_extended.rsregister_list_table(NInt32columns + oneList<Int32>column) and alogical_planhelper that only parses and plans, isolating SQL planner cost.logical_unnest_plus_200_columns,logical_unnest_plus_1000_columns(bare columns alongside an unnest) andlogical_unnest_in_200_exprs(columns inside expressions containing an unnest,which also exercises the repeated unnest-placeholder alias path).
sample_size(5)tosample_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 ofexpressions (columns with and without a relation, aliases sharing a name,
CAST/TRY_CAST, binary exprs, aggregates, literals, an unnest placeholderalias) and asserts
SchemaNameKeyequality agrees with comparing formattedschema_name()for every pair.test_push_projection_dedupl— asserts the resulting projection and its fieldnames for a sequence of pushes covering duplicates, relation-qualified vs bare
columns, same-name aliases and a cast of an already-present column.
Existing
UNNESTcoverage (sqllogictests and the SQL planner tests) exercisesthe rewrite end to end.
Are there any user-facing changes?
No public API changes —
SchemaNameKeyis private todatafusion::sql::utils.Faster planning for
UNNESTqueries with wide select lists. The only observablechange 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.