Skip to content

feat: estimate Decimal filter selectivity with interval analysis - #25790

Open
shrivasshankar wants to merge 4 commits into
apache:mainfrom
shrivasshankar:decimal-filter-selectivity
Open

shrivasshankar wants to merge 4 commits into
apache:mainfrom
shrivasshankar:decimal-filter-selectivity

Conversation

@shrivasshankar

@shrivasshankar shrivasshankar commented Sep 27, 2026 •

Copy link
Copy Markdown

Which issue does this PR close?

Rationale for this change

Decimal columns were excluded from interval analysis, so any filter touching
one fell back to the default 20% selectivity. For the Q6-style predicate in the
issue, that meant estimating ~1.2M rows when only ~114K match.

What changes are included in this PR?

  • FilterExec statistics use a new check_statistics_support, which also
    accepts Decimal32/64/128/256.
  • next_value / prev_value handle decimals, so strict comparisons get tight
    bounds (x < 24.00 becomes x <= 23.99). At the limit of a decimal's
    precision the bound becomes unbounded instead of overflowing.
  • get_extreme_value! supports Decimal32/64, which previously hit
    unreachable!().
  • CastExpr now propagates constraints back through lossless decimal widening
    casts. Without this, a literal with a different scale than the column (e.g.
    a >= 0.055 on Decimal(15, 2), coerced to Decimal128(30, 15)) left the
    column range untouched and the estimate at 100%.

check_support and is_datatype_supported are unchanged, so symmetric hash
join pruning behaves as before. Decimal support there could be a follow-up.

What is the testing strategy for this PR?

  • Unit tests for decimal next/prev values across all four widths, including
    precision boundaries like 9.99 / -9.99 for Decimal(3, 2), plus strict
    comparison bounds and Decimal32/64 overflow handling.
  • test_filter_statistics_decimal_expr and
    test_filter_statistics_decimal_mixed_scale_expr check row estimates and
    tightened min/max for same-scale and mixed-scale predicates. The mixed-scale
    test mirrors the plan the SQL planner produces.
  • New cases in test_cast_constraint_propagation, plus an exhaustive check
    that propagating through a decimal widening cast never excludes a valid input.
  • test_custom_filter_selectivity relied on decimals being unsupported, so I
    switched it to Utf8.
  • No sqllogictest or TPC-H/TPC-DS plan outputs changed.

Reproducer from the issue (TPC-H SF1, current main):

SELECT Before After Actual
Date range 869,535 869,535 909,455
Date + Decimal conditions 1,200,243 111,291 114,160

Worth a closer look in review:

  • The cast change extends the allowlist added in fix: restrict reverse cast constraint propagation to safe conversions #25531. Decimal widening is
    lossless and order-preserving, and casting bounds back rounds to a
    neighboring value, which only widens the range. That reasoning is what
    makes it safe, so I'd appreciate a second opinion on it.
  • Decimal arithmetic inside filters (e.g. price * (1 - discount) > x) now
    goes through interval analysis too. The existing tests pass, but I didn't add
    tests specifically for that.
  • A decimal column with no min/max stats now gets selectivity 1.0 instead of
    the default, which matches how integer columns already behave.

Are there any user-facing changes?

Better row estimates for filters on decimal columns, which may change some
plan choices. One new public function, check_statistics_support; no breaking
changes.

Decimal columns were excluded from interval analysis, so any FilterExec
predicate touching a decimal column fell back to the default selectivity.

- Add `check_statistics_support`, which also accepts Decimal32/64/128/256,
  and use it for FilterExec statistics. `check_support` and
  `is_datatype_supported` are unchanged, so symmetric hash join pruning
  keeps its current behavior.
- Step decimals by one unit in the last place in `next_value`/`prev_value`
  so strict comparisons tighten bounds, saturating to unbounded at the
  extremes of the decimal's precision.
- Support Decimal32/64 in `get_extreme_value!` for overflow handling.
- Allow reverse constraint propagation through lossless decimal widening
  casts, so predicates whose literal has a different scale than the column
  (e.g. `CAST(a AS Decimal128(30, 15)) >= 0.055`) still narrow the column.
@github-actions github-actions Bot added logical-expr Logical plan and expressions physical-expr Changes to the physical-expr crates physical-plan Changes to the physical-plan crate labels Sep 27, 2026
@github-actions

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion-expr-common v55.1.0 (current)
       Built [  26.552s] (current)
     Parsing datafusion-expr-common v55.1.0 (current)
      Parsed [   0.018s] (current)
    Building datafusion-expr-common v55.1.0 (baseline)
       Built [  18.661s] (baseline)
     Parsing datafusion-expr-common v55.1.0 (baseline)
      Parsed [   0.018s] (baseline)
    Checking datafusion-expr-common v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.318s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  47.174s] datafusion-expr-common
    Building datafusion-physical-expr v55.1.0 (current)
       Built [  26.055s] (current)
     Parsing datafusion-physical-expr v55.1.0 (current)
      Parsed [   0.043s] (current)
    Building datafusion-physical-expr v55.1.0 (baseline)
       Built [  25.555s] (baseline)
     Parsing datafusion-physical-expr v55.1.0 (baseline)
      Parsed [   0.045s] (baseline)
    Checking datafusion-physical-expr v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.492s] 223 checks: 222 pass, 1 fail, 0 warn, 31 skip

--- failure inherent_method_missing: pub method removed or renamed ---

Description:
A publicly-visible method or associated fn is no longer available under its prior name. It may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.50.0/src/lints/inherent_method_missing.ron

Failed in:
  RangePartitioning::try_new_with_samples, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/3e99f63d9b038fa972f2e2054fb1a32de37b7a64/datafusion/physical-expr/src/partitioning.rs:277
  RangePartitioning::samples, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/3e99f63d9b038fa972f2e2054fb1a32de37b7a64/datafusion/physical-expr/src/partitioning.rs:305
  RangePartitioning::max_partition_count, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/3e99f63d9b038fa972f2e2054fb1a32de37b7a64/datafusion/physical-expr/src/partitioning.rs:320
  RangePartitioning::has_same_layout, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/3e99f63d9b038fa972f2e2054fb1a32de37b7a64/datafusion/physical-expr/src/partitioning.rs:330
  RangePartitioning::scale, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/3e99f63d9b038fa972f2e2054fb1a32de37b7a64/datafusion/physical-expr/src/partitioning.rs:343

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  53.253s] datafusion-physical-expr
    Building datafusion-physical-plan v55.1.0 (current)
       Built [  36.408s] (current)
     Parsing datafusion-physical-plan v55.1.0 (current)
      Parsed [   0.165s] (current)
    Building datafusion-physical-plan v55.1.0 (baseline)
       Built [  36.500s] (baseline)
     Parsing datafusion-physical-plan v55.1.0 (baseline)
      Parsed [   0.166s] (baseline)
    Checking datafusion-physical-plan v55.1.0 -> v55.1.0 (no change; assume patch)
     Checked [   0.908s] 223 checks: 223 pass, 31 skip
     Summary no semver update required
    Finished [  75.731s] datafusion-physical-plan

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Sep 28, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.70432% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.50%. Comparing base (acf5c89) to head (0900fc9).
⚠️ Report is 21 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/expr-common/src/interval_arithmetic.rs 88.42% 4 Missing and 10 partials ⚠️
datafusion/physical-plan/src/filter.rs 87.09% 4 Missing and 8 partials ⚠️
datafusion/physical-expr/src/expressions/cast.rs 82.60% 2 Missing and 6 partials ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #25790    +/-   ##
========================================
  Coverage   82.50%   82.50%            
========================================
  Files        1141     1141            
  Lines      438825   439135   +310     
  Branches   438825   439135   +310     
========================================
+ Hits       362050   362315   +265     
- Misses      54892    54907    +15     
- Partials    21883    21913    +30     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kosiew

kosiew commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

hi @shrivasshankar
I would love to review this after the merge conflicts are resolved.

@shrivasshankar

Copy link
Copy Markdown
Author

hi @shrivasshankar I would love to review this after the merge conflicts are resolved.

Thanks @kosiew! Conflicts are resolved now, ready for review.

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

@shrivasshankar,

Thanks for working on decimal filter selectivity. The approach is appropriate, and the focused tests cover important decimal boundaries and cast behavior.

Keeping decimal support specific to statistics estimation, rather than expanding the existing symmetric hash join eligibility gate, is the right architectural boundary. The shared expression traversal also avoids unnecessary duplication. However, enabling decimal types must not admit arithmetic propagation that violates source column type invariants.

I'm requesting changes because valid decimal arithmetic predicates can now fail filter construction or produce column statistics with incorrect decimal types. These are correctness regressions that need to be addressed before merging. The additional SQL-level and cast-boundary tests are useful but non-blocking suggestions.

) -> bool {
check_support_with(expr, schema, |data_type| {
is_datatype_supported(data_type)
|| matches!(

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.

Decimal arithmetic can propagate bounds with the wrong source type. For a: Decimal128(15,2) with statistics [1.00,10.00], a * Decimal128(2.00,15,2) > Decimal128(5.0000,31,4) makes FilterExec::try_new fail with Selectivity is out of limit: 8324028856.826859 because inverse propagation retains scale 4; a - 2.00 > 5.00 also publishes Decimal128(17,2) bounds for a Decimal128(15,2) column. Please restore source-typed bounds with conservative outward rounding, or exclude decimal arithmetic from statistics eligibility. Clamping selectivity alone is insufficient. Add regressions for multiplication construction/selectivity and subtraction min/max types.

let schema =
Schema::new(vec![Field::new("a", DataType::Decimal128(15, 2), false)]);
let input = Arc::new(StatisticsExec::new(
Statistics {

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.

Optional follow-up: consider a SQLLogicTest for a Q6-style decimal range with ordinary fractional literals and known min/max statistics, asserting EXPLAIN estimates and query results. The current mixed-scale test constructs the cast explicitly, so it does not cover SQL coercion through physical expression construction to statistics estimation.

Ok(())
}

#[test]

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.

Optional follow-up: consider representative containment tests for negative scales, cross-width widening, and bounds near precision limits. The exhaustive test covers only Decimal128 scale 2 to 3 near zero, while the Decimal32 to Decimal128 case currently checks eligibility rather than boundary propagation.

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

Labels

auto detected api change Auto detected API change logical-expr Logical plan and expressions physical-expr Changes to the physical-expr crates physical-plan Changes to the physical-plan crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Estimate Decimal filter selectivity

3 participants