From 44527135526087bca6ebed80d010d3334a4ab5ff Mon Sep 17 00:00:00 2001
From: Ilia Evdokimov <ilya.evdokimov@tantorlabs.ru>
Date: Thu, 22 Oct 2026 12:31:03 +0300
Subject: [PATCH v1] Fix NOT selectivity estimates for nullable boolean
 expressions

Use booltestsel(IS_FALSE) when statistics are available for the argument
of NOT, so NULL values are excluded from the estimated matching rows.
Retain the existing recursive estimate when statistics are unavailable.
---
 src/backend/optimizer/path/clausesel.c | 26 +++++++++++++++++++-------
 1 file changed, 19 insertions(+), 7 deletions(-)

diff --git a/src/backend/optimizer/path/clausesel.c b/src/backend/optimizer/path/clausesel.c
index 25c4d177ad9..631752dbced 100644
--- a/src/backend/optimizer/path/clausesel.c
+++ b/src/backend/optimizer/path/clausesel.c
@@ -797,13 +797,25 @@ clause_selectivity_ext(PlannerInfo *root,
 	}
 	else if (is_notclause(clause))
 	{
-		/* inverse of the selectivity of the underlying clause */
-		s1 = 1.0 - clause_selectivity_ext(root,
-										  (Node *) get_notclausearg((Expr *) clause),
-										  varRelid,
-										  jointype,
-										  sjinfo,
-										  use_extended_stats);
+		Node	   *arg = (Node *) get_notclausearg((Expr *) clause);
+		VariableStatData vardata;
+
+		examine_variable(root, arg, varRelid, &vardata);
+		if (HeapTupleIsValid(vardata.statsTuple))
+		{
+			/* Use statistics to select FALSE values, excluding NULLs. */
+			s1 = booltestsel(root, IS_FALSE, arg, varRelid, jointype, sjinfo);
+		}
+		else
+		{
+			/* Otherwise, estimate the underlying clause */
+			s1 = 1.0 - clause_selectivity_ext(root, arg,
+											  varRelid,
+											  jointype,
+											  sjinfo,
+											  use_extended_stats);
+		}
+		ReleaseVariableStats(vardata);
 	}
 	else if (is_andclause(clause))
 	{
-- 
2.34.1

