perf: reuse projection schema in OptimizeProjections instead of recomputing it - #24281
Draft
zhuqi-lucas wants to merge 2 commits into
Draft
perf: reuse projection schema in OptimizeProjections instead of recomputing it#24281zhuqi-lucas wants to merge 2 commits into
OptimizeProjections instead of recomputing it#24281zhuqi-lucas wants to merge 2 commits into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Improves OptimizeProjections performance by avoiding repeated recomputation of projection output schemas when pruning projection expressions, instead reusing/slicing the already-computed Projection.schema.
Changes:
- Update
rewrite_projection_given_requirementsto build pruned projections viaProjection::try_new_with_schemausing a sliced schema from the existing projection schema. - Add
project_schema_by_indiceshelper to project fields + functional dependencies while reusing schema metadata. - Add a unit test validating that sliced schemas match
projection_schemarecomputation across representative index subsets.
Suppressed comments (1)
datafusion/optimizer/src/optimize_projections/mod.rs:1285
project_schema_by_indicesalso projects functional dependencies and preserves schema-level metadata, but the test currently only compares fields and qualifiers. Adding assertions for functional dependencies and schema metadata will better protect the behavior this PR relies on.
// Output fields (name, data type, nullability, field metadata) must
// match the from-scratch computation exactly.
assert_eq!(
reused.fields(),
recomputed.fields(),
"fields differ for indices {indices:?}"
);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+1249
to
+1252
| binary_expr(col("b"), Operator::Plus, col("c")), | ||
| col("c").alias("c_alias"), | ||
| lit(1_i64).alias("one"), | ||
| Expr::Column(Column::new(Some(TableReference::bare("test")), "b")), |
…puting it `rewrite_projection_given_requirements` rebuilt the pruned projection with `Projection::try_new`, which recomputes the output schema from scratch via `projection_schema`: it calls `Expr::to_field` for every retained expression, and column resolution (`DFSchema::field_from_column`) is a linear scan, so recomputing a projection's schema is O(exprs * schema_width) and runs on every projection on every optimizer pass. This is especially costly for wide `SELECT *`-style projections over wide schemas. The retained expressions are a subset of the projection's original expressions, so their output fields are unchanged by pruning unreferenced sibling columns. Select those fields from the existing projection schema and construct the pruned projection with `try_new_with_schema`, mirroring the schema reuse already done in `merge_consecutive_projections`. When nothing is pruned the schema Arc is reused as-is. This turns the per-projection schema cost from O(exprs * width) into O(k). Behavior-preserving: the sliced schema is identical to the recomputed one. Adds `project_schema_by_indices_matches_recompute` asserting that equivalence across expression subsets; the full datafusion-optimizer suite still passes.
Addresses review feedback on the schema-reuse test: - The comment claimed a nullable literal, but lit(1_i64) is non-nullable, so nullability propagation was never actually exercised. Swapped it for a NULL Int64 literal and added assertions pinning the premise that the literal is nullable while the input columns are not. - project_schema_by_indices also carries schema-level metadata and projects functional dependencies through the kept indices, but the test only compared fields and qualifiers. Both are now asserted against the from-scratch computation for every subset.
zhuqi-lucas
force-pushed
the
optimize-projections-reuse-schema
branch
from
August 12, 2026 08:56
3c29a28 to
dd18a14
Compare
zhuqi-lucas
marked this pull request as draft
August 12, 2026 09:49
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this close?
Closes #24264. Related to prior
OptimizeProjectionsperf work (#21726).Rationale for this change
rewrite_projection_given_requirements(the core of theOptimizeProjectionsrule) prunes a projection's expressions to the subset actually required, then rebuilds the projection withProjection::try_new:try_newrecomputes the output schema from scratch viaprojection_schema, which:Expr::to_fieldfor every retained expression (type/nullability/qualifier/metadata inference), andDFSchema::field_from_column→index_of_column_by_name, which is a linear scan over the input schema (there is no name→index map).So recomputing one projection's schema is
O(exprs * schema_width), and it runs for every projection on every optimizer pass. For wide,SELECT *-style projections over wide schemas (tens of columns) this becomes effectively quadratic and shows up prominently in planning profiles.But the retained expressions are a subset of the projection's original expressions, and pruning unreferenced sibling columns cannot change the retained columns' output fields. The answer is already sitting in
proj.schema— no need to re-derive it.What changes are included in this PR?
rewrite_projection_given_requirementsnow derives the pruned output schema by selecting the already-computed fields from the existing projection schema (project_schema_by_indices) and builds the projection withProjection::try_new_with_schema, instead of recomputing viatry_new.SELECT *case), the existing schemaArcis reused as-is.FunctionalDependencies::project_functional_dependencies).merge_consecutive_projections(which reusesschemaunchanged when the expression list is unchanged).Complexity for a pruned projection goes from
O(exprs * schema_width)(schema recompute) toO(k)(field slice), and toO(1)when nothing is pruned.Correctness
The sliced schema is identical to the one
projection_schemawould recompute: fieldiof the projection schema corresponds to expressioni, andRequiredIndicesyields a sorted, deduplicated index subset, so slicingproj.schemaat those indices produces exactly the fields of the retained expressions, with qualifiers and metadata preserved.New test
project_schema_by_indices_matches_recomputeasserts, for a mixed expression list (plain column, computed binary expr, alias, nullable literal, qualified column) and every representative index subset, thatproject_schema_by_indices(schema, indices)produces the same fields and qualifiers asprojection_schema(input, exprs_used), and that the identity subset reuses the sameArc.The full
datafusion-optimizertest suite (760 unit + 26 integration, including theEXPLAINplan snapshots) passes unchanged, i.e. no optimized plan output changes.Are there any user-facing changes?
No. This is an internal optimizer performance improvement; planned/optimized plans are unchanged.