Skip to content

perf: specialize CASE WHEN for divide-by-zero protection - #24402

Open
shinzoxD wants to merge 1 commit into
apache:mainfrom
shinzoxD:feat/11570-case-when-div-zero-protection
Open

perf: specialize CASE WHEN for divide-by-zero protection#24402
shinzoxD wants to merge 1 commit into
apache:mainfrom
shinzoxD:feat/11570-case-when-div-zero-protection

Conversation

@shinzoxD

@shinzoxDshinzoxD commented Aug 15, 2026

Copy link
Copy Markdown

Which issue does this PR close?

Rationale for this change

A common TPC-DS pattern protects against divide-by-zero with:

CASE WHEN y >0 THEN x / y ELSE NULL END

The general CaseExpr path (filter + scatter) is expensive for this. #19994 tried replacing it with a fully vectorized always-divide (eq / zip / div / nullif, dummy-dividing zeros by 1). That wins when few denominators are zero, but loses when many are.

After #20097 (ExpressionOrExpression for CASE WHEN x THEN y [ELSE NULL]) and #20498 (type-specific scatter), the general CASE path is in better shape. This PR specializes the remaining divide-by-zero protection pattern without the #19994 always-divide tradeoff.

What changes are included in this PR?

New EvalMethod::DivideByZeroProtection that detects:

CASE WHEN y {>, !=, <} 0 THEN x / y [ELSE NULL]

including swapped comparisons (0 < y, 0 != y, 0 > y) and Cast / TryCast wrappers around the checked operand / divisor.

Evaluation keeps the original WHEN predicate so >, !=, and < stay correct, then:

  1. Evaluates WHEN (NULL WHEN treated as false, so those rows are not divided)
  2. Nulls the divisor on excluded rows (nullif)
  3. Divides. Skipped rows become NULL without a dummy divide-by-one
  4. Fast paths for all-true (plain divide) and all-false / no-true (scalar NULL)

Only applied when:

  • Single WHEN / THEN and implicit or explicit ELSE NULL
  • The checked operand is the same as the divisor (including through Cast / TryCast)
  • Both divide operands are cheap (column, literal, or cast of those) so evaluating them on the full batch cannot introduce errors on skipped rows

Unlike #19994, this does not dummy-divide by 1 and does not collapse > / < / != into a single "is zero" mask.

Are these changes tested?

Yes.

Unit tests in datafusion/physical-expr/src/expressions/case.rs:

  • test_divide_by_zero_protection_specialization: pattern detection + results with Cast
  • test_divide_by_zero_protection_predicates: !=, >, <, and swapped 0 != d
  • test_divide_by_zero_protection_all_true_all_false_and_nulls
  • test_divide_by_zero_protection_specialization_not_applied: WHEN a / divisor b must not specialize
  • test_divide_by_zero_protection_not_applied_with_else: ELSE 0 must not specialize

SLTs in datafusion/sqllogictest/test_files/case.slt (existing >, !=, < cases from the #19994 review, plus):

  • Swapped form WHEN 0 < d
  • Float divide-by-zero protection
  • Implicit ELSE (no ELSE clause)
  • NULL divisor treated as not matching WHEN

Bench comment in datafusion/physical-expr/benches/case_when.rs updated to match the specialization trigger (!= 0).

Are there any user-facing changes?

No. Internal optimization; results are unchanged.

Testing

  • Existing case.slt>, !=, < divide-by-zero rows still correct
  • New SLTs for swapped comparison, float, implicit ELSE, and NULL divisor
  • Unit tests for specialization, predicates, all-true / all-false / nulls, and negative cases
  • cargo fmt --all / cargo clippy --all-targets --all-features -- -D warnings (CI)

Detect `CASE WHEN y {>, !=, <} 0 THEN x / y [ELSE NULL]` and evaluate
it by applying the WHEN mask to the divisor (null on excluded rows)
instead of the general CASE filter/scatter path.
Unlike apache#19994, this keeps the original predicate (so `>`, `!=`, and
`<` stay correct) and does not dummy-divide by 1.
Closesapache#11570
CopilotAI lite review requested due to automatic review settings August 15, 2026 23:12

CopilotAI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actionsgithub-actionsBot added physical-expr Changes to the physical-expr crates sqllogictest SQL Logic Tests (.slt) labels Aug 15, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

physical-exprChanges to the physical-expr cratessqllogictestSQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Potential optimization for CASE WHEN for protecting against divide by zero

2 participants

@shinzoxD