From 2bd319841e0ddf12b06dad1a583aabafa7d94032 Mon Sep 17 00:00:00 2001
From: Manuel Reyes Bravo <manuelreyesbravo@gmail.com>
Date: Sun, 4 Oct 2026 15:38:09 -0300
Subject: [PATCH v1] Make OR => OR predicate proofs linear for corresponding
 arms

create_bitmap_scan_plan() checks whether each scan clause is implied by
the indexquals of the bitmap plan.  For an OR clause turned into a
BitmapOr, such as a row-value IN list on a multicolumn index, the
indexquals are an OR whose arms were built from the clause's arms one
by one, so arm i of one corresponds to arm i of the other.  The
OR-clause => OR-clause rule in predicate_implied_by_recurse() searched
the predicate's arms from the first one for each arm of the clause, so
that test took a number of proof attempts quadratic in the number of
arms, each failing attempt possibly building and evaluating a test
expression in operator_predicate_proof().  With 2000 entries in
"c = 1 AND (a, b) IN (...)" planning took about 370 ms.

When both sides are plain OR lists, try the predicate's arm at the same
position first, and the others only if that fails.  The result of the
proof is unchanged; when the arms do not correspond, this costs one
more proof attempt per arm.

The test_predtest module gains cases where the arms correspond, are
swapped, and differ in number.

Reported-by: Stefan Guha
Discussion: https://postgr.es/m/859cceb0-2be3-4f01-8235-b3d1786eeb72@stefanguha.com
---
 src/backend/optimizer/util/predtest.c         | 29 +++++++++-
 .../test_predtest/expected/test_predtest.out  | 58 +++++++++++++++++++
 .../test_predtest/sql/test_predtest.sql       | 22 +++++++
 3 files changed, 108 insertions(+), 1 deletion(-)

diff --git a/src/backend/optimizer/util/predtest.c b/src/backend/optimizer/util/predtest.c
index 690a23d619a..2a735033ae2 100644
--- a/src/backend/optimizer/util/predtest.c
+++ b/src/backend/optimizer/util/predtest.c
@@ -296,6 +296,8 @@ predicate_implied_by_recurse(Node *clause, Node *predicate,
 	PredIterInfoData pred_info;
 	PredClass	pclass;
 	bool		result;
+	List	   *pargs;
+	int			cpos;
 
 	/* skip through RestrictInfo */
 	Assert(clause != NULL);
@@ -393,15 +395,40 @@ predicate_implied_by_recurse(Node *clause, Node *predicate,
 					/*
 					 * OR-clause => OR-clause if each of A's items implies any
 					 * of B's items.  Messy but can't do it any more simply.
+					 *
+					 * When both are plain OR lists, first try B's item at the
+					 * same position as A's.  create_bitmap_scan_plan() tests
+					 * a scan clause against the indexquals of the BitmapOr
+					 * built from it arm by arm, so the arms correspond in
+					 * order, and this makes the test take a number of proof
+					 * attempts linear rather than quadratic in the number of
+					 * arms.  The result is the same either way.
 					 */
+					pargs = NIL;
+					if (is_orclause(clause) && is_orclause(predicate))
+						pargs = ((BoolExpr *) predicate)->args;
+					cpos = 0;
 					result = true;
 					iterate_begin(citem, clause, clause_info)
 					{
 						bool		presult = false;
+						Node	   *tried = NULL;
+
+						if (cpos < list_length(pargs))
+						{
+							tried = (Node *) list_nth(pargs, cpos);
+							presult = predicate_implied_by_recurse(citem,
+																   tried,
+																   weak);
+						}
+						cpos++;
 
 						iterate_begin(pitem, predicate, pred_info)
 						{
-							if (predicate_implied_by_recurse(citem, pitem,
+							if (presult)
+								break;
+							if (pitem != tried &&
+								predicate_implied_by_recurse(citem, pitem,
 															 weak))
 							{
 								presult = true;
diff --git a/src/test/modules/test_predtest/expected/test_predtest.out b/src/test/modules/test_predtest/expected/test_predtest.out
index ad82b4f8f91..f8225b82a7b 100644
--- a/src/test/modules/test_predtest/expected/test_predtest.out
+++ b/src/test/modules/test_predtest/expected/test_predtest.out
@@ -431,6 +431,64 @@ w_i_holds         | t
 s_r_holds         | f
 w_r_holds         | f
 
+-- OR-clause => OR-clause tries the item at the same position first; check
+-- arms that correspond, arms that don't, and lists of different lengths
+select * from test_predtest($$
+select x or z, (x and y) or (z and w)
+from booleans
+$$);
+-[ RECORD 1 ]-----+--
+strong_implied_by | t
+weak_implied_by   | t
+strong_refuted_by | f
+weak_refuted_by   | f
+s_i_holds         | t
+w_i_holds         | t
+s_r_holds         | f
+w_r_holds         | f
+
+select * from test_predtest($$
+select z or x, (x and y) or (z and w)
+from booleans
+$$);
+-[ RECORD 1 ]-----+--
+strong_implied_by | t
+weak_implied_by   | t
+strong_refuted_by | f
+weak_refuted_by   | f
+s_i_holds         | t
+w_i_holds         | t
+s_r_holds         | f
+w_r_holds         | f
+
+select * from test_predtest($$
+select x or z, (x and y) or (z and w) or w
+from booleans
+$$);
+-[ RECORD 1 ]-----+--
+strong_implied_by | f
+weak_implied_by   | f
+strong_refuted_by | f
+weak_refuted_by   | f
+s_i_holds         | f
+w_i_holds         | f
+s_r_holds         | f
+w_r_holds         | f
+
+select * from test_predtest($$
+select w or x or z, (x and y) or z
+from booleans
+$$);
+-[ RECORD 1 ]-----+--
+strong_implied_by | t
+weak_implied_by   | t
+strong_refuted_by | f
+weak_refuted_by   | f
+s_i_holds         | t
+w_i_holds         | t
+s_r_holds         | f
+w_r_holds         | f
+
 select * from test_predtest($$
 select x and z, x and y and z
 from booleans
diff --git a/src/test/modules/test_predtest/sql/test_predtest.sql b/src/test/modules/test_predtest/sql/test_predtest.sql
index dc59f0c22f0..2432bf2c1c9 100644
--- a/src/test/modules/test_predtest/sql/test_predtest.sql
+++ b/src/test/modules/test_predtest/sql/test_predtest.sql
@@ -180,6 +180,28 @@ select x or y or z, x or z
 from booleans
 $$);
 
+-- OR-clause => OR-clause tries the item at the same position first; check
+-- arms that correspond, arms that don't, and lists of different lengths
+select * from test_predtest($$
+select x or z, (x and y) or (z and w)
+from booleans
+$$);
+
+select * from test_predtest($$
+select z or x, (x and y) or (z and w)
+from booleans
+$$);
+
+select * from test_predtest($$
+select x or z, (x and y) or (z and w) or w
+from booleans
+$$);
+
+select * from test_predtest($$
+select w or x or z, (x and y) or z
+from booleans
+$$);
+
 select * from test_predtest($$
 select x and z, x and y and z
 from booleans
-- 
2.55.0

