Skip to content

feat: allow CaseExpr to reuse its first required operand as ELSE - #27281

Closed
Weijun-H wants to merge 5 commits into
apache:mainfrom
Weijun-H:case-first-required-operand-reuse
Closed

Weijun-H wants to merge 5 commits into
apache:mainfrom
Weijun-H:case-first-required-operand-reuse

Conversation

@Weijun-H

@Weijun-H Weijun-H commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Which issue does this PR close?

Closes #27280.

Rationale for this change

CASE WHEN f(x) = ... THEN ... ELSE f(x) END repeats f(x) for ELSE rows. This opt-in API reuses its first result at the original evaluation position, avoiding extra work for expensive operands. Direct physical-plan builders can use it without logical CSE, while preserving evaluation position.

What changes are included in this PR?

  • Add RequiredOperandReusePolicy and CaseExpr::with_required_operand_reuse. Callers identify an unconditional, exactly-once operand and guarantee that column replacement, projection and a private column preserve expression semantics; unknown leaves decline reuse.
  • Evaluate the actual first-WHEN instance once. Expand scalar results after argument rewrites without reevaluation. Rebuilds retain permission only if the original call is preserved and WHEN/ELSE arguments still match.
  • Restrict reuse to CASE without a base expression. Permission participates in Eq/Hash; try_to_proto returns Ok(None) for a CASE with permission.
  • Add a reproducible complete-CASE Criterion benchmark, including first-use preparation and repeated batches.

What is the testing strategy for this PR?

Validated tree 42c33d272af2: 77 CASE tests (10 reuse), whole-workspace clippy, formatting, and extended workspace tests pass (13,632 standard tests, 8 ignored; 547/547 SQLLogicTest files). Both new regressions fail on the prior implementation and pass with the instance-selection and scalar-rewrite fixes.

The table compares ON/OFF within the same corrected version, not this version against an unpatched baseline. Physical-expression benchmark: Rust 1.99.1, Apple arm64, release-nonlto; 8,192 rows. Each OFF/ON mode has three rounds of 30 samples. Times below are medians of three per-round arithmetic means of time/iterations; percentages use those OFF/ON values. First includes CASE construction, authorization and preparation, with inputs/operand nodes prebuilt. Repeated reuses the prepared plan. Output and evaluated-row checks run outside timing. Two input batches alternate; at ELSE 50% each sends a different contiguous half to ELSE. Each iteration evaluates one 8,192-row batch; times are per batch.

Operand / total input columns (time unit) ELSE First OFF → ON (time saved) Repeated OFF → ON (time saved)
add / 1 (µs) 0% 2.312 → 3.735 (-61.5%) 1.615 → 1.851 (-14.6%)
add / 1 (µs) 50% 29.692 → 30.206 (-1.7%) 28.730 → 28.011 (+2.5%)
add / 1 (µs) 100% 2.887 → 3.705 (-28.3%) 2.164 → 1.844 (+14.8%)
add / 100 (µs) 0% 2.306 → 4.780 (-107.3%) 1.602 → 2.877 (-79.6%)
add / 100 (µs) 50% 29.560 → 31.546 (-6.7%) 28.603 → 29.316 (-2.5%†)
add / 100 (µs) 100% 2.916 → 4.772 (-63.6%) 2.149 → 2.835 (-31.9%)
MD5 of 4096 bytes / 1 (ms) 0% 39.576 → 39.506 (+0.2%†) 38.492 → 38.576 (-0.2%†)
MD5 of 4096 bytes / 1 (ms) 50% 58.864 → 38.571 (+34.5%) 58.516 → 38.777 (+33.7%)
MD5 of 4096 bytes / 1 (ms) 100% 77.472 → 38.666 (+50.1%) 76.809 → 38.586 (+49.8%)

Positive percentages mean less time. † Paired rounds disagree in direction; no directional conclusion. The 100-column input adds 99 unused Int32 columns sharing buffers; only widths 1 and 100 were tested. These single-machine microbenchmarks establish neither statistical significance nor end-to-end query speedups.

Are there any user-facing changes?

New opt-in physical-expression API. Conventional SQL planning does not enable it automatically. Callers must verify semantic safety and whether avoided computation outweighs added costs; cheap operands can become slower.

@Weijun-H
Weijun-H force-pushed the case-first-required-operand-reuse branch from b80a0c8 to bccefb6 Compare November 10, 2026 02:28
@github-actions github-actions Bot added physical-expr Changes to the physical-expr crates physical-plan Changes to the physical-plan crate labels Nov 10, 2026
@github-actions github-actions Bot removed the physical-plan Changes to the physical-plan crate label Nov 10, 2026
@Weijun-H
Weijun-H requested a balanced review from Copilot November 10, 2026 03:56

Copilot AI 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.

🟡 Changes recommended

Active findings identify incorrect evaluation of structurally equal but distinct volatile instances and a rewrite path that can turn valid CASE evaluation into an array-precondition error.

2 open issues
What changed in this PR

Adds an opt-in API for physical-plan producers to reuse a CASE’s first required WHEN operand as its ELSE value, avoiding duplicate evaluation without relying on logical optimization.

Changes:

  • Introduces a caller-supplied reuse policy and prepared CASE evaluation.
  • Tracks permission through expression rewrites, equality, hashing, and serialization eligibility.
  • Adds tests for reuse, branch selection, projection, rewrites, and error ordering.
File Description
datafusion/​physical-expr/​src/​expressions/​mod.rs Re-exports RequiredOperandReusePolicy.
datafusion/​physical-expr/​src/​expressions/​case.rs Implements operand reuse, permission handling, and regression tests.

Review effort: Balanced

Comment on lines +423 to +427
let operand = owner
.body
.else_expr
.as_ref()
.expect("authorized CASE has ELSE");
Comment on lines +1583 to +1586
rebuilt = rebuilt.with_required_operand_reuse(
authorized,
Arc::clone(&required.policy),
);
@Weijun-H Weijun-H closed this Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-expr Changes to the physical-expr crates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let physical-plan producers reuse a CASE's first-WHEN operand as its ELSE value

2 participants