Skip to content

feat: simplify contradicting and redundant predicates on a column - #25207

Open
wudidapaopao wants to merge 4 commits into
apache:mainfrom
wudidapaopao:simplify-predicates-contradictions
Open

feat: simplify contradicting and redundant predicates on a column#25207
wudidapaopao wants to merge 4 commits into
apache:mainfrom
wudidapaopao:simplify-predicates-contradictions

Conversation

@wudidapaopao

@wudidapaopao wudidapaopao commented Sep 11, 2026

Copy link
Copy Markdown

Which issue does this PR close?

Rationale for this change

simplify_predicates reduces the >/>= and the </<= comparisons on a column to their most restrictive bound, but never compares the two groups with each other, and only looks for contradictions between equalities. A filter no row can satisfy therefore still scans the table.

WHERE clause Before After
a > 3 AND a < 1 a > 3 AND a < 1 EmptyRelation
a > 1 AND a < 1 a > 1 AND a < 1 EmptyRelation
a >= 1 AND a < 1 a >= 1 AND a < 1 EmptyRelation
a = 7 AND a < 2 a = 7 AND a < 2 EmptyRelation
a = 7 AND a != 7 a = 7 AND a != 7 EmptyRelation
a = 7 AND a > 5 a = 7 AND a > 5 a = 7
a > 10 AND a != 5 a > 10 AND a != 5 a > 10

a >= 1 AND a <= 1 still simplifies to a = 1: bounds meeting at a value both admit stay satisfiable.

What changes are included in this PR?

  • fix: skip comparisons against a NULL literal when grouping. ScalarValue::try_cmp follows sort order, where NULL sits below every other value, so a > NULL AND a > 5 was reduced to a > 5 although a > NULL is never true. Reachable only through the public simplify_predicates, as SimplifyExpressions folds these first.
  • feat: compare the groups with each other. Bounds leaving no value, and an equality that contradicts another predicate, reduce the conjunction to false; an equality drops what it subsumes; != joins the analysis and is dropped once a bound excludes its value.
  • test: unit tests and sqllogictest cases.

Reducing to false is valid here because a Filter keeps a row only when its predicate is true, making NULL and false interchangeable.

What is the testing strategy for this PR?

Unit tests in simplify_predicates.rs cover each comparison operator against an equality, every combination of strict and inclusive bounds, != dropped and kept, and comparisons against NULL. simplify_predicates.slt checks the plans and drops two # TODO markers this PR implements.

Are there any user-facing changes?

Filters no row can satisfy no longer scan their input. Result sets and public APIs are unchanged.

…ication

`simplify_predicates` grouped every `column <op> literal` comparison by
column, including ones whose literal is NULL, and then reduced each group
with `ScalarValue::try_cmp`. That comparison follows sort order, where NULL
is an ordinary value below every other one, rather than SQL three-valued
logic. A predicate such as `a > NULL` was therefore treated as a real but
weaker lower bound and dropped as redundant:

    a > NULL AND a > 5   =>   a > 5

`a > NULL` never evaluates to true, so the conjunction matches no row while
the simplified `a > 5` does. Skip comparisons against a NULL literal when
grouping so they are carried through untouched.

Queries do not reach this today because `SimplifyExpressions` folds
comparisons with NULL literals before `PushDownFilter` runs, but
`simplify_predicates` is public and callers can hit it directly.
`simplify_predicates` reduced the `>`/`>=` and `<`/`<=` comparisons on a
column to their most restrictive bound, but never compared the two groups
with each other, and only looked for contradictions between equalities.
Conjunctions that no row can satisfy were therefore left in the plan, and
comparisons already implied by an equality were still evaluated per row.

Reason across the groups instead, taking the same approach DuckDB's
`FilterCombiner::AddFilter` does:

- Contradicting bounds reduce the conjunction to `false`, so that
  `EliminateFilter` and `PropagateEmptyRelation` can prune the plan they
  filter. `x > 6 AND x < 5` is unsatisfiable, and so is `x > 1 AND x < 1`
  because a strict comparison excludes the value the bounds share. Note
  that DuckDB stops short of the latter.
- An equality pins the column to a single value, so it subsumes every
  other predicate that value satisfies, and contradicts the rest:
  `x = 5 AND x > 3` becomes `x = 5`, while `x = 5 AND x > 5` becomes
  `false`.
- `!=` predicates now take part in the analysis. One is dropped once a
  bound already excludes its value, as in `x > 10 AND x != 5`, and one
  that contradicts an equality reduces the conjunction to `false`.

A column whose predicates contradict each other now short circuits the
whole list, since predicates on other columns cannot make the conjunction
true again.

`false` stands for a conjunction that never evaluates to true, which under
three-valued logic includes evaluating to NULL. That is only equivalent for
the predicates of a `Filter`, which keeps a row solely when they evaluate
to true, and is where this runs.
@github-actions github-actions Bot added optimizer Optimizer rules sqllogictest SQL Logic Tests (.slt) labels Sep 11, 2026
Add unit tests for the reasoning that spans the comparison groups of a
column, and sqllogictest cases that check the plans it produces:

- an equality subsuming the predicates its value satisfies, and being
  rejected by each of the six comparison operators;
- bounds that leave no value, for every combination of strict and
  inclusive comparisons, next to the inclusive pair that admits one;
- one column's contradiction discarding the predicates on other columns;
- `!=` being dropped once a bound excludes its value, and kept otherwise;
- comparisons against a NULL literal staying untouched, which only a unit
  test can reach since `SimplifyExpressions` folds them beforehand.
@wudidapaopao
wudidapaopao force-pushed the simplify-predicates-contradictions branch from e4043ff to 9fb1c19 Compare September 11, 2026 20:31
@wudidapaopao

Copy link
Copy Markdown
Author

Hi! This is my first contribution, so the CI workflows need a committer's approval to run. Could someone help trigger them?
cc @alamb @adriangb

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.02564% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.94%. Comparing base (4048898) to head (9fb1c19).
⚠️ Report is 49 commits behind head on main.

Files with missing lines Patch % Lines
...er/src/simplify_expressions/simplify_predicates.rs 91.02% 3 Missing and 11 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #25207      +/-   ##
==========================================
+ Coverage   81.80%   81.94%   +0.13%     
==========================================
  Files        1130     1133       +3     
  Lines      417754   423654    +5900     
  Branches   417754   423654    +5900     
==========================================
+ Hits       341754   347143    +5389     
- Misses      55875    55910      +35     
- Partials    20125    20601     +476     

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

Grouping `!=` predicates by column moves them behind the ones that stay
ungrouped, so `p_brand != 'Brand#45'` now follows `p_size IN (...)` in the
conjunction. The predicates themselves are unchanged.
@wudidapaopao

wudidapaopao commented Sep 12, 2026

Copy link
Copy Markdown
Author

Updated the TPC-H q16 plan. The filter changed from

p_brand != 'Brand#45' AND p_size IN (...) AND p_type NOT LIKE 'MEDIUM POLISHED%'

to

p_size IN (...) AND p_brand != 'Brand#45' AND p_type NOT LIKE 'MEDIUM POLISHED%'

simplify_predicates now groups NotEq by column, and grouped predicates are emitted after the ungrouped ones, so p_brand != 'Brand#45' no longer comes first. The predicates themselves are unchanged.

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

Labels

optimizer Optimizer rules sqllogictest SQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Simplify predicate expressions like 'a > 1 and a < 1' to constant false

2 participants