Skip to content

perf: compute using columns once per SELECT, not per item - #26317

Open
JoeryH wants to merge 2 commits into
apache:mainfrom
JoeryH:sql-using-columns-once
Open

JoeryH wants to merge 2 commits into
apache:mainfrom
JoeryH:sql-using-columns-once

Conversation

@JoeryH

@JoeryH JoeryH commented Oct 14, 2026

Copy link
Copy Markdown

Which issue does this PR close?

Rationale for this change

Planning a SELECT costs O(items × input plan size), because SqlToRel::sql_select_to_rex calls LogicalPlan::using_columns() once per SELECT item. Each call walks the whole input plan, subqueries and expressions included, and the result depends only on the input plan. Nested wide SELECTs (views over views) therefore plan in roughly O(width² × depth²). The issue has the numbers. At 4000 columns × 12 levels, statement_to_plan goes from 8.0 s to 4.4 s with this change alone, and to 0.6 s once the DFSchema scan from #26314 is also gone.

What changes are included in this PR?

  • prepare_select_exprs computes plan.using_columns() once and passes it to sql_select_to_rex. plan_wildcard_options does the same for a wildcard's REPLACE items.
  • A sql_planner benchmark case, logical_nested_select_700_columns_8_levels. On an M1 Max its query goes from 178 ms to 122 ms in statement_to_plan, measured with a standalone harness because the full benchmark needs the clickbench data.

One behaviour difference: if using_columns() itself fails, the error now surfaces once for the SELECT instead of once per item through DataFusionErrorBuilder. using_columns only errors if the tree walk does, so I don't expect this to be observable, but I can keep the old shape with a lazily filled Option if you'd prefer.

What is the testing strategy for this PR?

The planned output is unchanged, so this relies on the existing datafusion-sql tests and sqllogictests, which pass locally. Those cover USING / NATURAL joins and wildcard expansion over them, the cases using_columns exists for. The new benchmark case covers the performance side.

Are there any user-facing changes?

No. Planning is faster and plans are identical.


Prepared with AI assistance (Claude Code).

@github-actions github-actions Bot added sql SQL Planner core Core DataFusion crate labels Oct 14, 2026
kosiew
kosiew previously requested changes Nov 6, 2026

@kosiew kosiew left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JoeryH,

Thanks for addressing the repeated USING-column discovery during SELECT planning. Sharing the result across explicit projection expressions is an appropriate optimization, and the new benchmark targets the wide, deeply nested queries described in the issue.

The architectural boundary is sound: USING-column discovery depends on the input plan, not on individual expressions, so computing it once per projection avoids redundant traversal without introducing persistent caching or changing public APIs. However, moving discovery unconditionally to the projection level also makes wildcard-only queries perform unnecessary full-plan traversals.

I'm requesting changes to preserve the existing wildcard fast paths while retaining the explicit-expression speedup. Please make discovery conditional or lazy and validate the qualified-wildcard case against the parent revision. The additional semantic and scaling coverage suggestions are non-blocking.

Comment thread datafusion/sql/src/select.rs Resolved
Comment thread datafusion/sql/src/select.rs Resolved
Comment thread datafusion/core/benches/sql_planner.rs Resolved
JoeryH and others added 2 commits November 6, 2026 09:47
SqlToRel::sql_select_to_rex called LogicalPlan::using_columns() for every
SELECT item, walking the whole input plan each time. Compute it once in
prepare_select_exprs (and once for a wildcard's REPLACE items) and pass it
down. Adds a nested wide SELECT case to the sql_planner benchmark.
Wildcard-only projections never normalize an expression, so they no longer
walk the input plan for USING columns. REPLACE items share the same lazily
collected set. Adds a width/depth matrix and a qualified-wildcard control to
the nested SELECT benchmark, and an SLT for REPLACE over a USING join.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@JoeryH
JoeryH force-pushed the sql-using-columns-once branch from e6789df to 3e532c4 Compare November 6, 2026 17:18
@github-actions github-actions Bot added the sqllogictest SQL Logic Tests (.slt) label Nov 6, 2026
@JoeryH

JoeryH commented Nov 6, 2026 •

Copy link
Copy Markdown
Author

thanks so much for the review @kosiew. I addressed your comments (with AI assistance)

@kosiew kosiew left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@JoeryH,

Thanks for the follow-up changes. The projection-local lazy cache appropriately eliminates redundant USING-column traversals while preserving expression normalization and wildcard REPLACE behavior. The added regression coverage and nested-projection benchmarks address the relevant correctness and performance concerns.

The ownership boundary is appropriate: USING-column metadata belongs to the SELECT input plan and can be shared across its projection expressions. Lazy initialization also avoids unnecessary collection for wildcard-only projections, while passing the same cache through REPLACE planning preserves consistent resolution semantics.

All previous comments have been addressed, including the wildcard-traversal concern. I found no remaining blockers or actionable suggestions.

@kosiew
kosiew dismissed their stale review November 10, 2026 11:55

requested changes addressed

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

Labels

core Core DataFusion crate sql SQL Planner sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQL planner calls LogicalPlan::using_columns() once per SELECT item, making wide nested SELECTs quadratic

2 participants