Fix const-simplification for index expressions and predicate
authorRichard Guo <rguo@postgresql.org>
Tue, 9 Dec 2025 07:56:26 +0000 (16:56 +0900)
committerRichard Guo <rguo@postgresql.org>
Tue, 9 Dec 2025 07:56:26 +0000 (16:56 +0900)
Similar to the issue with constraint and statistics expressions fixed
in 317c117d6, index expressions and predicate can also suffer from
incorrect reduction of NullTest clauses during const-simplification,
due to unfixed varnos and the use of a NULL root.  It has been
reported that this issue can cause the planner to fail to pick up a
partial index that it previously matched successfully.

Because we need to cache the const-simplified index expressions and
predicate in the relcache entry, we cannot fix the Vars before
applying eval_const_expressions.  To ensure proper reduction of
NullTest clauses, this patch runs eval_const_expressions a second time
-- after the Vars have been fixed and with a valid root.

It could be argued that the additional call to eval_const_expressions
might increase planning time, but I don't think that's a concern.  It
only runs when index expressions and predicate are present; it is
relatively cheap when run on small expression trees (which is
typically the case for index expressions and predicate), and it runs
on expressions that have already been const-simplified once, making
the second pass even cheaper.  In return, in cases like the one
reported, it allows the planner to match and use partial indexes,
which can lead to significant execution-time improvements.

Bug: #19007
Reported-by: Bryan Fox <bryfox@gmail.com>
Author: Richard Guo <guofenglinux@gmail.com>
Discussion: https://postgr.es/m/19007-4cc6e252ed8aa54a@postgresql.org

src/backend/optimizer/util/plancat.c
src/test/regress/expected/join.out
src/test/regress/expected/predicate.out
src/test/regress/sql/join.sql
src/test/regress/sql/predicate.sql

index 07f92fac239ad51f9b07d679697f5dc75e776ab6..ed0dac37f5153abec3d095cebd2841fd3e934d9b 100644 (file)
@@ -429,13 +429,32 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
             * modify the copies we obtain from the relcache to have the
             * correct varno for the parent relation, so that they match up
             * correctly against qual clauses.
+            *
+            * After fixing the varnos, we need to run the index expressions
+            * and predicate through const-simplification again, using a valid
+            * "root".  This ensures that NullTest quals for Vars can be
+            * properly reduced.
             */
            info->indexprs = RelationGetIndexExpressions(indexRelation);
            info->indpred = RelationGetIndexPredicate(indexRelation);
-           if (info->indexprs && varno != 1)
-               ChangeVarNodes((Node *) info->indexprs, 1, varno, 0);
-           if (info->indpred && varno != 1)
-               ChangeVarNodes((Node *) info->indpred, 1, varno, 0);
+           if (info->indexprs)
+           {
+               if (varno != 1)
+                   ChangeVarNodes((Node *) info->indexprs, 1, varno, 0);
+
+               info->indexprs = (List *)
+                   eval_const_expressions(root, (Node *) info->indexprs);
+           }
+           if (info->indpred)
+           {
+               if (varno != 1)
+                   ChangeVarNodes((Node *) info->indpred, 1, varno, 0);
+
+               info->indpred = (List *)
+                   eval_const_expressions(root,
+                                          (Node *) make_ands_explicit(info->indpred));
+               info->indpred = make_ands_implicit((Expr *) info->indpred);
+           }
 
            /* Build targetlist using the completed indexprs data */
            info->indextlist = build_index_tlist(root, info, relation);
@@ -1047,8 +1066,13 @@ infer_arbiter_indexes(PlannerInfo *root)
 
        /* Expression attributes (if any) must match */
        idxExprs = RelationGetIndexExpressions(idxRel);
-       if (idxExprs && varno != 1)
-           ChangeVarNodes((Node *) idxExprs, 1, varno, 0);
+       if (idxExprs)
+       {
+           if (varno != 1)
+               ChangeVarNodes((Node *) idxExprs, 1, varno, 0);
+
+           idxExprs = (List *) eval_const_expressions(root, (Node *) idxExprs);
+       }
 
        /*
         * If arbiterElems are present, check them.  (Note that if a
@@ -1109,8 +1133,16 @@ infer_arbiter_indexes(PlannerInfo *root)
            continue;
 
        predExprs = RelationGetIndexPredicate(idxRel);
-       if (predExprs && varno != 1)
-           ChangeVarNodes((Node *) predExprs, 1, varno, 0);
+       if (predExprs)
+       {
+           if (varno != 1)
+               ChangeVarNodes((Node *) predExprs, 1, varno, 0);
+
+           predExprs = (List *)
+               eval_const_expressions(root,
+                                      (Node *) make_ands_explicit(predExprs));
+           predExprs = make_ands_implicit((Expr *) predExprs);
+       }
 
        /*
         * Partial indexes affect each form of ON CONFLICT differently: if a
index 0e82ca1867a7dc12617c136875dbe2d9f5d97f3a..2a0a457a7d9f896a02e9e699947cdf0ccb524c72 100644 (file)
@@ -9547,7 +9547,7 @@ left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
          Output: j2.id1, j2.id2
 (8 rows)
 
-create unique index j1_id2_idx on j1(id2) where id2 is not null;
+create unique index j1_id2_idx on j1(id2) where id2 > 0;
 -- ensure we don't use a partial unique index as unique proofs
 explain (verbose, costs off)
 select * from j1
index 66fb0854b882b92e89768c7c9817e3c72b4e3536..94c343fe0309ad66638fd5d42d11b290700e86a5 100644 (file)
@@ -443,3 +443,44 @@ SELECT * FROM pred_tab2, pred_tab1 WHERE pred_tab1.a IS NULL OR pred_tab1.b < 2;
 RESET constraint_exclusion;
 DROP TABLE pred_tab1;
 DROP TABLE pred_tab2;
+-- Validate that NullTest quals in index expressions and predicate are reduced correctly
+CREATE TABLE pred_tab (a int, b int NOT NULL, c int NOT NULL);
+INSERT INTO pred_tab SELECT i, i, i FROM generate_series(1, 1000) i;
+CREATE INDEX pred_tab_exprs_idx ON pred_tab ((a < 5 AND b IS NOT NULL AND c IS NOT NULL));
+CREATE INDEX pred_tab_pred_idx ON pred_tab (a) WHERE b IS NOT NULL AND c IS NOT NULL;
+ANALYZE pred_tab;
+-- Ensure that index pred_tab_exprs_idx is used
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (a < 5 AND b IS NOT NULL AND c IS NOT NULL) IS TRUE;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Index Scan using pred_tab_exprs_idx on pred_tab
+   Index Cond: ((a < 5) = true)
+(2 rows)
+
+SELECT * FROM pred_tab WHERE (a < 5 AND b IS NOT NULL AND c IS NOT NULL) IS TRUE;
+ a | b | c 
+---+---+---
+ 1 | 1 | 1
+ 2 | 2 | 2
+ 3 | 3 | 3
+ 4 | 4 | 4
+(4 rows)
+
+-- Ensure that index pred_tab_pred_idx is used
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL;
+                   QUERY PLAN                   
+------------------------------------------------
+ Index Scan using pred_tab_pred_idx on pred_tab
+   Index Cond: (a < 3)
+(2 rows)
+
+SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL;
+ a | b | c 
+---+---+---
+ 1 | 1 | 1
+ 2 | 2 | 2
+(2 rows)
+
+DROP TABLE pred_tab;
index c6b8b09a381e1cf410686e572deb86895e1ae2df..c47118e9291a3f6d3e4687bb7deb0b5e528b773b 100644 (file)
@@ -3599,7 +3599,7 @@ explain (verbose, costs off)
 select * from j1
 left join j2 on j1.id1 = j2.id1 where j1.id2 = 1;
 
-create unique index j1_id2_idx on j1(id2) where id2 is not null;
+create unique index j1_id2_idx on j1(id2) where id2 > 0;
 
 -- ensure we don't use a partial unique index as unique proofs
 explain (verbose, costs off)
index 32302d60b6d04af07f6f47939ecba43e4d0aa32b..7d4fda1bc183e2c81bc2c25f9ed7e51815ba4392 100644 (file)
@@ -221,3 +221,22 @@ SELECT * FROM pred_tab2, pred_tab1 WHERE pred_tab1.a IS NULL OR pred_tab1.b < 2;
 RESET constraint_exclusion;
 DROP TABLE pred_tab1;
 DROP TABLE pred_tab2;
+
+-- Validate that NullTest quals in index expressions and predicate are reduced correctly
+CREATE TABLE pred_tab (a int, b int NOT NULL, c int NOT NULL);
+INSERT INTO pred_tab SELECT i, i, i FROM generate_series(1, 1000) i;
+CREATE INDEX pred_tab_exprs_idx ON pred_tab ((a < 5 AND b IS NOT NULL AND c IS NOT NULL));
+CREATE INDEX pred_tab_pred_idx ON pred_tab (a) WHERE b IS NOT NULL AND c IS NOT NULL;
+ANALYZE pred_tab;
+
+-- Ensure that index pred_tab_exprs_idx is used
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE (a < 5 AND b IS NOT NULL AND c IS NOT NULL) IS TRUE;
+SELECT * FROM pred_tab WHERE (a < 5 AND b IS NOT NULL AND c IS NOT NULL) IS TRUE;
+
+-- Ensure that index pred_tab_pred_idx is used
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL;
+SELECT * FROM pred_tab WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL;
+
+DROP TABLE pred_tab;