Skip to content

planner: simplify NOT NOT in WHERE predicates for left-join right-side pruning#69939

Open
sijie-Z wants to merge 3 commits into
pingcap:masterfrom
sijie-Z:fix/not-not-where-predicate-simplification
Open

planner: simplify NOT NOT in WHERE predicates for left-join right-side pruning#69939
sijie-Z wants to merge 3 commits into
pingcap:masterfrom
sijie-Z:fix/not-not-where-predicate-simplification

Conversation

@sijie-Z

@sijie-Z sijie-Z commented Jul 20, 2026

Copy link
Copy Markdown

What problem does this PR solve?

Issue Number: close #69927

Problem Summary:

NOT NOT a.c4 = 50 is logically equivalent to a.c4 = 50, but TiDB does not simplify the double negation before constraint derivation and left-join pruning. This causes a ~7.5x slowdown: the right table is scanned unnecessarily when the WHERE clause already contradicts the join condition.

What changed and how does it work?

In pushNotAcrossExpr (pkg/expression/util.go), when the UnaryNot case strips the outer NOT (not=false) and recurses into the inner child, the early return path now returns childExpr with changed=true instead of returning expr with changed=false. This allows the NOT NOT to be stripped in a single step rather than requiring the caller context to provide the not flag.

This is safe because NOT NOT X is logically X (with is_true_with_null semantics already handled by wrapWithIsTrue at the inner UnaryNot level).

Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No need to test

Side effects

  • Performance regression: Consumes more CPU
  • Performance regression: Consumes more Memory
  • Breaking backward compatibility

Documentation

  • Affects user behaviors
  • Contains syntax changes
  • Contains variable changes
  • Contains experimental features
  • Changes MySQL compatibility

Release note

Fix planner simplification of NOT NOT predicates in WHERE clauses, enabling left-join right-side pruning and avoiding ~7.5x query slowdown.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed simplification of nested NOT predicates, including double negation.
    • Improved predicate processing for constraint derivation and join pruning, helping avoid unnecessary scans and potential query slowdowns.
  • Tests

    • Added regression coverage for deeply nested NOT expressions.

…e pruning

pushNotAcrossExpr had logic that was right for NOT NOT on comparison ops like EQ.
The UnaryNot case recursively processes the child with the complement of .
For NOT NOT (a=1):
1. outer UnaryNot: not=false -> recurse on wrapWithIsTrue(child) with not=true
2. inner UnaryNot: not=true -> recurse on inner child with not=false
3. EQ with not=false: unchanged (no not to apply)

At step 3, the comparison is EQ, not=false, so it returns unchanged.
At step 2, !changed && !not -> we return  (false changed). Bug was here.
Since  was false at level 1 (!not, the changed got lost), the bug
returned the full NOT NOT expression unchanged.

Trivially, NOT NOT X is logically X (with is_true/null semantics preserved).
The fix: when we strip the outer NOT (not=false case) and the recursion didn't
modify the child, return childExpr (the simplified inner) anyway with changed=true.
This shortcuts the double-negation in one step.

Closes pingcap#69927

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ti-chi-bot ti-chi-bot Bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. do-not-merge/needs-tests-checked needs-ok-to-test Indicates a PR created by contributors and need ORG member send '/ok-to-test' to start testing. contribution This PR is from a community contributor. size/S Denotes a PR that changes 10-29 lines, ignoring generated files. labels Jul 20, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

Hi @sijie-Z. Thanks for your PR.

I'm waiting for a pingcap member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@ti-chi-bot ti-chi-bot Bot added the first-time-contributor Indicates that the PR was contributed by an external member and is a first-time contributor. label Jul 20, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

Welcome @sijie-Z!

It looks like this is your first PR to pingcap/tidb 🎉.

I'm the bot to help you request reviewers, add labels and more, See available commands.

We want to make sure your contribution gets all the attention it needs!



Thank you, and welcome to pingcap/tidb. 😃

@pingcap-cla-assistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


sijie-Z seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change updates NOT pushdown to return a recursively simplified child expression, adds coverage for seven nested NOT operators, and documents the resulting double-negation planner fix.

Changes

Double-negation simplification

Layer / File(s) Summary
Fix NOT pushdown and validate double negation
pkg/expression/util.go, pkg/expression/util_test.go, pr_body.md
pushNotAcrossExpr now returns the simplified child when removing an outer NOT; a seven-level nested-NOT regression case verifies the resulting expression, and the PR description documents the fix and release note.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

  • #69926 — Concerns related double-negation simplification through pushNotAcrossExpr, including predicates involving subqueries or IN expressions.

Poem

A rabbit found two NOTs in a row,
And helped the inner truth now show.
The planner hopped with cleaner flair,
Leaving needless scans nowhere.
Seven NOTs now rest quite neat—
Carrots cheer the simpler feat!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the double-negation simplification for WHERE predicates and right-side pruning.
Linked Issues check ✅ Passed The change matches #69927 by simplifying NOT NOT expressions so pruning can occur, and the added test covers the regression.
Out of Scope Changes check ✅ Passed No unrelated code changes are evident beyond the targeted NOT NOT simplification and regression test.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
pkg/expression/notnot_test.go (1)

11-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extend the existing TestPushDownNot instead of adding duplicate coverage.

pkg/expression/util_test.go already verifies that NOT NOT (a=1) simplifies to (a=1). Moving this assertion into that existing test keeps the change smaller and avoids a new duplicate test scaffold.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/expression/notnot_test.go` around lines 11 - 25, Remove the standalone
TestPushDownNotDoubleNegation test and move its NOT NOT simplification setup and
assertion into the existing TestPushDownNot in util_test.go. Preserve the
assertion that PushDownNot returns an expression equal to the original equality
expression, while avoiding duplicate test scaffolding.

Source: Coding guidelines

pkg/expression/util.go (1)

1088-1089: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the redundant conditional.

Line 1089 now returns the same value as the unconditional return on Line 1091, so !changed && !not no longer controls behavior. Collapse this to a single return childExpr, true to avoid misleading future readers.

Proposed simplification
-			if !changed && !not {
-				return childExpr, true
-			}
 			return childExpr, true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/expression/util.go` around lines 1088 - 1089, In the surrounding
expression simplification logic, remove the redundant conditional guarding the
return and keep a single unconditional return of childExpr, true. Update the
code near the visible changed and not checks without altering other behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/expression/notnot_test.go`:
- Line 1: Add the standard TiDB copyright and Apache 2.0 license header at the
top of the new notnot test file, before the package declaration, matching the
header format used by existing TiDB test files.
- Around line 3-9: Add the missing imports for the packages that provide
mock.NewContext and errors.RedactLogDisable in notnot_test.go, preserving the
existing test references so the new test compiles.

---

Nitpick comments:
In `@pkg/expression/notnot_test.go`:
- Around line 11-25: Remove the standalone TestPushDownNotDoubleNegation test
and move its NOT NOT simplification setup and assertion into the existing
TestPushDownNot in util_test.go. Preserve the assertion that PushDownNot returns
an expression equal to the original equality expression, while avoiding
duplicate test scaffolding.

In `@pkg/expression/util.go`:
- Around line 1088-1089: In the surrounding expression simplification logic,
remove the redundant conditional guarding the return and keep a single
unconditional return of childExpr, true. Update the code near the visible
changed and not checks without altering other behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 67293e68-f537-49eb-aa44-62b3e55a77ed

📥 Commits

Reviewing files that changed from the base of the PR and between cae12c1 and ada0fc0.

📒 Files selected for processing (2)
  • pkg/expression/notnot_test.go
  • pkg/expression/util.go

Comment thread pkg/expression/notnot_test.go Outdated
@@ -0,0 +1,25 @@
package expression

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the standard TiDB license header.

This is a new Go file without the required copyright and Apache 2.0 license header. Copy the standard header from an existing TiDB test file.

🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 1-1: : mkdir /home/jailuser/.cache/go/go-build1556602990/b8779/: no space left on device

(typecheck)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/expression/notnot_test.go` at line 1, Add the standard TiDB copyright and
Apache 2.0 license header at the top of the new notnot test file, before the
package declaration, matching the header format used by existing TiDB test
files.

Source: Coding guidelines

Comment thread pkg/expression/notnot_test.go Outdated
@ti-chi-bot ti-chi-bot Bot added size/M Denotes a PR that changes 30-99 lines, ignoring generated files. and removed size/S Denotes a PR that changes 10-29 lines, ignoring generated files. labels Jul 20, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign gengliqi for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot

ti-chi-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

[FORMAT CHECKER NOTIFICATION]

Notice: To remove the do-not-merge/needs-tests-checked label, please finished the tests then check the finished items in description.

For example:

Tests <!-- At least one of them must be included. -->

- [x] Unit test
- [ ] Integration test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No code

‼️ Must keep the HTML comments <!-- At least one of them must be included. -->

📖 For more info, you can check the "Contribute Code" section in the development guide.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/expression/util_test.go`:
- Around line 331-341: Update the regression test in the NOT-chain section to
construct an equality expression for (a=1) before applying the seven UnaryNot
wrappers. Change the expected result to the corresponding inequality predicate
(a!=1), while preserving the existing PushDownNot assertion structure.

In `@pr_body.md`:
- Line 19: Update the checked unit-test entry in the PR checklist to reference
pkg/expression/util_test.go and the actual TestPushDownNot test name, replacing
the incorrect notnot_test.go and TestPushDownNotDoubleNegation references.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bca91f20-b708-4772-a6c2-a94749ae5440

📥 Commits

Reviewing files that changed from the base of the PR and between 1a994d5 and 35aa648.

📒 Files selected for processing (3)
  • pkg/expression/util.go
  • pkg/expression/util_test.go
  • pr_body.md
💤 Files with no reviewable changes (1)
  • pkg/expression/util.go

Comment on lines +331 to +341

// Regression: NOT NOT NOT (a=1) should simplify to a!=1
notFunc = newFunctionWithMockCtx(ast.UnaryNot, col)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc) // 7 NOTs
ret = PushDownNot(ctx, notFunc)
require.True(t, ret.Equal(ctx, newFunctionWithMockCtx(ast.UnaryNot, newFunctionWithMockCtx(ast.IsTruthWithNull, col))))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test the documented (a=1) regression, not just a column.

The comment says this covers seven NOTs around (a=1), but the constructed chain wraps col directly. As written, the test does not exercise the comparison predicate from the PR objective. Build eqFunc first, wrap that expression seven times, and assert the corresponding a != 1 result.

Proposed fix
-	// Regression: NOT NOT NOT (a=1) should simplify to a!=1
-	notFunc = newFunctionWithMockCtx(ast.UnaryNot, col)
+	// Regression: seven NOTs around (a=1) should simplify to a!=1
+	eqFunc = newFunctionWithMockCtx(ast.EQ, col, NewOne())
+	notFunc = newFunctionWithMockCtx(ast.UnaryNot, eqFunc)
...
-	require.True(t, ret.Equal(ctx, newFunctionWithMockCtx(ast.UnaryNot, newFunctionWithMockCtx(ast.IsTruthWithNull, col))))
+	require.True(t, ret.Equal(ctx, newFunctionWithMockCtx(ast.NE, col, NewOne())))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Regression: NOT NOT NOT (a=1) should simplify to a!=1
notFunc = newFunctionWithMockCtx(ast.UnaryNot, col)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc) // 7 NOTs
ret = PushDownNot(ctx, notFunc)
require.True(t, ret.Equal(ctx, newFunctionWithMockCtx(ast.UnaryNot, newFunctionWithMockCtx(ast.IsTruthWithNull, col))))
// Regression: seven NOTs around (a=1) should simplify to a!=1
eqFunc = newFunctionWithMockCtx(ast.EQ, col, NewOne())
notFunc = newFunctionWithMockCtx(ast.UnaryNot, eqFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc)
notFunc = newFunctionWithMockCtx(ast.UnaryNot, notFunc) // 7 NOTs
ret = PushDownNot(ctx, notFunc)
require.True(t, ret.Equal(ctx, newFunctionWithMockCtx(ast.NE, col, NewOne())))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/expression/util_test.go` around lines 331 - 341, Update the regression
test in the NOT-chain section to construct an equality expression for (a=1)
before applying the seven UnaryNot wrappers. Change the expected result to the
corresponding inequality predicate (a!=1), while preserving the existing
PushDownNot assertion structure.

Comment thread pr_body.md

Tests

- [x] Unit test — added TestPushDownNotDoubleNegation in pkg/expression/notnot_test.go

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the unit-test file and test name.

The checklist references pkg/expression/notnot_test.go and TestPushDownNotDoubleNegation, but the supplied change is in pkg/expression/util_test.go within TestPushDownNot. Update this entry so the PR documentation accurately identifies the regression coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pr_body.md` at line 19, Update the checked unit-test entry in the PR
checklist to reference pkg/expression/util_test.go and the actual
TestPushDownNot test name, replacing the incorrect notnot_test.go and
TestPushDownNotDoubleNegation references.

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

Labels

contribution This PR is from a community contributor. do-not-merge/needs-tests-checked first-time-contributor Indicates that the PR was contributed by an external member and is a first-time contributor. needs-ok-to-test Indicates a PR created by contributors and need ORG member send '/ok-to-test' to start testing. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TiDB does not simplify NOT NOT in a WHERE predicate, preventing LEFT JOIN right-side pruning

1 participant