[SPARK-59348][SQL] Rewrite match-all LIKE '%' to IsNotNull in predicates - #58637
Open
david-mollitor-db wants to merge 1 commit into
Open
[SPARK-59348][SQL] Rewrite match-all LIKE '%' to IsNotNull in predicates#58637david-mollitor-db wants to merge 1 commit into
david-mollitor-db wants to merge 1 commit into
Conversation
`col LIKE '%'` (a pattern of only unescaped `%`) matches every non-null value, so a filter
`WHERE col LIKE '%'` keeps exactly the non-null rows. Spark evaluates it as a per-row regex
(`.*`) and pushes nothing to the data source.
This rewrites such patterns, in null-rejecting predicate positions, to `IsNotNull(col)`:
Filter (col LIKE '%') ==> Filter (isnotnull(col))
`IsNotNull` is far cheaper than a per-row regex, and it pushes down (Parquet/ORC skip row
groups via null-count statistics). If `col` is non-nullable, `IsNotNull(col)` then folds to
`true` and downstream `PruneFilters`/`NullPropagation` drop the filter entirely. The pattern
shows up from dynamically-generated SQL (an empty search box becoming `LIKE '%'`).
`col LIKE '%'` returns `true` for a non-null value and `null` for `null`, whereas
`IsNotNull(col)` returns `false` for `null`. They are equivalent only in null-rejecting
predicate positions, where `null` is treated as `false`. The rewrite is therefore implemented
in `ReplaceNullWithFalseInPredicate`, which already applies this kind of null-as-false
simplification (see its `Not(In(...))` handling): it targets exactly the null-rejecting plan
nodes and recurses only through null-preserving operators (`And`, `Or`, `If`, `CaseWhen`), not
into `Not` -- so `NOT (col LIKE '%')` and `col LIKE '%'` in a projection are left untouched,
where `null` vs `false` would be observable.
The rewrite fires only for a literal pattern of one or more `%` with the escape character not
being `%`; `LIKE ''`, prefixes, and `_` patterns are excluded. No collation gate is needed --
`'%'` matches every non-null value under any collation. `LikeAll`/`LikeAny` are out of scope.
Generated-by: Claude Opus 4.8
david-mollitor-db
force-pushed
the
like-percent-to-isnotnull
branch
from
September 9, 2026 13:21
ebb58fa to
7d9ab94
Compare
uros-b
approved these changes
Sep 10, 2026
Member
|
Nice rewrite, thank you @david-mollitor-db! Adding @stevomitric to also review this PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
col LIKE '%'(a pattern of only unescaped%) matches every non-null value, so a filterWHERE col LIKE '%'keeps exactly the non-null rows. Today Spark evaluates it as a per-rowregex (
.*) and pushes nothing to the data source.This PR rewrites such patterns, in null-rejecting predicate positions, to
IsNotNull(col):The rewrite is implemented in
ReplaceNullWithFalseInPredicate, which already performs thisclass of null-as-false simplification (cf. its existing
Not(In(...))/Not(InSet(...))handling). It fires only for a literal pattern of one or more
%with the escape character notbeing
%;LIKE '', prefixes, and_patterns are excluded.LikeAll/LikeAnyare out ofscope.
Why are the changes needed?
IsNotNullis far cheaper than compiling and running a regex per row.IsNotNullpushes down: Parquet/ORC skip row groups via null-count statistics; the rawLIKEregex does not.
colis non-nullable,IsNotNull(col)then folds totrueand downstreamPruneFilters/NullPropagationdrop the filter entirely.The pattern shows up in practice from dynamically-generated SQL (e.g. an empty search box
becoming
LIKE '%').Correctness.
col LIKE '%'returnstruefor a non-null value andnullfornull,whereas
IsNotNull(col)returnsfalsefornull. They are equivalent only in null-rejectingpredicate positions, where
nullis treated asfalse.ReplaceNullWithFalseInPredicatetargets exactly those plan nodes (
Filter,Joincondition,MergeIntoTable,DeleteFromTable,UpdateTable,ReplaceData,WriteDelta) and recurses only throughnull-preserving operators (
And,Or,If,CaseWhen), not intoNot— soNOT (col LIKE '%')andcol LIKE '%'in a projection are left untouched, wherenullvsfalsewould be observable. No collation gate is needed:'%'matches every non-null valueunder any collation.
Does this PR introduce any user-facing change?
No. Query results are identical; this is a performance improvement.
How was this patch tested?
New tests in
ReplaceNullWithFalseInPredicateSuite:'%','%%','%%%'->IsNotNull; throughAND/OR; in a join condition.'a%', empty'',%escaped (ESCAPE '%'), underNOT, and in aprojection (the null-vs-false safety boundary).
build/sbt 'catalyst/testOnly *ReplaceNullWithFalseInPredicateSuite'passes (36/36); scalastyleclean.
Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 4.8