0001-Order-qual-clauses-by-combined-cost-and-selectivity.patch

application/octet-stream

Filename: 0001-Order-qual-clauses-by-combined-cost-and-selectivity.patch
Type: application/octet-stream
Part: 0
Message: [RFC][PATCH] Order qual clauses by combined cost and selectivity

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch 0001
Subject: Order qual clauses by combined cost and selectivity
File+
contrib/pg_plan_advice/expected/join_order.out 1 1
contrib/pg_plan_advice/expected/scan.out 1 1
contrib/postgres_fdw/expected/postgres_fdw.out 3 3
src/backend/optimizer/plan/createplan.c 65 15
src/test/regress/expected/inherit.out 8 8
src/test/regress/expected/join.out 2 2
src/test/regress/expected/partition_join.out 24 24
src/test/regress/expected/partition_prune.out 33 33
src/test/regress/expected/predicate_order.out 222 0
src/test/regress/expected/rowsecurity.out 4 4
src/test/regress/expected/select_distinct.out 1 1
src/test/regress/expected/select.out 1 1
src/test/regress/expected/updatable_views.out 4 4
src/test/regress/parallel_schedule 1 1
src/test/regress/sql/predicate_order.sql 193 0
From 542ec565d8f0449d5f853af58f34886166ebfb03 Mon Sep 17 00:00:00 2001
From: Staroverov Ilya <i.staroverov@ftdata.ru>
Date: Wed, 22 Apr 2026 08:13:48 +0300
Subject: [PATCH] Order qual clauses by combined cost and selectivity

order_qual_clauses() currently ranks predicates by estimated per-tuple
cost alone.  For conjunctive (AND) predicates evaluated with
short-circuit logic, this can be suboptimal: a slightly more expensive
clause that rejects most rows should be evaluated before a cheap clause
that passes most of them.

Rank clauses by  cost / (1 - selectivity)  instead, where selectivity is
the fraction of rows that satisfy the clause.  This is a natural
heuristic that minimizes expected evaluation work when predicates are
independent and evaluated left-to-right.

Security-level constraints are still honored: clauses with a lower
security_level are never moved after clauses with a higher one, except
when the latter are leakproof.  When computed ranks are equal, the
original clause order is preserved.

Add a dedicated regression test that exercises the new ordering:
equal-cost reordering by selectivity, cost-versus-selectivity trade-off,
preservation of security_level ordering, and stability for equal ranks.
Update existing expected outputs whose EXPLAIN Filter lines changed
due to the new ordering.
---
 .../pg_plan_advice/expected/join_order.out    |   2 +-
 contrib/pg_plan_advice/expected/scan.out      |   2 +-
 .../postgres_fdw/expected/postgres_fdw.out    |   6 +-
 src/backend/optimizer/plan/createplan.c       |  80 +++++--
 src/test/regress/expected/inherit.out         |  16 +-
 src/test/regress/expected/join.out            |   4 +-
 src/test/regress/expected/partition_join.out  |  48 ++--
 src/test/regress/expected/partition_prune.out |  66 +++---
 src/test/regress/expected/predicate_order.out | 222 ++++++++++++++++++
 src/test/regress/expected/rowsecurity.out     |   8 +-
 src/test/regress/expected/select.out          |   2 +-
 src/test/regress/expected/select_distinct.out |   2 +-
 src/test/regress/expected/updatable_views.out |   8 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/predicate_order.sql      | 193 +++++++++++++++
 15 files changed, 563 insertions(+), 98 deletions(-)
 create mode 100644 src/test/regress/expected/predicate_order.out
 create mode 100644 src/test/regress/sql/predicate_order.sql

diff --git a/contrib/pg_plan_advice/expected/join_order.out b/contrib/pg_plan_advice/expected/join_order.out
index a5a9728e3fd..10af3ccf690 100644
--- a/contrib/pg_plan_advice/expected/join_order.out
+++ b/contrib/pg_plan_advice/expected/join_order.out
@@ -292,7 +292,7 @@ SELECT * FROM jo_fact f
 --------------------------------------------------------------
  Nested Loop
    Disabled: true
-   Join Filter: ((d1.id = f.dim1_id) AND (d2.id = f.dim2_id))
+   Join Filter: ((d2.id = f.dim2_id) AND (d1.id = f.dim1_id))
    ->  Nested Loop
          ->  Seq Scan on jo_dim1 d1
                Filter: (val1 = 1)
diff --git a/contrib/pg_plan_advice/expected/scan.out b/contrib/pg_plan_advice/expected/scan.out
index f4036e4cbdd..62639a4f993 100644
--- a/contrib/pg_plan_advice/expected/scan.out
+++ b/contrib/pg_plan_advice/expected/scan.out
@@ -426,7 +426,7 @@ EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table
                          QUERY PLAN                          
 -------------------------------------------------------------
  Seq Scan on scan_table
-   Filter: ((ctid > '(1,1)'::tid) AND (ctid < '(2,1)'::tid))
+   Filter: ((ctid < '(2,1)'::tid) AND (ctid > '(1,1)'::tid))
  Supplied Plan Advice:
    SEQ_SCAN(scan_table) /* matched */
  Generated Plan Advice:
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 10e87acabef..f96dba986b5 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -380,7 +380,7 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 =
 ----------------------------------------------------------------------------------------------------------------------------------
  Foreign Scan on public.ft1 t1
    Output: c1, c2, c3, c4, c5, c6, c7, c8
-   Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((c7 >= '1')) AND (("C 1" = 101)) AND ((c6 = '1'))
+   Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = 101)) AND ((c6 = '1')) AND ((c7 >= '1'))
 (3 rows)
 
 SELECT * FROM ft1 t1 WHERE t1.c1 = 101 AND t1.c6 = '1' AND t1.c7 >= '1';
@@ -2489,7 +2489,7 @@ SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
    Join Filter: (ft4.c1 = ft1.c1)
    ->  Foreign Scan on public.ft4
          Output: ft4.c1, ft4.c2, ft4.c3
-         Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 10)) AND ((c1 <= 15))
+         Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 <= 15)) AND ((c1 >= 10))
    ->  Materialize
          Output: ft1.c1, ft2.c1, (13)
          ->  Foreign Scan
@@ -3622,7 +3622,7 @@ select array_agg(c1 order by c1 using operator(public.<^)) from ft2 where c2 = 6
          Sort Key: ft2.c1 USING <^
          ->  Foreign Scan on public.ft2
                Output: c1, c2
-               Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) AND ((c2 = 6))
+               Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE ((c2 = 6)) AND (("C 1" < 100))
 (8 rows)
 
 -- This should not be pushed either.
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index de6a183da79..5484fcfaa49 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -16,6 +16,8 @@
  */
 #include "postgres.h"
 
+#include <float.h>
+
 #include "access/sysattr.h"
 #include "access/transam.h"
 #include "catalog/pg_class.h"
@@ -5239,15 +5241,14 @@ get_switched_clauses(List *clauses, Relids outerrelids)
  * different security levels in the list.  Quals of lower security_level
  * must go before quals of higher security_level, except that we can grant
  * exceptions to move up quals that are leakproof.  When security level
- * doesn't force the decision, we prefer to order clauses by estimated
- * execution cost, cheapest first.
+ * doesn't force the decision, we order clauses by a combined
+ * cost/selectivity rank so that cheap predicates that eliminate many rows
+ * are evaluated first.
  *
- * Ideally the order should be driven by a combination of execution cost and
- * selectivity, but it's not immediately clear how to account for both,
- * and given the uncertainty of the estimates the reliability of the decisions
- * would be doubtful anyway.  So we just order by security level then
- * estimated per-tuple cost, being careful not to change the order when
- * (as is often the case) the estimates are identical.
+ * For conjunctive predicates, we use the rank cost / (1 - selectivity),
+ * where selectivity is the fraction of rows that pass the clause.
+ * Clauses with a lower rank are checked first.  We are careful not to
+ * change the order when the computed ranks are identical.
  *
  * Although this will work on either bare clauses or RestrictInfos, it's
  * much faster to apply it to RestrictInfos, since it can re-use cost
@@ -5259,8 +5260,10 @@ get_switched_clauses(List *clauses, Relids outerrelids)
  *
  * Note: some callers pass lists that contain entries that will later be
  * removed; this is the easiest way to let this routine see RestrictInfos
- * instead of bare clauses.  This is another reason why trying to consider
- * selectivity in the ordering would likely do the wrong thing.
+ * instead of bare clauses.  In such cases, selectivity estimates for
+ * clauses that are later removed can still affect the temporary ordering,
+ * but that is acceptable because this routine only determines executor
+ * qual evaluation order after planning decisions have already been made.
  */
 static List *
 order_qual_clauses(PlannerInfo *root, List *clauses)
@@ -5269,6 +5272,8 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
 	{
 		Node	   *clause;
 		Cost		cost;
+		Selectivity	selectivity;
+		double		rank;
 		Index		security_level;
 	} QualItem;
 	int			nitems = list_length(clauses);
@@ -5282,8 +5287,9 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
 		return clauses;
 
 	/*
-	 * Collect the items and costs into an array.  This is to avoid repeated
-	 * cost_qual_eval work if the inputs aren't RestrictInfos.
+	 * Collect the items, costs, and selectivities into an array.  This is
+	 * to avoid repeated cost_qual_eval work if the inputs aren't
+	 * RestrictInfos.
 	 */
 	items = (QualItem *) palloc(nitems * sizeof(QualItem));
 	i = 0;
@@ -5295,6 +5301,38 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
 		cost_qual_eval_node(&qcost, clause, root);
 		items[i].clause = clause;
 		items[i].cost = qcost.per_tuple;
+
+		/*
+		 * Compute selectivity for this clause.  Selectivity is the
+		 * fraction of rows that pass the predicate (lower = more
+		 * selective = eliminates more rows).
+		 */
+		items[i].selectivity = clause_selectivity(root,
+												  clause,
+												  0,
+												  JOIN_INNER,
+												  NULL);
+
+		if (items[i].selectivity < 0.0)
+			items[i].selectivity = 0.0;
+		else if (items[i].selectivity > 1.0)
+			items[i].selectivity = 1.0;
+
+		/*
+		 * For conjunctive predicates, rank is cost / (1 - selectivity),
+		 * where selectivity is the fraction of rows that pass.  Lower
+		 * rank means that the clause is a better candidate to
+		 * evaluate earlier.
+		 *
+		 * If selectivity is 1.0, the clause eliminates no rows, so
+		 * assign the maximum rank and move it to the end within the
+		 * same security level.
+		 */
+		if (items[i].selectivity < 1.0)
+			items[i].rank = items[i].cost / (1.0 - items[i].selectivity);
+		else
+			items[i].rank = DBL_MAX;
+
 		if (IsA(clause, RestrictInfo))
 		{
 			RestrictInfo *rinfo = (RestrictInfo *) clause;
@@ -5334,10 +5372,22 @@ order_qual_clauses(PlannerInfo *root, List *clauses)
 		{
 			QualItem   *olditem = &items[j - 1];
 
-			if (newitem.security_level > olditem->security_level ||
-				(newitem.security_level == olditem->security_level &&
-				 newitem.cost >= olditem->cost))
+			/*
+			 * First, respect security levels: higher security_level
+			 * must come after lower.
+			 */
+			if (newitem.security_level > olditem->security_level)
 				break;
+			if (newitem.security_level < olditem->security_level)
+			{
+				items[j] = *olditem;
+				continue;
+			}
+
+			/* Same security level: order by cost-effectiveness rank. */
+			if (newitem.rank >= olditem->rank)
+				break;
+
 			items[j] = *olditem;
 		}
 		items[j] = newitem;
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 3d8e8d8afd2..51641524c07 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -3197,11 +3197,11 @@ explain (costs off) select * from range_list_parted where a between 3 and 23 and
 -----------------------------------------------------------------
  Append
    ->  Seq Scan on part_1_10_ab range_list_parted_1
-         Filter: ((a >= 3) AND (a <= 23) AND (b = 'ab'::bpchar))
+         Filter: ((b = 'ab'::bpchar) AND (a >= 3) AND (a <= 23))
    ->  Seq Scan on part_10_20_ab range_list_parted_2
-         Filter: ((a >= 3) AND (a <= 23) AND (b = 'ab'::bpchar))
+         Filter: ((b = 'ab'::bpchar) AND (a >= 3) AND (a <= 23))
    ->  Seq Scan on part_21_30_ab range_list_parted_3
-         Filter: ((a >= 3) AND (a <= 23) AND (b = 'ab'::bpchar))
+         Filter: ((b = 'ab'::bpchar) AND (a >= 3) AND (a <= 23))
 (7 rows)
 
 /* Should select no rows because range partition key cannot be null */
@@ -3345,7 +3345,7 @@ explain (costs off) select * from mcrparted where a = 20 and abs(b) = 10 and c >
                      QUERY PLAN                      
 -----------------------------------------------------
  Seq Scan on mcrparted4 mcrparted
-   Filter: ((c > 10) AND (a = 20) AND (abs(b) = 10))
+   Filter: ((a = 20) AND (c > 10) AND (abs(b) = 10))
 (2 rows)
 
 explain (costs off) select * from mcrparted where a = 20 and c > 20; -- scans mcrparted3, mcrparte4, mcrparte5, mcrparted_def
@@ -3353,13 +3353,13 @@ explain (costs off) select * from mcrparted where a = 20 and c > 20; -- scans mc
 ---------------------------------------------
  Append
    ->  Seq Scan on mcrparted3 mcrparted_1
-         Filter: ((c > 20) AND (a = 20))
+         Filter: ((a = 20) AND (c > 20))
    ->  Seq Scan on mcrparted4 mcrparted_2
-         Filter: ((c > 20) AND (a = 20))
+         Filter: ((a = 20) AND (c > 20))
    ->  Seq Scan on mcrparted5 mcrparted_3
-         Filter: ((c > 20) AND (a = 20))
+         Filter: ((a = 20) AND (c > 20))
    ->  Seq Scan on mcrparted_def mcrparted_4
-         Filter: ((c > 20) AND (a = 20))
+         Filter: ((a = 20) AND (c > 20))
 (9 rows)
 
 -- check that partitioned table Appends cope with being referenced in
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 78bf022f7b4..0db378be547 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -2809,7 +2809,7 @@ select count(*) from
  Aggregate
    ->  Merge Left Join
          Merge Cond: (x.thousand = y.unique2)
-         Join Filter: ((x.twothousand = y.hundred) AND (x.fivethous = y.unique2))
+         Join Filter: ((x.fivethous = y.unique2) AND (x.twothousand = y.hundred))
          ->  Sort
                Sort Key: x.thousand, x.twothousand, x.fivethous
                ->  Seq Scan on tenk1 x
@@ -9787,7 +9787,7 @@ inner join j2 on j1.id1 = j2.id1 and j1.id2 = j2.id2;
  Nested Loop
    Output: j1.id1, j1.id2, j2.id1, j2.id2
    Inner Unique: true
-   Join Filter: ((j1.id1 = j2.id1) AND (j1.id2 = j2.id2))
+   Join Filter: ((j1.id2 = j2.id2) AND (j1.id1 = j2.id1))
    ->  Seq Scan on public.j2
          Output: j2.id1, j2.id2
    ->  Seq Scan on public.j1
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 38643d41fd7..f8072f99e5d 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -285,7 +285,7 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a <
                Filter: (b > 250)
          ->  Hash
                ->  Seq Scan on prt1_p2 t1
-                     Filter: ((a < 450) AND (b = 0))
+                     Filter: ((b = 0) AND (a < 450))
 (9 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a < 450 AND t2.b > 250 AND t1.b = 0 ORDER BY t1.a, t2.b;
@@ -311,9 +311,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JO
          ->  Hash
                ->  Append
                      ->  Seq Scan on prt1_p1 prt1_1
-                           Filter: ((a < 450) AND (b = 0))
+                           Filter: ((b = 0) AND (a < 450))
                      ->  Seq Scan on prt1_p2 prt1_2
-                           Filter: ((a < 450) AND (b = 0))
+                           Filter: ((b = 0) AND (a < 450))
 (15 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JOIN (SELECT * FROM prt2 WHERE b > 250) t2 ON t1.a = t2.b WHERE t1.b = 0 ORDER BY t1.a, t2.b;
@@ -1400,9 +1400,9 @@ SELECT t1.a, t2.b FROM (SELECT * FROM prt1 WHERE a < 450) t1 LEFT JOIN (SELECT *
                Sort Key: prt1.a
                ->  Append
                      ->  Seq Scan on prt1_p1 prt1_1
-                           Filter: ((a < 450) AND (b = 0))
+                           Filter: ((b = 0) AND (a < 450))
                      ->  Seq Scan on prt1_p2 prt1_2
-                           Filter: ((a < 450) AND (b = 0))
+                           Filter: ((b = 0) AND (a < 450))
          ->  Sort
                Sort Key: prt2.b
                ->  Append
@@ -2536,7 +2536,7 @@ where not exists (select 1 from prtx2
  Append
    ->  Nested Loop Anti Join
          ->  Seq Scan on prtx1_1
-               Filter: ((a < 20) AND (c = 120))
+               Filter: ((c = 120) AND (a < 20))
          ->  Bitmap Heap Scan on prtx2_1
                Recheck Cond: ((b = prtx1_1.b) AND (c = 123))
                Filter: (a = prtx1_1.a)
@@ -2547,7 +2547,7 @@ where not exists (select 1 from prtx2
                            Index Cond: (c = 123)
    ->  Nested Loop Anti Join
          ->  Seq Scan on prtx1_2
-               Filter: ((a < 20) AND (c = 120))
+               Filter: ((c = 120) AND (a < 20))
          ->  Bitmap Heap Scan on prtx2_2
                Recheck Cond: ((b = prtx1_2.b) AND (c = 123))
                Filter: (a = prtx1_2.a)
@@ -2577,7 +2577,7 @@ where not exists (select 1 from prtx2
  Append
    ->  Nested Loop Anti Join
          ->  Seq Scan on prtx1_1
-               Filter: ((a < 20) AND (c = 91))
+               Filter: ((c = 91) AND (a < 20))
          ->  Bitmap Heap Scan on prtx2_1
                Recheck Cond: ((b = (prtx1_1.b + 1)) OR (c = 99))
                Filter: (a = prtx1_1.a)
@@ -2588,7 +2588,7 @@ where not exists (select 1 from prtx2
                            Index Cond: (c = 99)
    ->  Nested Loop Anti Join
          ->  Seq Scan on prtx1_2
-               Filter: ((a < 20) AND (c = 91))
+               Filter: ((c = 91) AND (a < 20))
          ->  Bitmap Heap Scan on prtx2_2
                Recheck Cond: ((b = (prtx1_2.b + 1)) OR (c = 99))
                Filter: (a = prtx1_2.a)
@@ -3451,13 +3451,13 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_adv t1 INNER JOIN prt2_adv t2 ON (t1.a =
                ->  Seq Scan on prt2_adv_p1 t2_1
                ->  Hash
                      ->  Seq Scan on prt1_adv_p1 t1_1
-                           Filter: ((a < 300) AND (b = 0))
+                           Filter: ((b = 0) AND (a < 300))
          ->  Hash Join
                Hash Cond: (t2_2.b = t1_2.a)
                ->  Seq Scan on prt2_adv_p2 t2_2
                ->  Hash
                      ->  Seq Scan on prt1_adv_p2 t1_2
-                           Filter: ((a < 300) AND (b = 0))
+                           Filter: ((b = 0) AND (a < 300))
 (15 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_adv t1 INNER JOIN prt2_adv t2 ON (t1.a = t2.b) WHERE t1.a < 300 AND t1.b = 0 ORDER BY t1.a, t2.b;
@@ -3490,13 +3490,13 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_adv t1 INNER JOIN prt2_adv t2 ON (t1.a =
                ->  Seq Scan on prt2_adv_p1 t2_1
                ->  Hash
                      ->  Seq Scan on prt1_adv_p1 t1_1
-                           Filter: ((a >= 100) AND (a < 300) AND (b = 0))
+                           Filter: ((b = 0) AND (a >= 100) AND (a < 300))
          ->  Hash Join
                Hash Cond: (t2_2.b = t1_2.a)
                ->  Seq Scan on prt2_adv_p2 t2_2
                ->  Hash
                      ->  Seq Scan on prt1_adv_p2 t1_2
-                           Filter: ((a >= 100) AND (a < 300) AND (b = 0))
+                           Filter: ((b = 0) AND (a >= 100) AND (a < 300))
 (15 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_adv t1 INNER JOIN prt2_adv t2 ON (t1.a = t2.b) WHERE t1.a >= 100 AND t1.a < 300 AND t1.b = 0 ORDER BY t1.a, t2.b;
@@ -4447,7 +4447,7 @@ SELECT t1.a, t1.c, t2.a, t2.c FROM plt1_adv t1 LEFT JOIN plt2_adv t2 ON (t1.a =
                      ->  Seq Scan on plt1_adv_p3 t1_3
                            Filter: (b < 10)
          ->  Nested Loop Left Join
-               Join Filter: ((t1_4.a = t2_4.a) AND (t1_4.c = t2_4.c))
+               Join Filter: ((t1_4.c = t2_4.c) AND (t1_4.a = t2_4.a))
                ->  Seq Scan on plt1_adv_extra t1_4
                      Filter: (b < 10)
                ->  Seq Scan on plt2_adv_extra t2_4
@@ -4553,9 +4553,9 @@ SELECT t1.a, t1.c, t2.a, t2.c, t3.a, t3.c FROM plt1_adv t1 LEFT JOIN plt2_adv t2
                                  ->  Seq Scan on plt1_adv_p3 t1_3
                                        Filter: (b < 10)
          ->  Nested Loop Left Join
-               Join Filter: ((t1_4.a = t3_4.a) AND (t1_4.c = t3_4.c))
+               Join Filter: ((t1_4.c = t3_4.c) AND (t1_4.a = t3_4.a))
                ->  Nested Loop Left Join
-                     Join Filter: ((t1_4.a = t2_4.a) AND (t1_4.c = t2_4.c))
+                     Join Filter: ((t1_4.c = t2_4.c) AND (t1_4.a = t2_4.a))
                      ->  Seq Scan on plt1_adv_extra t1_4
                            Filter: (b < 10)
                      ->  Seq Scan on plt2_adv_extra t2_4
@@ -5022,7 +5022,7 @@ SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = t2
                ->  Seq Scan on beta_neg_p2 t2_2
                ->  Hash
                      ->  Seq Scan on alpha_neg_p2 t1_2
-                           Filter: ((b >= 125) AND (b < 225))
+                           Filter: ((b < 225) AND (b >= 125))
          ->  Hash Join
                Hash Cond: ((t2_4.a = t1_4.a) AND (t2_4.b = t1_4.b))
                ->  Append
@@ -5032,11 +5032,11 @@ SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = t2
                ->  Hash
                      ->  Append
                            ->  Seq Scan on alpha_pos_p1 t1_4
-                                 Filter: ((b >= 125) AND (b < 225))
+                                 Filter: ((b < 225) AND (b >= 125))
                            ->  Seq Scan on alpha_pos_p2 t1_5
-                                 Filter: ((b >= 125) AND (b < 225))
+                                 Filter: ((b < 225) AND (b >= 125))
                            ->  Seq Scan on alpha_pos_p3 t1_6
-                                 Filter: ((b >= 125) AND (b < 225))
+                                 Filter: ((b < 225) AND (b >= 125))
 (29 rows)
 
 SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = t2.b) WHERE t1.b >= 125 AND t1.b < 225 ORDER BY t1.a, t1.b;
@@ -5105,13 +5105,13 @@ SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.c = t2
                            ->  Seq Scan on beta_neg_p2 t2_3
                                  Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210)))
          ->  Nested Loop
-               Join Filter: ((t1_4.a = t2_4.a) AND (t1_4.c = t2_4.c))
+               Join Filter: ((t1_4.c = t2_4.c) AND (t1_4.a = t2_4.a))
                ->  Seq Scan on alpha_pos_p2 t1_4
                      Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
                ->  Seq Scan on beta_pos_p2 t2_4
                      Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210)))
          ->  Nested Loop
-               Join Filter: ((t1_5.a = t2_5.a) AND (t1_5.c = t2_5.c))
+               Join Filter: ((t1_5.c = t2_5.c) AND (t1_5.a = t2_5.a))
                ->  Seq Scan on alpha_pos_p3 t1_5
                      Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
                ->  Seq Scan on beta_pos_p3 t2_5
@@ -5161,13 +5161,13 @@ SELECT t1.*, t2.* FROM alpha t1 INNER JOIN beta t2 ON (t1.a = t2.a AND t1.b = t2
                      ->  Seq Scan on beta_neg_p2 t2_2
                            Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210)))
          ->  Nested Loop
-               Join Filter: ((t1_3.a = t2_3.a) AND (t1_3.b = t2_3.b) AND (t1_3.c = t2_3.c))
+               Join Filter: ((t1_3.b = t2_3.b) AND (t1_3.c = t2_3.c) AND (t1_3.a = t2_3.a))
                ->  Seq Scan on alpha_pos_p2 t1_3
                      Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
                ->  Seq Scan on beta_pos_p2 t2_3
                      Filter: (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210)))
          ->  Nested Loop
-               Join Filter: ((t1_4.a = t2_4.a) AND (t1_4.b = t2_4.b) AND (t1_4.c = t2_4.c))
+               Join Filter: ((t1_4.b = t2_4.b) AND (t1_4.c = t2_4.c) AND (t1_4.a = t2_4.a))
                ->  Seq Scan on alpha_pos_p3 t1_4
                      Filter: ((c = ANY ('{0004,0009}'::text[])) AND (((b >= 100) AND (b < 110)) OR ((b >= 200) AND (b < 210))))
                ->  Seq Scan on beta_pos_p3 t2_4
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 849049f9c51..a85f69f601d 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -364,21 +364,21 @@ explain (costs off) select * from rlp where a > 15 and b = 'ab';
 ---------------------------------------------------------
  Append
    ->  Seq Scan on rlp3abcd rlp_1
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: (((b)::text = 'ab'::text) AND (a > 15))
    ->  Seq Scan on rlp4_1 rlp_2
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: (((b)::text = 'ab'::text) AND (a > 15))
    ->  Seq Scan on rlp4_2 rlp_3
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: (((b)::text = 'ab'::text) AND (a > 15))
    ->  Seq Scan on rlp4_default rlp_4
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: (((b)::text = 'ab'::text) AND (a > 15))
    ->  Seq Scan on rlp5_1 rlp_5
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: (((b)::text = 'ab'::text) AND (a > 15))
    ->  Seq Scan on rlp5_default rlp_6
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: (((b)::text = 'ab'::text) AND (a > 15))
    ->  Seq Scan on rlp_default_30 rlp_7
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: (((b)::text = 'ab'::text) AND (a > 15))
    ->  Seq Scan on rlp_default_default rlp_8
-         Filter: ((a > 15) AND ((b)::text = 'ab'::text))
+         Filter: (((b)::text = 'ab'::text) AND (a > 15))
 (17 rows)
 
 explain (costs off) select * from rlp where a = 16;
@@ -406,7 +406,7 @@ explain (costs off) select * from rlp where a = 16 and b < 'ab';
                     QUERY PLAN                     
 ---------------------------------------------------
  Seq Scan on rlp3_default rlp
-   Filter: (((b)::text < 'ab'::text) AND (a = 16))
+   Filter: ((a = 16) AND ((b)::text < 'ab'::text))
 (2 rows)
 
 explain (costs off) select * from rlp where a = 16 and b <= 'ab';
@@ -414,9 +414,9 @@ explain (costs off) select * from rlp where a = 16 and b <= 'ab';
 ----------------------------------------------------------
  Append
    ->  Seq Scan on rlp3abcd rlp_1
-         Filter: (((b)::text <= 'ab'::text) AND (a = 16))
+         Filter: ((a = 16) AND ((b)::text <= 'ab'::text))
    ->  Seq Scan on rlp3_default rlp_2
-         Filter: (((b)::text <= 'ab'::text) AND (a = 16))
+         Filter: ((a = 16) AND ((b)::text <= 'ab'::text))
 (5 rows)
 
 explain (costs off) select * from rlp where a = 16 and b is null;
@@ -636,7 +636,7 @@ explain (costs off) select * from rlp where a > 1 and a = 10;	/* only default */
             QUERY PLAN            
 ----------------------------------
  Seq Scan on rlp_default_10 rlp
-   Filter: ((a > 1) AND (a = 10))
+   Filter: ((a = 10) AND (a > 1))
 (2 rows)
 
 explain (costs off) select * from rlp where a > 1 and a >=15;	/* rlp3 onwards, including default */
@@ -741,9 +741,9 @@ explain (costs off) select * from mc3p where a = 1 and abs(b) = 1 and c < 8;
 --------------------------------------------------------
  Append
    ->  Seq Scan on mc3p0 mc3p_1
-         Filter: ((c < 8) AND (a = 1) AND (abs(b) = 1))
+         Filter: ((a = 1) AND (c < 8) AND (abs(b) = 1))
    ->  Seq Scan on mc3p1 mc3p_2
-         Filter: ((c < 8) AND (a = 1) AND (abs(b) = 1))
+         Filter: ((a = 1) AND (c < 8) AND (abs(b) = 1))
 (5 rows)
 
 explain (costs off) select * from mc3p where a = 10 and abs(b) between 5 and 35;
@@ -991,7 +991,7 @@ explain (costs off) select * from mc2p where a = 2 and b < 1;
            QUERY PLAN            
 ---------------------------------
  Seq Scan on mc2p3 mc2p
-   Filter: ((b < 1) AND (a = 2))
+   Filter: ((a = 2) AND (b < 1))
 (2 rows)
 
 explain (costs off) select * from mc2p where a > 1;
@@ -1014,7 +1014,7 @@ explain (costs off) select * from mc2p where a = 1 and b > 1;
            QUERY PLAN            
 ---------------------------------
  Seq Scan on mc2p2 mc2p
-   Filter: ((b > 1) AND (a = 1))
+   Filter: ((a = 1) AND (b > 1))
 (2 rows)
 
 -- all partitions but the default one should be pruned
@@ -1809,9 +1809,9 @@ explain (costs off) select * from rlp where a = 15 and b <> 'ab' and b <> 'cd' a
 ------------------------------------------------------------------------------------------------------------------------------------------
  Append
    ->  Seq Scan on rlp3efgh rlp_1
-         Filter: ((b IS NOT NULL) AND ((b)::text <> 'ab'::text) AND ((b)::text <> 'cd'::text) AND ((b)::text <> 'xy'::text) AND (a = 15))
+         Filter: ((b IS NOT NULL) AND (a = 15) AND ((b)::text <> 'ab'::text) AND ((b)::text <> 'cd'::text) AND ((b)::text <> 'xy'::text))
    ->  Seq Scan on rlp3_default rlp_2
-         Filter: ((b IS NOT NULL) AND ((b)::text <> 'ab'::text) AND ((b)::text <> 'cd'::text) AND ((b)::text <> 'xy'::text) AND (a = 15))
+         Filter: ((b IS NOT NULL) AND (a = 15) AND ((b)::text <> 'ab'::text) AND ((b)::text <> 'cd'::text) AND ((b)::text <> 'xy'::text))
 (5 rows)
 
 --
@@ -2026,13 +2026,13 @@ explain (costs off) select * from hp where a < 1 and b = 'xxx';
 -------------------------------------------------
  Append
    ->  Seq Scan on hp0 hp_1
-         Filter: ((a < 1) AND (b = 'xxx'::text))
+         Filter: ((b = 'xxx'::text) AND (a < 1))
    ->  Seq Scan on hp1 hp_2
-         Filter: ((a < 1) AND (b = 'xxx'::text))
+         Filter: ((b = 'xxx'::text) AND (a < 1))
    ->  Seq Scan on hp2 hp_3
-         Filter: ((a < 1) AND (b = 'xxx'::text))
+         Filter: ((b = 'xxx'::text) AND (a < 1))
    ->  Seq Scan on hp3 hp_4
-         Filter: ((a < 1) AND (b = 'xxx'::text))
+         Filter: ((b = 'xxx'::text) AND (a < 1))
 (9 rows)
 
 explain (costs off) select * from hp where a <> 1 and b = 'yyy';
@@ -2040,13 +2040,13 @@ explain (costs off) select * from hp where a <> 1 and b = 'yyy';
 --------------------------------------------------
  Append
    ->  Seq Scan on hp0 hp_1
-         Filter: ((a <> 1) AND (b = 'yyy'::text))
+         Filter: ((b = 'yyy'::text) AND (a <> 1))
    ->  Seq Scan on hp1 hp_2
-         Filter: ((a <> 1) AND (b = 'yyy'::text))
+         Filter: ((b = 'yyy'::text) AND (a <> 1))
    ->  Seq Scan on hp2 hp_3
-         Filter: ((a <> 1) AND (b = 'yyy'::text))
+         Filter: ((b = 'yyy'::text) AND (a <> 1))
    ->  Seq Scan on hp3 hp_4
-         Filter: ((a <> 1) AND (b = 'yyy'::text))
+         Filter: ((b = 'yyy'::text) AND (a <> 1))
 (9 rows)
 
 explain (costs off) select * from hp where a <> 1 and b <> 'xxx';
@@ -4113,7 +4113,7 @@ select * from listp where a = (select 2) and b <> 10;
                        QUERY PLAN                       
 --------------------------------------------------------
  Seq Scan on listp1 listp (actual rows=0.00 loops=1)
-   Filter: ((b <> 10) AND (a = (InitPlan expr_1).col1))
+   Filter: ((a = (InitPlan expr_1).col1) AND (b <> 10))
    InitPlan expr_1
      ->  Result (never executed)
 (4 rows)
@@ -4275,7 +4275,7 @@ explain (costs off) select * from rp_prefix_test1 where a <= 1 and b = 'a';
                     QUERY PLAN                    
 --------------------------------------------------
  Seq Scan on rp_prefix_test1_p1 rp_prefix_test1
-   Filter: ((a <= 1) AND ((b)::text = 'a'::text))
+   Filter: (((b)::text = 'a'::text) AND (a <= 1))
 (2 rows)
 
 create table rp_prefix_test2 (a int, b int, c int) partition by range(a, b, c);
@@ -4287,7 +4287,7 @@ explain (costs off) select * from rp_prefix_test2 where a <= 1 and b = 1 and c >
                    QUERY PLAN                   
 ------------------------------------------------
  Seq Scan on rp_prefix_test2_p1 rp_prefix_test2
-   Filter: ((a <= 1) AND (c >= 0) AND (b = 1))
+   Filter: ((b = 1) AND (a <= 1) AND (c >= 0))
 (2 rows)
 
 create table rp_prefix_test3 (a int, b int, c int, d int) partition by range(a, b, c, d);
@@ -4309,7 +4309,7 @@ explain (costs off) select * from rp_prefix_test3 where a >= 1 and b >= 1 and b
                                QUERY PLAN                               
 ------------------------------------------------------------------------
  Seq Scan on rp_prefix_test3_p2 rp_prefix_test3
-   Filter: ((a >= 1) AND (b >= 1) AND (d >= 0) AND (b = 2) AND (c = 2))
+   Filter: ((b = 2) AND (c = 2) AND (a >= 1) AND (b >= 1) AND (d >= 0))
 (2 rows)
 
 drop table rp_prefix_test1;
@@ -4586,7 +4586,7 @@ explain (verbose, costs off) execute update_part_abc_view (1, 'd');
          Subplans Removed: 1
          ->  Seq Scan on public.part_abc_1
                Output: $2, part_abc_1.tableoid, part_abc_1.ctid
-               Filter: ((part_abc_1.b <> 'a'::text) AND (part_abc_1.a = $1))
+               Filter: ((part_abc_1.a = $1) AND (part_abc_1.b <> 'a'::text))
 (8 rows)
 
 execute update_part_abc_view (1, 'd');
@@ -4605,7 +4605,7 @@ explain (verbose, costs off) execute update_part_abc_view (2, 'a');
          Subplans Removed: 1
          ->  Seq Scan on public.part_abc_2
                Output: $2, part_abc_2.tableoid, part_abc_2.ctid
-               Filter: ((part_abc_2.b <> 'a'::text) AND (part_abc_2.a = $1))
+               Filter: ((part_abc_2.a = $1) AND (part_abc_2.b <> 'a'::text))
 (8 rows)
 
 execute update_part_abc_view (2, 'a');
@@ -4641,7 +4641,7 @@ when matched then delete returning pt.a;
          ->  Append
                Subplans Removed: 1
                ->  Seq Scan on part_abc_1
-                     Filter: ((b <> 'a'::text) AND (a = stable_one()))
+                     Filter: ((a = stable_one()) AND (b <> 'a'::text))
          ->  Materialize
                ->  Seq Scan on part_abc_1 pt1
                      Filter: (a = stable_one())
diff --git a/src/test/regress/expected/predicate_order.out b/src/test/regress/expected/predicate_order.out
new file mode 100644
index 00000000000..3b97f005137
--- /dev/null
+++ b/src/test/regress/expected/predicate_order.out
@@ -0,0 +1,222 @@
+--
+-- Predicate ordering by selectivity
+--
+-- Test 1. Check reordering of equal-cost clauses by selectivity.
+--         Both clauses use the built-in "=" operator and have the
+--         same per-tuple cost.  The clause on column "a" is more
+--         selective and should appear first in the Filter.
+--
+CREATE TABLE pred_order (
+	a int,
+	b int
+);
+INSERT INTO pred_order
+SELECT i, i % 2
+FROM generate_series(1, 1000) AS g(i);
+ANALYZE pred_order;
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order
+WHERE b = 0 AND a = 2;
+           QUERY PLAN            
+---------------------------------
+ Seq Scan on pred_order
+   Filter: ((a = 2) AND (b = 0))
+(2 rows)
+
+SELECT COUNT(*)
+FROM pred_order
+WHERE b = 0 AND a = 2;
+ count 
+-------
+     1
+(1 row)
+
+--
+-- Test setup. Use a SET clause to prevent SQL-function inlining.
+--             This keeps the operator node and its declared cost
+--             visible to the planner.
+--
+CREATE FUNCTION pred_order_eq_slow(int, int)
+RETURNS boolean
+LANGUAGE plpgsql
+IMMUTABLE
+PARALLEL SAFE
+COST 1.5
+AS $$
+BEGIN
+	RETURN $1 = $2;
+END;
+$$;
+CREATE OPERATOR #=# (
+	LEFTARG = int4,
+	RIGHTARG = int4,
+	PROCEDURE = pred_order_eq_slow,
+	RESTRICT = eqsel,
+	JOIN = eqjoinsel
+);
+--
+-- Test 2. Check that a more selective clause can move before a
+--         cheaper clause when its cost/selectivity rank is better.
+--         Expected result: the predicate on column a is evaluated
+--         before the predicate on column b. The old cost-only
+--         ordering would keep the cheaper predicate first.
+--
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order
+WHERE b = 0 AND a #=# 2;
+            QUERY PLAN             
+-----------------------------------
+ Seq Scan on pred_order
+   Filter: ((a #=# 2) AND (b = 0))
+(2 rows)
+
+SELECT COUNT(*)
+FROM pred_order
+WHERE b = 0 AND a #=# 2;
+ count 
+-------
+     1
+(1 row)
+
+-- Test setup.
+CREATE FUNCTION pred_order_eq_very_slow(int, int)
+RETURNS boolean
+LANGUAGE plpgsql
+IMMUTABLE
+PARALLEL SAFE
+COST 10
+AS $$
+BEGIN
+	RETURN $1 = $2;
+END;
+$$;
+CREATE OPERATOR #==# (
+	LEFTARG = int4,
+	RIGHTARG = int4,
+	PROCEDURE = pred_order_eq_very_slow,
+	RESTRICT = eqsel,
+	JOIN = eqjoinsel
+);
+--
+-- Test 3. Check that a much more expensive clause stays after a
+--         cheaper clause when selectivity does not compensate for
+--         the extra cost.
+--         Expected result: the predicate on column b is evaluated
+--         before the predicate on column a.
+--
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order
+WHERE b = 0 AND a #==# 2;
+             QUERY PLAN             
+------------------------------------
+ Seq Scan on pred_order
+   Filter: ((b = 0) AND (a #==# 2))
+(2 rows)
+
+SELECT COUNT(*)
+FROM pred_order
+WHERE b = 0 AND a #==# 2;
+ count 
+-------
+     1
+(1 row)
+
+--
+-- Test 4. Check that row-level security predicates are never
+--         reordered past user-supplied predicates even when
+--         the user predicate has a better rank.
+--         Expected result: the RLS predicate (b = 0) is evaluated
+--         before the user predicate (a #=# 2) because the latter
+--         is not leakproof and has a higher security_level.
+--
+ALTER TABLE pred_order ENABLE ROW LEVEL SECURITY;
+ALTER TABLE pred_order FORCE ROW LEVEL SECURITY;
+CREATE POLICY pred_order_policy ON pred_order
+	USING (b = 0);
+CREATE ROLE pred_order_rls_user;
+GRANT SELECT ON pred_order TO pred_order_rls_user;
+SET ROLE pred_order_rls_user;
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order
+WHERE a #=# 2;
+            QUERY PLAN             
+-----------------------------------
+ Seq Scan on pred_order
+   Filter: ((b = 0) AND (a #=# 2))
+(2 rows)
+
+SELECT COUNT(*)
+FROM pred_order
+WHERE a #=# 2;
+ count 
+-------
+     1
+(1 row)
+
+RESET ROLE;
+DROP POLICY pred_order_policy ON pred_order;
+REVOKE ALL ON pred_order FROM pred_order_rls_user;
+DROP ROLE pred_order_rls_user;
+ALTER TABLE pred_order DISABLE ROW LEVEL SECURITY;
+ALTER TABLE pred_order NO FORCE ROW LEVEL SECURITY;
+--
+-- Test 5. Check that clauses with identical ranks preserve their
+--         original order.
+--         Expected result: both columns have the same selectivity
+--         and cost, so the filter keeps the original clause order
+--         (c = 5) AND (d = 5).
+--
+CREATE TABLE pred_order_eq (
+	c int,
+	d int
+);
+INSERT INTO pred_order_eq
+SELECT i % 10, i % 10
+FROM generate_series(1, 1000) AS g(i);
+ANALYZE pred_order_eq;
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order_eq
+WHERE c = 5 AND d = 5;
+           QUERY PLAN            
+---------------------------------
+ Seq Scan on pred_order_eq
+   Filter: ((c = 5) AND (d = 5))
+(2 rows)
+
+SELECT COUNT(*)
+FROM pred_order_eq
+WHERE c = 5 AND d = 5;
+ count 
+-------
+   100
+(1 row)
+
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order_eq
+WHERE d = 5 AND c = 5;
+           QUERY PLAN            
+---------------------------------
+ Seq Scan on pred_order_eq
+   Filter: ((d = 5) AND (c = 5))
+(2 rows)
+
+SELECT COUNT(*)
+FROM pred_order_eq
+WHERE d = 5 AND c = 5;
+ count 
+-------
+   100
+(1 row)
+
+DROP TABLE pred_order_eq;
+DROP OPERATOR #==# (int4, int4);
+DROP FUNCTION pred_order_eq_very_slow(int, int);
+DROP OPERATOR #=# (int4, int4);
+DROP FUNCTION pred_order_eq_slow(int, int);
+DROP TABLE pred_order;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..bf63d4bfd69 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -636,7 +636,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle);
                                                     QUERY PLAN                                                    
 ------------------------------------------------------------------------------------------------------------------
  Seq Scan on document
-   Filter: ((cid <> 44) AND (cid <> 44) AND (cid < 50) AND (dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
+   Filter: ((cid < 50) AND (dlevel <= (InitPlan expr_1).col1) AND (cid <> 44) AND (cid <> 44) AND f_leak(dtitle))
    InitPlan expr_1
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
@@ -653,7 +653,7 @@ EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dt
    ->  Seq Scan on category
    ->  Hash
          ->  Seq Scan on document
-               Filter: ((cid <> 44) AND (cid <> 44) AND (cid < 50) AND (dlevel <= (InitPlan expr_1).col1) AND f_leak(dtitle))
+               Filter: ((cid < 50) AND (dlevel <= (InitPlan expr_1).col1) AND (cid <> 44) AND (cid <> 44) AND f_leak(dtitle))
 (9 rows)
 
 -- 44 would technically fail for both p2r and p1r, but we should get an error
@@ -2264,7 +2264,7 @@ EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
 -----------------------------------------------------------------------
  Update on b1
    ->  Seq Scan on b1
-         Filter: ((a > 0) AND (a = 4) AND ((a % 2) = 0) AND f_leak(b))
+         Filter: ((a = 4) AND (a > 0) AND ((a % 2) = 0) AND f_leak(b))
 (3 rows)
 
 UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
@@ -2274,7 +2274,7 @@ EXPLAIN (COSTS OFF) DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
 -----------------------------------------------------------------------
  Delete on b1
    ->  Seq Scan on b1
-         Filter: ((a > 0) AND (a = 6) AND ((a % 2) = 0) AND f_leak(b))
+         Filter: ((a = 6) AND (a > 0) AND ((a % 2) = 0) AND f_leak(b))
 (3 rows)
 
 DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
diff --git a/src/test/regress/expected/select.out b/src/test/regress/expected/select.out
index 34f040beecc..0cdd1a37b26 100644
--- a/src/test/regress/expected/select.out
+++ b/src/test/regress/expected/select.out
@@ -834,7 +834,7 @@ select unique2 from onek2 where unique2 = 11 and stringu1 < 'C';
                       QUERY PLAN                       
 -------------------------------------------------------
  Seq Scan on onek2
-   Filter: ((stringu1 < 'C'::name) AND (unique2 = 11))
+   Filter: ((unique2 = 11) AND (stringu1 < 'C'::name))
 (2 rows)
 
 select unique2 from onek2 where unique2 = 11 and stringu1 < 'C';
diff --git a/src/test/regress/expected/select_distinct.out b/src/test/regress/expected/select_distinct.out
index 379ba0bc9fa..2744da6a139 100644
--- a/src/test/regress/expected/select_distinct.out
+++ b/src/test/regress/expected/select_distinct.out
@@ -322,7 +322,7 @@ SELECT DISTINCT four FROM tenk1 WHERE four = 0 AND two <> 0;
 ---------------------------------------------
  Limit
    ->  Seq Scan on tenk1
-         Filter: ((two <> 0) AND (four = 0))
+         Filter: ((four = 0) AND (two <> 0))
 (3 rows)
 
 -- Ensure no rows are returned
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 8852160718f..24ed0dffb85 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -964,7 +964,7 @@ EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2;
          ->  Index Scan using base_tbl_pkey on base_tbl
                Index Cond: (a = 2)
          ->  Subquery Scan on rw_view1
-               Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2))
+               Filter: ((rw_view1.a = 2) AND (rw_view1.a < 10))
                ->  Bitmap Heap Scan on base_tbl base_tbl_1
                      Recheck Cond: (a > 0)
                      ->  Bitmap Index Scan on base_tbl_pkey
@@ -979,7 +979,7 @@ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE a=2;
          ->  Index Scan using base_tbl_pkey on base_tbl
                Index Cond: (a = 2)
          ->  Subquery Scan on rw_view1
-               Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2))
+               Filter: ((rw_view1.a = 2) AND (rw_view1.a < 10))
                ->  Bitmap Heap Scan on base_tbl base_tbl_1
                      Recheck Cond: (a > 0)
                      ->  Bitmap Index Scan on base_tbl_pkey
@@ -1255,7 +1255,7 @@ EXPLAIN (costs off) UPDATE rw_view2 SET a=3 WHERE a=2;
 ----------------------------------------------------------
  Update on rw_view1 rw_view1_1
    ->  Subquery Scan on rw_view1
-         Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2))
+         Filter: ((rw_view1.a = 2) AND (rw_view1.a < 10))
          ->  Bitmap Heap Scan on base_tbl
                Recheck Cond: (a > 0)
                ->  Bitmap Index Scan on base_tbl_pkey
@@ -1267,7 +1267,7 @@ EXPLAIN (costs off) DELETE FROM rw_view2 WHERE a=2;
 ----------------------------------------------------------
  Delete on rw_view1 rw_view1_1
    ->  Subquery Scan on rw_view1
-         Filter: ((rw_view1.a < 10) AND (rw_view1.a = 2))
+         Filter: ((rw_view1.a = 2) AND (rw_view1.a < 10))
          ->  Bitmap Heap Scan on base_tbl
                Recheck Cond: (a > 0)
                ->  Bitmap Index Scan on base_tbl_pkey
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 5d4f910155e..9c847809730 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_merge partition_split partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain memoize stats predicate numa eager_aggregate graph_table_rls planner_est
+test: partition_merge partition_split partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain memoize stats predicate predicate_order numa eager_aggregate graph_table_rls planner_est
 
 # ----------
 # Another group of parallel tests (compression)
diff --git a/src/test/regress/sql/predicate_order.sql b/src/test/regress/sql/predicate_order.sql
new file mode 100644
index 00000000000..556a86227b7
--- /dev/null
+++ b/src/test/regress/sql/predicate_order.sql
@@ -0,0 +1,193 @@
+--
+-- Predicate ordering by selectivity
+--
+-- Test 1. Check reordering of equal-cost clauses by selectivity.
+--         Both clauses use the built-in "=" operator and have the
+--         same per-tuple cost.  The clause on column "a" is more
+--         selective and should appear first in the Filter.
+--
+
+CREATE TABLE pred_order (
+	a int,
+	b int
+);
+
+INSERT INTO pred_order
+SELECT i, i % 2
+FROM generate_series(1, 1000) AS g(i);
+
+ANALYZE pred_order;
+
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order
+WHERE b = 0 AND a = 2;
+
+SELECT COUNT(*)
+FROM pred_order
+WHERE b = 0 AND a = 2;
+
+--
+-- Test setup. Use a SET clause to prevent SQL-function inlining.
+--             This keeps the operator node and its declared cost
+--             visible to the planner.
+--
+
+CREATE FUNCTION pred_order_eq_slow(int, int)
+RETURNS boolean
+LANGUAGE plpgsql
+IMMUTABLE
+PARALLEL SAFE
+COST 1.5
+AS $$
+BEGIN
+	RETURN $1 = $2;
+END;
+$$;
+
+CREATE OPERATOR #=# (
+	LEFTARG = int4,
+	RIGHTARG = int4,
+	PROCEDURE = pred_order_eq_slow,
+	RESTRICT = eqsel,
+	JOIN = eqjoinsel
+);
+
+--
+-- Test 2. Check that a more selective clause can move before a
+--         cheaper clause when its cost/selectivity rank is better.
+--         Expected result: the predicate on column a is evaluated
+--         before the predicate on column b. The old cost-only
+--         ordering would keep the cheaper predicate first.
+--
+
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order
+WHERE b = 0 AND a #=# 2;
+
+SELECT COUNT(*)
+FROM pred_order
+WHERE b = 0 AND a #=# 2;
+
+-- Test setup.
+
+CREATE FUNCTION pred_order_eq_very_slow(int, int)
+RETURNS boolean
+LANGUAGE plpgsql
+IMMUTABLE
+PARALLEL SAFE
+COST 10
+AS $$
+BEGIN
+	RETURN $1 = $2;
+END;
+$$;
+
+CREATE OPERATOR #==# (
+	LEFTARG = int4,
+	RIGHTARG = int4,
+	PROCEDURE = pred_order_eq_very_slow,
+	RESTRICT = eqsel,
+	JOIN = eqjoinsel
+);
+
+--
+-- Test 3. Check that a much more expensive clause stays after a
+--         cheaper clause when selectivity does not compensate for
+--         the extra cost.
+--         Expected result: the predicate on column b is evaluated
+--         before the predicate on column a.
+--
+
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order
+WHERE b = 0 AND a #==# 2;
+
+SELECT COUNT(*)
+FROM pred_order
+WHERE b = 0 AND a #==# 2;
+
+--
+-- Test 4. Check that row-level security predicates are never
+--         reordered past user-supplied predicates even when
+--         the user predicate has a better rank.
+--         Expected result: the RLS predicate (b = 0) is evaluated
+--         before the user predicate (a #=# 2) because the latter
+--         is not leakproof and has a higher security_level.
+--
+
+ALTER TABLE pred_order ENABLE ROW LEVEL SECURITY;
+ALTER TABLE pred_order FORCE ROW LEVEL SECURITY;
+
+CREATE POLICY pred_order_policy ON pred_order
+	USING (b = 0);
+
+CREATE ROLE pred_order_rls_user;
+GRANT SELECT ON pred_order TO pred_order_rls_user;
+
+SET ROLE pred_order_rls_user;
+
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order
+WHERE a #=# 2;
+
+SELECT COUNT(*)
+FROM pred_order
+WHERE a #=# 2;
+
+RESET ROLE;
+
+DROP POLICY pred_order_policy ON pred_order;
+REVOKE ALL ON pred_order FROM pred_order_rls_user;
+DROP ROLE pred_order_rls_user;
+
+ALTER TABLE pred_order DISABLE ROW LEVEL SECURITY;
+ALTER TABLE pred_order NO FORCE ROW LEVEL SECURITY;
+
+--
+-- Test 5. Check that clauses with identical ranks preserve their
+--         original order.
+--         Expected result: both columns have the same selectivity
+--         and cost, so the filter keeps the original clause order
+--         (c = 5) AND (d = 5).
+--
+
+CREATE TABLE pred_order_eq (
+	c int,
+	d int
+);
+
+INSERT INTO pred_order_eq
+SELECT i % 10, i % 10
+FROM generate_series(1, 1000) AS g(i);
+
+ANALYZE pred_order_eq;
+
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order_eq
+WHERE c = 5 AND d = 5;
+
+SELECT COUNT(*)
+FROM pred_order_eq
+WHERE c = 5 AND d = 5;
+
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM pred_order_eq
+WHERE d = 5 AND c = 5;
+
+SELECT COUNT(*)
+FROM pred_order_eq
+WHERE d = 5 AND c = 5;
+
+DROP TABLE pred_order_eq;
+
+DROP OPERATOR #==# (int4, int4);
+DROP FUNCTION pred_order_eq_very_slow(int, int);
+DROP OPERATOR #=# (int4, int4);
+DROP FUNCTION pred_order_eq_slow(int, int);
+DROP TABLE pred_order;
-- 
2.43.0