From 50a1bb492f86cac16fc81c39bfdf9143949f6df1 Mon Sep 17 00:00:00 2001
From: AyoubKAZ <kazarayoub2004@gmail.com>
Date: Tue, 27 Oct 2026 16:00:04 +0100
Subject: [PATCH v5] .

---
 src/backend/optimizer/plan/Makefile           |    1 +
 src/backend/optimizer/plan/meson.build        |    1 +
 src/backend/optimizer/plan/planner.c          |   13 +
 src/backend/optimizer/plan/planorexpand.c     |  699 ++++++++++
 src/backend/utils/misc/guc_parameters.dat     |   10 +
 src/backend/utils/misc/postgresql.conf.sample |    1 +
 src/include/optimizer/planmain.h              |    8 +
 src/test/regress/expected/join.out            |    2 +
 src/test/regress/expected/or_expansion.out    | 1145 +++++++++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/join.sql                 |    4 +
 src/test/regress/sql/or_expansion.sql         |  388 ++++++
 12 files changed, 2273 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/optimizer/plan/planorexpand.c
 create mode 100644 src/test/regress/expected/or_expansion.out
 create mode 100644 src/test/regress/sql/or_expansion.sql

diff --git a/src/backend/optimizer/plan/Makefile b/src/backend/optimizer/plan/Makefile
index 80ef162e484..3abff3baf65 100644
--- a/src/backend/optimizer/plan/Makefile
+++ b/src/backend/optimizer/plan/Makefile
@@ -18,6 +18,7 @@ OBJS = \
 	initsplan.o \
 	planagg.o \
 	planmain.o \
+	planorexpand.o \
 	planner.o \
 	setrefs.o \
 	subselect.o
diff --git a/src/backend/optimizer/plan/meson.build b/src/backend/optimizer/plan/meson.build
index c565b2adbcc..7e900b3107b 100644
--- a/src/backend/optimizer/plan/meson.build
+++ b/src/backend/optimizer/plan/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
   'initsplan.c',
   'planagg.c',
   'planmain.c',
+  'planorexpand.c',
   'planner.c',
   'setrefs.c',
   'subselect.c',
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index edaa42d713a..f0f0e526fe0 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -1850,6 +1850,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
 		List	   *activeWindows = NIL;
 		grouping_sets_data *gset_data = NULL;
 		standard_qp_extra qp_extra;
+		List	   *or_cands;
 
 		/* A recursive query should always have setOperations */
 		Assert(!root->hasRecursion);
@@ -1925,6 +1926,11 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
 		if (parse->hasAggs)
 			preprocess_minmax_aggregates(root);
 
+		/*
+		 * Consider expanding eligible OR clauses to UNION ALL (Append) paths.
+		 */
+		or_cands = plan_or_expansion_arms(root);
+
 		/*
 		 * Figure out whether there's a hard limit on the number of rows that
 		 * query_planner's result subplan needs to return.  Even if we know a
@@ -1961,6 +1967,13 @@ grouping_planner(PlannerInfo *root, double tuple_fraction,
 		 */
 		current_rel = query_planner(root, standard_qp_callback, &qp_extra);
 
+		/*
+		 * If we generated any OR-expansion candidates, add the resulting
+		 * Append path(s) to the topmost scan/join relation.
+		 */
+		if (or_cands != NIL)
+			add_or_expansion_paths(root, current_rel, or_cands);
+
 		/*
 		 * Convert the query's result tlist into PathTarget format.
 		 *
diff --git a/src/backend/optimizer/plan/planorexpand.c b/src/backend/optimizer/plan/planorexpand.c
new file mode 100644
index 00000000000..12cce52cb03
--- /dev/null
+++ b/src/backend/optimizer/plan/planorexpand.c
@@ -0,0 +1,699 @@
+/*-------------------------------------------------------------------------
+ *
+ * planorexpand.c
+ *	  Consider whether OR clauses in join queries can be expanded to
+ *	  UNION ALL (Append) paths.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/optimizer/plan/planorexpand.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "catalog/pg_class.h"
+#include "catalog/pg_type.h"
+#include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
+#include "optimizer/clauses.h"
+#include "optimizer/cost.h"
+#include "optimizer/optimizer.h"
+#include "optimizer/pathnode.h"
+#include "optimizer/planmain.h"
+#include "optimizer/planner.h"
+#include "optimizer/prep.h"
+#include "optimizer/subselect.h"
+#include "optimizer/tlist.h"
+#include "parser/parsetree.h"
+
+/* Parameters set by GUC */
+int			or_expansion_limit;
+
+typedef struct OrExpansionArm
+{
+	PlannerInfo *subroot;
+	RelOptInfo *final_rel;
+	Path	   *arm_path;
+	Index		arm_rti;
+}			OrExpansionArm;
+
+typedef struct OrExpansionCandidate
+{
+	List	   *arms;			/* List of OrExpansionArm */
+}			OrExpansionCandidate;
+
+static bool is_query_safe_for_or_expansion(PlannerInfo *root);
+static void or_expansion_qp_callback(PlannerInfo *root, void *extra);
+static OrExpansionCandidate * create_or_expansion_subpaths(PlannerInfo *root,
+														   BoolExpr *orclause,
+														   int qual_idx);
+static bool has_outer_joins(Node *jtnode);
+static bool match_target_expr(Node *want, Node *have);
+static Path *or_expansion_align_path(PlannerInfo *root, RelOptInfo *joinrel,
+									 Path *arm, PathTarget *target);
+static PathTarget *create_subquery_pathtarget(PlannerInfo *root, Index varno,
+											  PathTarget *subtarget);
+
+/*
+ * is_query_safe_for_or_expansion
+ *	  Verify query-level safety gates for OR expansion.
+ */
+static bool
+is_query_safe_for_or_expansion(PlannerInfo *root)
+{
+	Query	   *parse = root->parse;
+	ListCell   *lc;
+
+	/*
+	 * Only top-level queries may be expanded. Expanding within subqueries
+	 * leads to extreme planning overhead with nested subqueries.
+	 */
+	if (root->parent_root != NULL)
+		return false;
+
+	/* Volatile functions in jointree (WHERE or FROM) are unsafe to duplicate */
+	if (contain_volatile_functions((Node *) parse->jointree))
+		return false;
+
+	/*
+	 * Reject if the jointree contains any SubPlan or SubLink (in WHERE or
+	 * outer join ON conditions). At this point, SS_process_sublinks() has
+	 * already converted sublinks into SubPlan nodes; contain_subplans()
+	 * catches SubPlans, InitPlans, and any remaining SubLinks.
+	 */
+	if (contain_subplans((Node *) parse->jointree))
+		return false;
+
+	/* Disallow queries with lateral references */
+	if (root->hasLateralRTEs)
+		return false;
+
+	/* Disallow if query already contains append relations */
+	if (root->append_rel_list != NIL)
+		return false;
+
+	/*
+	 * Check RTEs. We allow only known to be safe RTE cases: - RTE_RELATION:
+	 * plain base tables (not partitioned, no inheritance, no tablesample, not
+	 * foreign) - RTE_JOIN: joins (except FULL joins) - RTE_VALUES:
+	 * non-volatile values lists without subplans - RTE_FUNCTION: non-volatile
+	 * function RTEs without subplans - RTE_TABLEFUNC: non-volatile table
+	 * functions without subplans - RTE_SUBQUERY: only flattened/inert
+	 * subqueries (subquery == NULL) - RTE_GROUP, RTE_RESULT: synthetic
+	 * planner entries Any other or active RTE kind (e.g. active RTE_SUBQUERY,
+	 * RTE_CTE) is rejected for safety.
+	 */
+	foreach(lc, parse->rtable)
+	{
+		RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
+
+		switch (rte->rtekind)
+		{
+			case RTE_RELATION:
+
+				/*
+				 * Disallow partitioned tables, foreign tables, and
+				 * inheritance
+				 */
+				if (rte->inh ||
+					rte->relkind == RELKIND_PARTITIONED_TABLE ||
+					rte->relkind == RELKIND_FOREIGN_TABLE)
+					return false;
+
+				/* Disallow tablesample (arms could sample different rows) */
+				if (rte->tablesample != NULL)
+					return false;
+				break;
+
+			case RTE_JOIN:
+				if (rte->jointype == JOIN_FULL)
+					return false;
+				break;
+
+			case RTE_VALUES:
+				if (contain_volatile_functions((Node *) rte->values_lists) ||
+					contain_subplans((Node *) rte->values_lists))
+					return false;
+				break;
+
+			case RTE_FUNCTION:
+				if (contain_volatile_functions((Node *) rte->functions) ||
+					contain_subplans((Node *) rte->functions))
+					return false;
+				break;
+
+			case RTE_TABLEFUNC:
+				if (contain_volatile_functions((Node *) rte->tablefunc) ||
+					contain_subplans((Node *) rte->tablefunc))
+					return false;
+				break;
+
+			case RTE_SUBQUERY:
+				/* Only allow inert/flattened subqueries; reject active ones */
+				if (rte->subquery != NULL)
+					return false;
+				break;
+
+			case RTE_GROUP:
+			case RTE_RESULT:
+				break;
+
+			default:
+				/* Reject RTE_CTE, RTE_NAMEDTUPLESTORE, and any future types */
+				return false;
+		}
+
+		/*
+		 * Security barrier quals must not contain volatile functions or
+		 * subplans
+		 */
+		if (contain_volatile_functions((Node *) rte->securityQuals) ||
+			contain_subplans((Node *) rte->securityQuals))
+			return false;
+	}
+
+	return true;
+}
+
+static bool
+has_outer_joins(Node *jtnode)
+{
+	if (jtnode == NULL)
+		return false;
+	if (IsA(jtnode, FromExpr))
+	{
+		FromExpr   *f = (FromExpr *) jtnode;
+		ListCell   *lc;
+
+		foreach(lc, f->fromlist)
+		{
+			if (has_outer_joins(lfirst(lc)))
+				return true;
+		}
+		return false;
+	}
+	if (IsA(jtnode, JoinExpr))
+	{
+		JoinExpr   *j = (JoinExpr *) jtnode;
+
+		if (IS_OUTER_JOIN(j->jointype))
+			return true;
+		return has_outer_joins(j->larg) || has_outer_joins(j->rarg);
+	}
+
+	return false;
+}
+
+/*
+ * or_expansion_qp_callback
+ *	  Query planner callback for subroot arm planning.
+ */
+static void
+or_expansion_qp_callback(PlannerInfo *root, void *extra)
+{
+	/*
+	 * Ordering is destroyed through UNION ALL / Append, so clear pathkeys
+	 * that might otherwise restrict lower paths.
+	 */
+	root->group_pathkeys = NIL;
+	root->window_pathkeys = NIL;
+	root->distinct_pathkeys = NIL;
+	root->sort_pathkeys = NIL;
+	root->query_pathkeys = NIL;
+}
+
+/*
+ * create_or_expansion_subpaths
+ *	  Create best paths for each arm of the candidate OR clause and append
+ *	  synthetic RTE_SUBQUERY entries to root's rtable.
+ */
+static OrExpansionCandidate *
+create_or_expansion_subpaths(PlannerInfo *root, BoolExpr *orclause, int qual_idx)
+{
+	OrExpansionCandidate *cand;
+	List	   *arms = NIL;
+	int			num_arms = list_length(orclause->args);
+	int			k;
+
+	for (k = 0; k < num_arms; k++)
+	{
+		PlannerInfo *subroot;
+		Query	   *parse;
+		List	   *arm_quals = NIL;
+		Node	   *arm_k;
+		int			j;
+		RelOptInfo *final_rel;
+		OrExpansionArm *arm;
+
+		/* Clone PlannerInfo state modeled on planagg.c */
+		subroot = palloc_object(PlannerInfo);
+		memcpy(subroot, root, sizeof(PlannerInfo));
+		subroot->query_level++;
+		subroot->parent_root = root;
+		subroot->plan_name = choose_plan_name(root->glob, "orexpand", false);
+		subroot->parse = parse = (Query *) copyObject(root->parse);
+
+		subroot->simple_rel_array = NULL;
+		subroot->simple_rte_array = NULL;
+		subroot->simple_rel_array_size = 0;
+
+		subroot->processed_tlist = (List *) copyObject(root->processed_tlist);
+		subroot->append_rel_list = (List *) copyObject(root->append_rel_list);
+
+		/* Each arm is planned for full retrieval by the Append */
+		subroot->tuple_fraction = 0.0;
+		subroot->limit_tuples = -1.0;
+
+		/*
+		 * Drop upper relation pointers copied from root, as the arm plans
+		 * only the scan/join level.
+		 */
+		memset(subroot->upper_rels, 0, sizeof(subroot->upper_rels));
+		memset(subroot->upper_targets, 0, sizeof(subroot->upper_targets));
+
+		/*
+		 * Give subroot its own JoinDomain and reset planner working lists so
+		 * query_planner(subroot) does not corrupt root's state.
+		 */
+		subroot->join_domains = list_make1(makeNode(JoinDomain));
+		subroot->plan_params = NIL;
+		subroot->outer_params = NULL;
+		subroot->rowMarks = NIL;
+		subroot->init_plans = NIL;
+		subroot->agginfos = NIL;
+		subroot->aggtransinfos = NIL;
+		subroot->join_info_list = NIL;
+		subroot->eq_classes = NIL;
+		subroot->placeholder_list = NIL;
+		subroot->placeholder_array = NULL;
+		subroot->placeholder_array_size = 0;
+		subroot->left_join_clauses = NIL;
+		subroot->right_join_clauses = NIL;
+		subroot->full_join_clauses = NIL;
+		subroot->fkey_list = NIL;
+		subroot->initial_rels = NIL;
+		subroot->last_rinfo_serial = 0;
+
+		/*
+		 * Build arm quals: R AND C_k AND (C_0 IS NOT TRUE) AND ... AND
+		 * (C_{k-1} IS NOT TRUE)
+		 */
+		arm_quals = list_delete_nth_cell(copyObject(root->parse->jointree->quals),
+										 qual_idx);
+
+		/* Add arm k's disjunct, flattened if it is an AND expression */
+		arm_k = (Node *) copyObject(list_nth(orclause->args, k));
+		arm_quals = list_concat(arm_quals, make_ands_implicit((Expr *) arm_k));
+
+		/* Add null-safe exclusion conditions for all previous arms */
+		for (j = 0; j < k; j++)
+		{
+			Node	   *prev_arm = (Node *) copyObject(list_nth(orclause->args, j));
+			BooleanTest *btest;
+
+			btest = makeNode(BooleanTest);
+			btest->arg = (Expr *) prev_arm;
+			btest->booltesttype = IS_NOT_TRUE;
+			btest->location = -1;
+
+			arm_quals = lappend(arm_quals, btest);
+		}
+
+		parse->jointree->quals = (Node *) arm_quals;
+
+		/* Re-run reduce_outer_joins if rtable contains outer joins */
+		if (has_outer_joins((Node *) parse->jointree))
+			reduce_outer_joins(subroot);
+
+		/* Plan this arm */
+		final_rel = query_planner(subroot, or_expansion_qp_callback, NULL);
+
+		if (!final_rel || !final_rel->cheapest_total_path ||
+			final_rel->cheapest_total_path->param_info != NULL)
+			return NULL;
+
+		/* Cope with params and initplans used within this subquery */
+		SS_identify_outer_params(subroot);
+		SS_charge_for_initplans(subroot, final_rel);
+
+		arm = palloc_object(OrExpansionArm);
+		arm->subroot = subroot;
+		arm->final_rel = final_rel;
+		arm->arm_path = final_rel->cheapest_total_path;
+		arm->arm_rti = 0;
+
+		arms = lappend(arms, arm);
+	}
+
+	cand = palloc_object(OrExpansionCandidate);
+	cand->arms = arms;
+	return cand;
+}
+
+/*
+ * plan_or_expansion_arms
+ *	  Check eligibility and generate subpaths for OR clauses in top-level WHERE.
+ *	  Called by grouping_planner() before query_planner().
+ */
+List *
+plan_or_expansion_arms(PlannerInfo *root)
+{
+	List	   *results = NIL;
+	Query	   *parse = root->parse;
+	bool		checked_query = false;
+	ListCell   *lc;
+	int			qual_idx;
+
+	if (or_expansion_limit < 2)
+		return NIL;
+
+	if (parse->commandType != CMD_SELECT ||
+		parse->rowMarks != NIL ||
+		parse->setOperations != NULL ||
+		parse->cteList != NIL ||
+		root->hasRecursion)
+		return NIL;
+
+	if (list_length(parse->rtable) < 2)
+		return NIL;
+
+	if (parse->jointree == NULL || parse->jointree->quals == NULL)
+		return NIL;
+
+	/* Scan top-level WHERE quals for eligible OR clauses */
+	qual_idx = 0;
+	foreach(lc, (List *) parse->jointree->quals)
+	{
+		Node	   *qual = (Node *) lfirst(lc);
+
+		if (is_orclause(qual))
+		{
+			BoolExpr   *orclause = (BoolExpr *) qual;
+			int			num_arms = list_length(orclause->args);
+			Relids		all_arm_varnos;
+			int			nrelations = 0;
+			int			rti = -1;
+
+			if (num_arms < 2 || num_arms > or_expansion_limit)
+			{
+				qual_idx++;
+				continue;
+			}
+
+			all_arm_varnos = pull_varnos(root, (Node *) orclause);
+
+			/*
+			 * Require the OR clause to reference at least 2 distinct
+			 * relations. pull_varnos() includes outer-join RTE_JOIN relids
+			 * via Var.varnullingrels; skip these so we only count base
+			 * relations.
+			 */
+			while ((rti = bms_next_member(all_arm_varnos, rti)) >= 0)
+			{
+				RangeTblEntry *rte = rt_fetch(rti, parse->rtable);
+
+				if (rte->rtekind != RTE_JOIN)
+					nrelations++;
+			}
+			bms_free(all_arm_varnos);
+
+			if (nrelations < 2)
+			{
+				qual_idx++;
+				continue;
+			}
+
+			/* Query-level safety check */
+			if (!checked_query)
+			{
+				if (!is_query_safe_for_or_expansion(root))
+					return NIL;
+				checked_query = true;
+			}
+
+			/* Transform OR clause and generate subpaths for each arm */
+			{
+				OrExpansionCandidate *cand = create_or_expansion_subpaths(root, orclause, qual_idx);
+
+				if (cand != NULL)
+					results = lappend(results, cand);
+			}
+		}
+		qual_idx++;
+	}
+
+	/*
+	 * If any candidates were found, append their synthetic RTE_SUBQUERY
+	 * entries to root's rtable in a single pass.  Doing this after all
+	 * candidates are planned ensures that later candidates do not copy
+	 * earlier candidates' synthetic RTEs into their subroot parse trees.
+	 */
+	if (results != NIL)
+	{
+		ListCell   *lc_cand;
+
+		foreach(lc_cand, results)
+		{
+			OrExpansionCandidate *cand = (OrExpansionCandidate *) lfirst(lc_cand);
+			ListCell   *lc_arm;
+
+			foreach(lc_arm, cand->arms)
+			{
+				OrExpansionArm *arm = (OrExpansionArm *) lfirst(lc_arm);
+				RangeTblEntry *rte = makeNode(RangeTblEntry);
+
+				rte->rtekind = RTE_SUBQUERY;
+				rte->subquery = arm->subroot->parse;
+				rte->alias = NULL;
+				rte->eref = makeAlias(arm->subroot->plan_name, NIL);
+				rte->lateral = false;
+				rte->inh = false;
+				rte->inFromCl = false;
+
+				root->parse->rtable = lappend(root->parse->rtable, rte);
+				arm->arm_rti = list_length(root->parse->rtable);
+			}
+		}
+	}
+
+	return results;
+}
+
+/*
+ * match_target_expr
+ *	  Check if two targetlist expressions match, ignoring varnullingrels
+ *	  differences that may arise from outer join reduction in subroot.
+ */
+static bool
+match_target_expr(Node *want, Node *have)
+{
+	if (want == have)
+		return true;
+	if (want == NULL || have == NULL)
+		return false;
+	if (nodeTag(want) != nodeTag(have))
+		return false;
+
+	if (IsA(want, Var))
+	{
+		Var		   *v1 = (Var *) want;
+		Var		   *v2 = (Var *) have;
+
+		return (v1->varno == v2->varno &&
+				v1->varattno == v2->varattno &&
+				v1->varlevelsup == v2->varlevelsup);
+	}
+
+	if (IsA(want, PlaceHolderVar))
+	{
+		PlaceHolderVar *p1 = (PlaceHolderVar *) want;
+		PlaceHolderVar *p2 = (PlaceHolderVar *) have;
+
+		return (p1->phid == p2->phid &&
+				p1->phlevelsup == p2->phlevelsup);
+	}
+
+	return equal(want, have);
+}
+
+/*
+ * or_expansion_align_path
+ *	  Ensure the arm path emits columns in the exact order needed by target.
+ *	  If column order differs, wrap with a projection using the arm's own
+ *	  expressions.
+ */
+static Path *
+or_expansion_align_path(PlannerInfo *root, RelOptInfo *joinrel, Path *arm, PathTarget *target)
+{
+	List	   *want = target->exprs;
+	List	   *have = arm->pathtarget->exprs;
+	List	   *new_exprs = NIL;
+	ListCell   *lc_want;
+	PathTarget *newtarget;
+
+	/* identical target lists */
+	if (equal(want, have))
+		return arm;
+
+	if (list_length(want) != list_length(have))
+		return NULL;
+
+	foreach(lc_want, want)
+	{
+		Node	   *want_node = (Node *) lfirst(lc_want);
+		Node	   *matched = NULL;
+		ListCell   *lc;
+
+		foreach(lc, have)
+		{
+			Node	   *have_node = (Node *) lfirst(lc);
+
+			if (match_target_expr(want_node, have_node))
+			{
+				matched = have_node;
+				break;
+			}
+		}
+
+		if (matched == NULL)
+			return NULL;
+
+		new_exprs = lappend(new_exprs, matched);
+	}
+
+	if (equal(new_exprs, have))
+		return arm;
+
+	newtarget = create_empty_pathtarget();
+	newtarget->exprs = new_exprs;
+	set_pathtarget_cost_width(root, newtarget);
+
+	/* Apply projection in-place if supported, or wrap with a ProjectionPath */
+	return apply_projection_to_path(root, joinrel, arm, newtarget);
+}
+
+/*
+ * create_subquery_pathtarget
+ *	  Build a PathTarget for a synthetic subquery relation containing identity
+ *	  Vars referencing the subpath's output columns (varno, 1..n).
+ */
+static PathTarget *
+create_subquery_pathtarget(PlannerInfo *root, Index varno, PathTarget *subtarget)
+{
+	PathTarget *target = create_empty_pathtarget();
+	int			attno = 0;
+	ListCell   *lc;
+
+	foreach(lc, subtarget->exprs)
+	{
+		Node	   *expr = (Node *) lfirst(lc);
+		Var		   *var;
+
+		attno++;
+		var = makeVar(varno,
+					  attno,
+					  exprType(expr),
+					  exprTypmod(expr),
+					  exprCollation(expr),
+					  0);
+		target->exprs = lappend(target->exprs, var);
+	}
+
+	return set_pathtarget_cost_width(root, target);
+}
+
+/*
+ * add_or_expansion_paths
+ *	  Combine arm subpaths into an AppendPath and add to the top join rel.
+ *	  Called by grouping_planner() right after query_planner().
+ */
+void
+add_or_expansion_paths(PlannerInfo *root, RelOptInfo *joinrel, List *or_cands)
+{
+	ListCell   *lc;
+	bool		added_any = false;
+
+	if (joinrel == NULL || joinrel->reltarget == NULL)
+		return;
+
+	foreach(lc, or_cands)
+	{
+		OrExpansionCandidate *cand = (OrExpansionCandidate *) lfirst(lc);
+		List	   *aligned_subpaths = NIL;
+		ListCell   *lc_arm;
+		bool		valid = true;
+		AppendPathInput ainput;
+		AppendPath *appendpath;
+
+		/* Align and wrap each arm into a SubqueryScanPath */
+		foreach(lc_arm, cand->arms)
+		{
+			OrExpansionArm *arm = (OrExpansionArm *) lfirst(lc_arm);
+			Path	   *aligned;
+			RelOptInfo *arm_rel;
+			SubqueryScanPath *sqpath;
+
+			aligned = or_expansion_align_path(arm->subroot,
+											  arm->final_rel,
+											  arm->arm_path,
+											  joinrel->reltarget);
+			if (aligned == NULL)
+			{
+				valid = false;
+				break;
+			}
+
+			/* Build the simple relation for the synthetic subquery RTE */
+			arm_rel = build_simple_rel(root, arm->arm_rti, NULL);
+			arm_rel->subroot = arm->subroot;
+			arm_rel->tuples = arm_rel->rows = aligned->rows;
+
+			/* Build reltarget with identity Vars (arm_rti, 1..n) */
+			arm_rel->reltarget = create_subquery_pathtarget(root,
+															arm->arm_rti,
+															aligned->pathtarget);
+
+			/* Wrap the arm subpath in a SubqueryScanPath */
+			sqpath = create_subqueryscan_path(root,
+											  arm_rel,
+											  aligned,
+											  true, /* trivial_pathtarget */
+											  NIL,
+											  NULL);
+
+			aligned_subpaths = lappend(aligned_subpaths, (Path *) sqpath);
+		}
+
+		if (!valid)
+			continue;
+
+		ainput.subpaths = aligned_subpaths;
+		ainput.partial_subpaths = NIL;
+		ainput.child_append_relid_sets = NIL;
+
+		/*
+		 * Pass joinrel->rows to create_append_path to keep all paths for
+		 * joinrel at one consistent row estimate. Leave consider_parallel
+		 * false; the arm paths are non-partial, so the Append is not
+		 * parallel-aware.
+		 */
+		appendpath = create_append_path(root,
+										joinrel,
+										ainput,
+										NIL,
+										NULL,
+										0,
+										false,
+										joinrel->rows);
+
+		add_path(joinrel, (Path *) appendpath);
+		added_any = true;
+	}
+
+	/* Refigure which is the cheapest path for the joinrel */
+	if (added_any)
+		set_cheapest(joinrel);
+}
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 380679a76c0..8f6d63d0726 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2319,6 +2319,16 @@
   ifdef => 'DEBUG_BOUNDED_SORT',
 },
 
+{ name => 'or_expansion_limit', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
+  short_desc => 'Sets the maximum number of OR arms considered for OR-expansion.',
+  long_desc => 'The planner will consider expanding OR clauses if they have no more than this many arms. 0 disables OR-expansion.',
+  flags => 'GUC_EXPLAIN',
+  variable => 'or_expansion_limit',
+  boot_val => '8',
+  min => '0',
+  max => 'INT_MAX',
+},
+
 { name => 'output_plugin_libraries', type => 'string', context => 'PGC_SUSET', group => 'REPLICATION_SENDING',
   short_desc => 'Lists libraries that may be named as logical decoding output plugins.',
   long_desc => 'Users with REPLICATION privileges may only use plugins in this list when creating logical replication slots.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 01f98de8a9f..b11ea2a847e 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -495,6 +495,7 @@
 #jit = off                              # allow JIT compilation
 #join_collapse_limit = 8                # 1 disables collapsing of explicit
                                         # JOIN clauses
+#or_expansion_limit = 8                 # 0 disables OR-expansion to UNION ALL
 #plan_cache_mode = auto                 # auto, force_generic_plan or
                                         # force_custom_plan
 #recursive_worktable_factor = 10.0      # range 0.001-1000000
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index c23ab568b98..d338b7705ba 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -36,6 +36,14 @@ extern RelOptInfo *query_planner(PlannerInfo *root,
  */
 extern void preprocess_minmax_aggregates(PlannerInfo *root);
 
+/*
+ * prototypes for plan/planorexpand.c
+ */
+extern PGDLLIMPORT int or_expansion_limit;
+extern List *plan_or_expansion_arms(PlannerInfo *root);
+extern void add_or_expansion_paths(PlannerInfo *root, RelOptInfo *joinrel,
+								   List *or_cands);
+
 /*
  * prototypes for plan/createplan.c
  */
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 1e6aa380257..8658ea7a8ab 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -12561,6 +12561,7 @@ WHERE ss.unique1 = ss.phv AND t1.unique1 < 100;
 -- Test for a nested loop join involving index scan, transforming OR-clauses
 -- to SAOP.
 --
+SET or_expansion_limit = 0;
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) FROM tenk1 t1, tenk1 t2
 WHERE t2.thousand = t1.tenthous OR t2.thousand = t1.unique1 OR t2.thousand = t1.unique2;
@@ -12580,6 +12581,7 @@ WHERE t2.thousand = t1.tenthous OR t2.thousand = t1.unique1 OR t2.thousand = t1.
  20000
 (1 row)
 
+RESET or_expansion_limit;
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) FROM onek t1 LEFT JOIN tenk1 t2
     ON (t2.thousand = t1.tenthous OR t2.thousand = t1.thousand);
diff --git a/src/test/regress/expected/or_expansion.out b/src/test/regress/expected/or_expansion.out
new file mode 100644
index 00000000000..c158cda2cab
--- /dev/null
+++ b/src/test/regress/expected/or_expansion.out
@@ -0,0 +1,1145 @@
+--
+-- Tests for OR-clause expansion into UNION ALL Append paths
+--
+CREATE TABLE orexp_a (id int, val int);
+CREATE TABLE orexp_b (x int, y int, val int);
+INSERT INTO orexp_a VALUES
+  (1, 10),
+  (2, 20),
+  (3, 30),
+  (4, NULL),
+  (5, 50);
+INSERT INTO orexp_b VALUES
+  (1, 99, 100),
+  (99, 2, 200),
+  (3, 3, 300),
+  (4, 99, 400),
+  (99, 99, 500),
+  (NULL, NULL, 600);
+-- check GUC parameter
+SHOW or_expansion_limit;
+ or_expansion_limit 
+--------------------
+ 8
+(1 row)
+
+--
+-- Disjunctive join conditions
+--
+-- disjunction of join keys
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.x, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+ORDER BY a.id, b.x, b.y;
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Sort
+   Sort Key: a_1.id, b_1.x, b_1.y
+   ->  Append
+         ->  Merge Join
+               Merge Cond: (b_1.x = a_1.id)
+               ->  Sort
+                     Sort Key: b_1.x
+                     ->  Seq Scan on orexp_b b_1
+               ->  Sort
+                     Sort Key: a_1.id
+                     ->  Seq Scan on orexp_a a_1
+         ->  Merge Join
+               Merge Cond: (b_2.y = a_2.id)
+               Join Filter: ((a_2.id = b_2.x) IS NOT TRUE)
+               ->  Sort
+                     Sort Key: b_2.y
+                     ->  Seq Scan on orexp_b b_2
+               ->  Sort
+                     Sort Key: a_2.id
+                     ->  Seq Scan on orexp_a a_2
+(20 rows)
+
+SELECT a.id, b.x, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+ORDER BY a.id, b.x, b.y;
+ id | x  | y  
+----+----+----
+  1 |  1 | 99
+  2 | 99 |  2
+  3 |  3 |  3
+  4 |  4 | 99
+(4 rows)
+
+-- ensure results match when disabled
+SET or_expansion_limit = 0;
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.x, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+ORDER BY a.id, b.x, b.y;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Sort
+   Sort Key: a.id, b.x, b.y
+   ->  Nested Loop
+         Join Filter: ((a.id = b.x) OR (a.id = b.y))
+         ->  Seq Scan on orexp_a a
+         ->  Materialize
+               ->  Seq Scan on orexp_b b
+(7 rows)
+
+SELECT a.id, b.x, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+ORDER BY a.id, b.x, b.y;
+ id | x  | y  
+----+----+----
+  1 |  1 | 99
+  2 | 99 |  2
+  3 |  3 |  3
+  4 |  4 | 99
+(4 rows)
+
+RESET or_expansion_limit;
+--
+-- Cross-table filter predicates
+--
+-- cross-table disjunction with separate join condition
+EXPLAIN (COSTS OFF)
+SELECT a.id, a.val, b.val FROM orexp_a a JOIN orexp_b b ON a.id = b.x
+WHERE a.val = 10 OR b.val = 300
+ORDER BY a.id, a.val, b.val;
+                      QUERY PLAN                      
+------------------------------------------------------
+ Sort
+   Sort Key: a_1.id, a_1.val, b_1.val
+   ->  Append
+         ->  Hash Join
+               Hash Cond: (b_1.x = a_1.id)
+               ->  Seq Scan on orexp_b b_1
+               ->  Hash
+                     ->  Seq Scan on orexp_a a_1
+                           Filter: (val = 10)
+         ->  Hash Join
+               Hash Cond: (a_2.id = b_2.x)
+               ->  Seq Scan on orexp_a a_2
+                     Filter: ((val = 10) IS NOT TRUE)
+               ->  Hash
+                     ->  Seq Scan on orexp_b b_2
+                           Filter: (val = 300)
+(16 rows)
+
+SELECT a.id, a.val, b.val FROM orexp_a a JOIN orexp_b b ON a.id = b.x
+WHERE a.val = 10 OR b.val = 300
+ORDER BY a.id, a.val, b.val;
+ id | val | val 
+----+-----+-----
+  1 |  10 | 100
+  3 |  30 | 300
+(2 rows)
+
+--
+-- Aggregation and grouping
+--
+-- GROUP BY over an expanded join
+EXPLAIN (COSTS OFF)
+SELECT a.id, count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY a.id
+ORDER BY a.id;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Sort
+   Sort Key: a_1.id
+   ->  HashAggregate
+         Group Key: a_1.id
+         ->  Append
+               ->  Merge Join
+                     Merge Cond: (b_1.x = a_1.id)
+                     ->  Sort
+                           Sort Key: b_1.x
+                           ->  Seq Scan on orexp_b b_1
+                     ->  Sort
+                           Sort Key: a_1.id
+                           ->  Seq Scan on orexp_a a_1
+               ->  Merge Join
+                     Merge Cond: (b_2.y = a_2.id)
+                     Join Filter: ((a_2.id = b_2.x) IS NOT TRUE)
+                     ->  Sort
+                           Sort Key: b_2.y
+                           ->  Seq Scan on orexp_b b_2
+                     ->  Sort
+                           Sort Key: a_2.id
+                           ->  Seq Scan on orexp_a a_2
+(22 rows)
+
+SELECT a.id, count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY a.id
+ORDER BY a.id;
+ id | count 
+----+-------
+  1 |     1
+  2 |     1
+  3 |     1
+  4 |     1
+(4 rows)
+
+-- zero-column target
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y;
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Aggregate
+   ->  Append
+         ->  Merge Join
+               Merge Cond: (b_1.x = a_1.id)
+               ->  Sort
+                     Sort Key: b_1.x
+                     ->  Seq Scan on orexp_b b_1
+               ->  Sort
+                     Sort Key: a_1.id
+                     ->  Seq Scan on orexp_a a_1
+         ->  Merge Join
+               Merge Cond: (b_2.y = a_2.id)
+               Join Filter: ((a_2.id = b_2.x) IS NOT TRUE)
+               ->  Sort
+                     Sort Key: b_2.y
+                     ->  Seq Scan on orexp_b b_2
+               ->  Sort
+                     Sort Key: a_2.id
+                     ->  Seq Scan on orexp_a a_2
+(19 rows)
+
+SELECT count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y;
+ count 
+-------
+     4
+(1 row)
+
+-- non-aggregate HAVING clause
+EXPLAIN (COSTS OFF)
+SELECT a.id, count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY a.id
+HAVING a.id > 1
+ORDER BY a.id;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Sort
+   Sort Key: a_1.id
+   ->  HashAggregate
+         Group Key: a_1.id
+         ->  Append
+               ->  Merge Join
+                     Merge Cond: (a_1.id = b_1.x)
+                     ->  Sort
+                           Sort Key: a_1.id
+                           ->  Seq Scan on orexp_a a_1
+                                 Filter: (id > 1)
+                     ->  Sort
+                           Sort Key: b_1.x
+                           ->  Seq Scan on orexp_b b_1
+               ->  Merge Join
+                     Merge Cond: (a_2.id = b_2.y)
+                     Join Filter: ((a_2.id = b_2.x) IS NOT TRUE)
+                     ->  Sort
+                           Sort Key: a_2.id
+                           ->  Seq Scan on orexp_a a_2
+                                 Filter: (id > 1)
+                     ->  Sort
+                           Sort Key: b_2.y
+                           ->  Seq Scan on orexp_b b_2
+(24 rows)
+
+-- aggregate HAVING clause
+EXPLAIN (COSTS OFF)
+SELECT a.id, count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY a.id
+HAVING count(*) > 1
+ORDER BY a.id;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Sort
+   Sort Key: a_1.id
+   ->  HashAggregate
+         Group Key: a_1.id
+         Filter: (count(*) > 1)
+         ->  Append
+               ->  Merge Join
+                     Merge Cond: (b_1.x = a_1.id)
+                     ->  Sort
+                           Sort Key: b_1.x
+                           ->  Seq Scan on orexp_b b_1
+                     ->  Sort
+                           Sort Key: a_1.id
+                           ->  Seq Scan on orexp_a a_1
+               ->  Merge Join
+                     Merge Cond: (b_2.y = a_2.id)
+                     Join Filter: ((a_2.id = b_2.x) IS NOT TRUE)
+                     ->  Sort
+                           Sort Key: b_2.y
+                           ->  Seq Scan on orexp_b b_2
+                     ->  Sort
+                           Sort Key: a_2.id
+                           ->  Seq Scan on orexp_a a_2
+(23 rows)
+
+-- grouping sets
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.x, grouping (a.id), count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY GROUPING SETS ((a.id), (b.x), ())
+ORDER BY 1, 2, 3, 4;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Sort
+   Sort Key: a_1.id, b_1.x, (GROUPING(a_1.id)), (count(*))
+   ->  MixedAggregate
+         Hash Key: a_1.id
+         Hash Key: b_1.x
+         Group Key: ()
+         ->  Append
+               ->  Merge Join
+                     Merge Cond: (b_1.x = a_1.id)
+                     ->  Sort
+                           Sort Key: b_1.x
+                           ->  Seq Scan on orexp_b b_1
+                     ->  Sort
+                           Sort Key: a_1.id
+                           ->  Seq Scan on orexp_a a_1
+               ->  Merge Join
+                     Merge Cond: (b_2.y = a_2.id)
+                     Join Filter: ((a_2.id = b_2.x) IS NOT TRUE)
+                     ->  Sort
+                           Sort Key: b_2.y
+                           ->  Seq Scan on orexp_b b_2
+                     ->  Sort
+                           Sort Key: a_2.id
+                           ->  Seq Scan on orexp_a a_2
+(24 rows)
+
+SELECT a.id, b.x, grouping (a.id), count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY GROUPING SETS ((a.id), (b.x), ())
+ORDER BY 1, 2, 3, 4;
+ id | x  | grouping | count 
+----+----+----------+-------
+  1 |    |        0 |     1
+  2 |    |        0 |     1
+  3 |    |        0 |     1
+  4 |    |        0 |     1
+    |  1 |        1 |     1
+    |  3 |        1 |     1
+    |  4 |        1 |     1
+    | 99 |        1 |     1
+    |    |        1 |     4
+(9 rows)
+
+--
+-- Subqueries, views, and derived tables
+--
+-- pulled-up simple subquery
+EXPLAIN (COSTS OFF)
+SELECT x.id, b.y FROM (SELECT id, val FROM orexp_a) x, orexp_b b
+WHERE x.id = b.x OR x.val = b.y
+ORDER BY 1, 2;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Sort
+   Sort Key: orexp_a_1.id, b_1.y
+   ->  Append
+         ->  Merge Join
+               Merge Cond: (b_1.x = orexp_a_1.id)
+               ->  Sort
+                     Sort Key: b_1.x
+                     ->  Seq Scan on orexp_b b_1
+               ->  Sort
+                     Sort Key: orexp_a_1.id
+                     ->  Seq Scan on orexp_a orexp_a_1
+         ->  Merge Join
+               Merge Cond: (b_2.y = orexp_a_2.val)
+               Join Filter: ((orexp_a_2.id = b_2.x) IS NOT TRUE)
+               ->  Sort
+                     Sort Key: b_2.y
+                     ->  Seq Scan on orexp_b b_2
+               ->  Sort
+                     Sort Key: orexp_a_2.val
+                     ->  Seq Scan on orexp_a orexp_a_2
+(20 rows)
+
+SELECT x.id, b.y FROM (SELECT id, val FROM orexp_a) x, orexp_b b
+WHERE x.id = b.x OR x.val = b.y
+ORDER BY 1, 2;
+ id | y  
+----+----
+  1 | 99
+  3 |  3
+  4 | 99
+(3 rows)
+
+-- pulled-up subquery containing outer join
+CREATE TABLE orexp_c (id int, val int);
+CREATE TABLE orexp_d (id int, val int);
+INSERT INTO orexp_c VALUES (1, 10), (2, 20), (3, NULL);
+INSERT INTO orexp_d VALUES (1, 100), (3, 300), (5, 500);
+EXPLAIN (COSTS OFF)
+SELECT x.cid, b.y FROM (SELECT c.id AS cid, c.val AS cval, d.val + 1 AS dval
+                        FROM orexp_c c LEFT JOIN orexp_d d ON c.id = d.id) x,
+                      orexp_b b
+WHERE x.dval = b.y OR x.cid = b.x
+ORDER BY 1, 2;
+                           QUERY PLAN                            
+-----------------------------------------------------------------
+ Sort
+   Sort Key: c_1.id, b_1.y
+   ->  Append
+         ->  Merge Join
+               Merge Cond: (c_1.id = d_1.id)
+               ->  Sort
+                     Sort Key: c_1.id
+                     ->  Seq Scan on orexp_c c_1
+               ->  Sort
+                     Sort Key: d_1.id
+                     ->  Merge Join
+                           Merge Cond: (b_1.y = ((d_1.val + 1)))
+                           ->  Sort
+                                 Sort Key: b_1.y
+                                 ->  Seq Scan on orexp_b b_1
+                           ->  Sort
+                                 Sort Key: ((d_1.val + 1))
+                                 ->  Seq Scan on orexp_d d_1
+         ->  Merge Right Join
+               Merge Cond: (d_2.id = c_2.id)
+               Filter: (((d_2.val + 1) = b_2.y) IS NOT TRUE)
+               ->  Sort
+                     Sort Key: d_2.id
+                     ->  Seq Scan on orexp_d d_2
+               ->  Materialize
+                     ->  Merge Join
+                           Merge Cond: (b_2.x = c_2.id)
+                           ->  Sort
+                                 Sort Key: b_2.x
+                                 ->  Seq Scan on orexp_b b_2
+                           ->  Sort
+                                 Sort Key: c_2.id
+                                 ->  Seq Scan on orexp_c c_2
+(33 rows)
+
+SELECT x.cid, b.y FROM (SELECT c.id AS cid, c.val AS cval, d.val + 1 AS dval
+                        FROM orexp_c c LEFT JOIN orexp_d d ON c.id = d.id) x,
+                      orexp_b b
+WHERE x.dval = b.y OR x.cid = b.x
+ORDER BY 1, 2;
+ cid | y  
+-----+----
+   1 | 99
+   3 |  3
+(2 rows)
+
+-- view expanded into pulled-up subquery
+CREATE VIEW orexp_v AS
+  SELECT a.id AS aid, a.val AS aval, b.x AS bx FROM orexp_a a, orexp_b b;
+EXPLAIN (COSTS OFF)
+SELECT v.aid, b.y FROM orexp_v v, orexp_b b
+WHERE v.aval = b.y OR v.bx = b.y
+ORDER BY 1, 2;
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Sort
+   Sort Key: a_1.id, b_2.y
+   ->  Append
+         ->  Nested Loop
+               ->  Merge Join
+                     Merge Cond: (b_2.y = a_1.val)
+                     ->  Sort
+                           Sort Key: b_2.y
+                           ->  Seq Scan on orexp_b b_2
+                     ->  Sort
+                           Sort Key: a_1.val
+                           ->  Seq Scan on orexp_a a_1
+               ->  Materialize
+                     ->  Seq Scan on orexp_b b_3
+         ->  Nested Loop
+               Join Filter: ((a_2.val = b_4.y) IS NOT TRUE)
+               ->  Merge Join
+                     Merge Cond: (b_5.x = b_4.y)
+                     ->  Sort
+                           Sort Key: b_5.x
+                           ->  Seq Scan on orexp_b b_5
+                     ->  Sort
+                           Sort Key: b_4.y
+                           ->  Seq Scan on orexp_b b_4
+               ->  Materialize
+                     ->  Seq Scan on orexp_a a_2
+(26 rows)
+
+SELECT v.aid, b.y FROM orexp_v v, orexp_b b
+WHERE v.aval = b.y OR v.bx = b.y
+ORDER BY 1, 2;
+ aid | y  
+-----+----
+   1 |  3
+   1 | 99
+   1 | 99
+   1 | 99
+   1 | 99
+   1 | 99
+   1 | 99
+   2 |  3
+   2 | 99
+   2 | 99
+   2 | 99
+   2 | 99
+   2 | 99
+   2 | 99
+   3 |  3
+   3 | 99
+   3 | 99
+   3 | 99
+   3 | 99
+   3 | 99
+   3 | 99
+   4 |  3
+   4 | 99
+   4 | 99
+   4 | 99
+   4 | 99
+   4 | 99
+   4 | 99
+   5 |  3
+   5 | 99
+   5 | 99
+   5 | 99
+   5 | 99
+   5 | 99
+   5 | 99
+(35 rows)
+
+-- non-table RTE_RESULT derived table
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b, (SELECT 1 AS one) x
+WHERE a.id = b.x OR a.id = b.y
+ORDER BY 1, 2;
+                        QUERY PLAN                         
+-----------------------------------------------------------
+ Sort
+   Sort Key: a_1.id, b_1.y
+   ->  Append
+         ->  Merge Join
+               Merge Cond: (b_1.x = a_1.id)
+               ->  Sort
+                     Sort Key: b_1.x
+                     ->  Seq Scan on orexp_b b_1
+               ->  Sort
+                     Sort Key: a_1.id
+                     ->  Seq Scan on orexp_a a_1
+         ->  Merge Join
+               Merge Cond: (b_2.y = a_2.id)
+               Join Filter: ((a_2.id = b_2.x) IS NOT TRUE)
+               ->  Sort
+                     Sort Key: b_2.y
+                     ->  Seq Scan on orexp_b b_2
+               ->  Sort
+                     Sort Key: a_2.id
+                     ->  Seq Scan on orexp_a a_2
+(20 rows)
+
+--
+-- Non-table range table entries
+--
+-- VALUES RTE with nulls
+EXPLAIN (COSTS OFF)
+SELECT v.a, b.y FROM (VALUES (1, 99), (2, NULL), (3, 3), (NULL, 4)) v (a, b), orexp_b b
+WHERE v.a = b.x OR v.b = b.y
+ORDER BY 1, 2;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Sort
+   Sort Key: "*VALUES*_1".column1, b_1.y
+   ->  Append
+         ->  Hash Join
+               Hash Cond: (b_1.x = "*VALUES*_1".column1)
+               ->  Seq Scan on orexp_b b_1
+               ->  Hash
+                     ->  Values Scan on "*VALUES*_1"
+         ->  Hash Join
+               Hash Cond: (b_2.y = "*VALUES*_2".column2)
+               Join Filter: (("*VALUES*_2".column1 = b_2.x) IS NOT TRUE)
+               ->  Seq Scan on orexp_b b_2
+               ->  Hash
+                     ->  Values Scan on "*VALUES*_2"
+(14 rows)
+
+SELECT v.a, b.y FROM (VALUES (1, 99), (2, NULL), (3, 3), (NULL, 4)) v (a, b), orexp_b b
+WHERE v.a = b.x OR v.b = b.y
+ORDER BY 1, 2;
+ a | y  
+---+----
+ 1 | 99
+ 1 | 99
+ 1 | 99
+ 3 |  3
+(4 rows)
+
+-- set-returning function RTEs
+EXPLAIN (COSTS OFF)
+SELECT g, b.y FROM generate_series(1, 4) g, orexp_b b
+WHERE g = b.x OR g = b.y
+ORDER BY 1, 2;
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Sort
+   Sort Key: g_1.g, b_1.y
+   ->  Append
+         ->  Hash Join
+               Hash Cond: (b_1.x = g_1.g)
+               ->  Seq Scan on orexp_b b_1
+               ->  Hash
+                     ->  Function Scan on generate_series g_1
+         ->  Hash Join
+               Hash Cond: (b_2.y = g_2.g)
+               Join Filter: ((g_2.g = b_2.x) IS NOT TRUE)
+               ->  Seq Scan on orexp_b b_2
+               ->  Hash
+                     ->  Function Scan on generate_series g_2
+(14 rows)
+
+SELECT g, b.y FROM generate_series(1, 4) g, orexp_b b
+WHERE g = b.x OR g = b.y
+ORDER BY 1, 2;
+ g | y  
+---+----
+ 1 | 99
+ 2 |  2
+ 3 |  3
+ 4 | 99
+(4 rows)
+
+-- table functions
+EXPLAIN (COSTS OFF)
+SELECT jt.a, b.y FROM json_table('{"a": 1}'::jsonb, '$' COLUMNS (a int PATH '$.a')) jt, orexp_b b
+WHERE jt.a = b.x OR jt.a = b.y;
+                         QUERY PLAN                         
+------------------------------------------------------------
+ Append
+   ->  Hash Join
+         Hash Cond: (b_1.x = jt_1.a)
+         ->  Seq Scan on orexp_b b_1
+         ->  Hash
+               ->  Table Function Scan on "json_table" jt_1
+   ->  Hash Join
+         Hash Cond: (b_2.y = jt_2.a)
+         Join Filter: ((jt_2.a = b_2.x) IS NOT TRUE)
+         ->  Seq Scan on orexp_b b_2
+         ->  Hash
+               ->  Table Function Scan on "json_table" jt_2
+(12 rows)
+
+SELECT jt.a, b.y FROM json_table('{"a": 1}'::jsonb, '$' COLUMNS (a int PATH '$.a')) jt, orexp_b b
+WHERE jt.a = b.x OR jt.a = b.y;
+ a | y  
+---+----
+ 1 | 99
+(1 row)
+
+--
+-- Pseudoconstant predicates
+--
+-- arm with pseudoconstant qual
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR (b.y = 2 AND now() > '2000-01-01 00:00:00'::timestamp);
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
+ Append
+   ->  Merge Join
+         Merge Cond: (b_1.x = a_1.id)
+         ->  Sort
+               Sort Key: b_1.x
+               ->  Seq Scan on orexp_b b_1
+         ->  Sort
+               Sort Key: a_1.id
+               ->  Seq Scan on orexp_a a_1
+   ->  Result
+         One-Time Filter: (now() > 'Sat Jan 01 00:00:00 2000'::timestamp without time zone)
+         ->  Nested Loop
+               Join Filter: ((a_2.id = b_2.x) IS NOT TRUE)
+               ->  Seq Scan on orexp_a a_2
+               ->  Materialize
+                     ->  Seq Scan on orexp_b b_2
+                           Filter: (y = 2)
+(17 rows)
+
+SELECT count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR (b.y = 2 AND now() > '2000-01-01 00:00:00'::timestamp);
+ count 
+-------
+     8
+(1 row)
+
+-- pseudoconstant arm alongside subquery arm
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE (b.y = 2 AND now() > '2000-01-01 00:00:00'::timestamp)
+   OR (a.id = b.x AND a.val > (SELECT count(*) FROM orexp_b));
+                                                        QUERY PLAN                                                         
+---------------------------------------------------------------------------------------------------------------------------
+ Append
+   InitPlan expr_1
+     ->  Aggregate
+           ->  Seq Scan on orexp_b
+   ->  Result
+         One-Time Filter: (now() > 'Sat Jan 01 00:00:00 2000'::timestamp without time zone)
+         ->  Nested Loop
+               ->  Seq Scan on orexp_a a_1
+               ->  Materialize
+                     ->  Seq Scan on orexp_b b_1
+                           Filter: (y = 2)
+   ->  Merge Join
+         Merge Cond: (a_2.id = b_2.x)
+         ->  Sort
+               Sort Key: a_2.id
+               ->  Seq Scan on orexp_a a_2
+                     Filter: (val > (InitPlan expr_1).col1)
+         ->  Sort
+               Sort Key: b_2.x
+               ->  Seq Scan on orexp_b b_2
+                     Filter: (((y = 2) AND (now() > 'Sat Jan 01 00:00:00 2000'::timestamp without time zone)) IS NOT TRUE)
+(21 rows)
+
+SELECT count(*) FROM orexp_a a, orexp_b b
+WHERE (b.y = 2 AND now() > '2000-01-01 00:00:00'::timestamp)
+   OR (a.id = b.x AND a.val > (SELECT count(*) FROM orexp_b));
+ count 
+-------
+     7
+(1 row)
+
+--
+-- Outer joins
+--
+-- FULL JOIN with strict WHERE quals
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a FULL JOIN orexp_b b ON a.id = b.x
+WHERE a.id = b.y OR b.x = a.id
+ORDER BY 1, 2;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Incremental Sort
+   Sort Key: a.id, b.y
+   Presorted Key: a.id
+   ->  Merge Join
+         Merge Cond: (b.x = a.id)
+         Join Filter: ((a.id = b.y) OR (b.x = a.id))
+         ->  Sort
+               Sort Key: b.x
+               ->  Seq Scan on orexp_b b
+         ->  Sort
+               Sort Key: a.id
+               ->  Seq Scan on orexp_a a
+(12 rows)
+
+SELECT a.id, b.y FROM orexp_a a FULL JOIN orexp_b b ON a.id = b.x
+WHERE a.id = b.y OR b.x = a.id
+ORDER BY 1, 2;
+ id | y  
+----+----
+  1 | 99
+  3 |  3
+  4 | 99
+(3 rows)
+
+-- LEFT JOIN with cross-relation disjunction in WHERE
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.x, b.y FROM orexp_a a LEFT JOIN orexp_b b ON (a.id = b.x OR a.val = b.y)
+WHERE b.val = 100 OR a.id = 1
+ORDER BY 1, 2, 3;
+                             QUERY PLAN                             
+--------------------------------------------------------------------
+ Sort
+   Sort Key: a_1.id, b_1.x, b_1.y
+   ->  Append
+         ->  Nested Loop
+               Join Filter: ((a_1.id = b_1.x) OR (a_1.val = b_1.y))
+               ->  Seq Scan on orexp_a a_1
+               ->  Materialize
+                     ->  Seq Scan on orexp_b b_1
+                           Filter: (val = 100)
+         ->  Nested Loop Left Join
+               Join Filter: ((a_2.id = b_2.x) OR (a_2.val = b_2.y))
+               Filter: ((b_2.val = 100) IS NOT TRUE)
+               ->  Seq Scan on orexp_a a_2
+                     Filter: (id = 1)
+               ->  Materialize
+                     ->  Seq Scan on orexp_b b_2
+(16 rows)
+
+SELECT a.id, b.x, b.y FROM orexp_a a LEFT JOIN orexp_b b ON (a.id = b.x OR a.val = b.y)
+WHERE b.val = 100 OR a.id = 1
+ORDER BY 1, 2, 3;
+ id | x | y  
+----+---+----
+  1 | 1 | 99
+(1 row)
+
+--
+-- Queries ineligible for OR expansion
+--
+-- volatile set-returning function
+EXPLAIN (COSTS OFF)
+SELECT g, b.y FROM generate_series(1, (random() * 3)::int) g, orexp_b b
+WHERE g = b.x OR g = b.y;
+                 QUERY PLAN                  
+---------------------------------------------
+ Nested Loop
+   Join Filter: ((g.g = b.x) OR (g.g = b.y))
+   ->  Function Scan on generate_series g
+   ->  Materialize
+         ->  Seq Scan on orexp_b b
+(5 rows)
+
+-- volatile VALUES list
+EXPLAIN (COSTS OFF)
+SELECT v.a, b.y FROM (VALUES ((random() * 3)::int)) v (a), orexp_b b
+WHERE v.a = b.x OR v.a = b.y;
+                                QUERY PLAN                                 
+---------------------------------------------------------------------------
+ Nested Loop
+   Join Filter: (("*VALUES*".column1 = b.x) OR ("*VALUES*".column1 = b.y))
+   ->  Values Scan on "*VALUES*"
+   ->  Seq Scan on orexp_b b
+(4 rows)
+
+-- volatile JSON_TABLE expressions
+EXPLAIN (COSTS OFF)
+SELECT jt.a, b.y FROM json_table(('{"a":' || (random() * 2)::int || '}')::jsonb,
+                                 '$' COLUMNS (a int PATH '$.a')) jt, orexp_b b
+WHERE jt.a = b.x OR jt.a = b.y;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Join Filter: ((jt.a = b.x) OR (jt.a = b.y))
+   ->  Table Function Scan on "json_table" jt
+   ->  Materialize
+         ->  Seq Scan on orexp_b b
+(5 rows)
+
+-- volatile function in WHERE
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR (a.id = b.y AND random() < 0.5);
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
+ Nested Loop
+   Join Filter: ((a.id = b.x) OR ((a.id = b.y) AND (random() < '0.5'::double precision)))
+   ->  Seq Scan on orexp_a a
+   ->  Materialize
+         ->  Seq Scan on orexp_b b
+(5 rows)
+
+-- correlated SubPlan in WHERE
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE (a.id = b.x OR a.id = b.y)
+  AND a.val > (SELECT max(b2.val) FROM orexp_b b2 WHERE b2.x = a.id);
+                    QUERY PLAN                    
+--------------------------------------------------
+ Nested Loop
+   Join Filter: ((a.id = b.x) OR (a.id = b.y))
+   ->  Seq Scan on orexp_b b
+   ->  Materialize
+         ->  Seq Scan on orexp_a a
+               Filter: (val > (SubPlan expr_1))
+               SubPlan expr_1
+                 ->  Aggregate
+                       ->  Seq Scan on orexp_b b2
+                             Filter: (x = a.id)
+(10 rows)
+
+-- TABLESAMPLE
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b TABLESAMPLE SYSTEM (50)
+WHERE a.id = b.x OR a.id = b.y;
+                  QUERY PLAN                   
+-----------------------------------------------
+ Nested Loop
+   Join Filter: ((a.id = b.x) OR (a.id = b.y))
+   ->  Seq Scan on orexp_a a
+   ->  Materialize
+         ->  Sample Scan on orexp_b b
+               Sampling: system ('50'::real)
+(6 rows)
+
+-- unreduced FULL JOIN
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a FULL JOIN orexp_b b ON a.id = b.x
+WHERE a.val IS NULL OR b.val IS NULL;
+                   QUERY PLAN                   
+------------------------------------------------
+ Merge Full Join
+   Merge Cond: (b.x = a.id)
+   Filter: ((a.val IS NULL) OR (b.val IS NULL))
+   ->  Sort
+         Sort Key: b.x
+         ->  Seq Scan on orexp_b b
+   ->  Sort
+         Sort Key: a.id
+         ->  Seq Scan on orexp_a a
+(9 rows)
+
+-- lateral reference
+EXPLAIN (COSTS OFF)
+SELECT a.id, l.ly FROM orexp_a a,
+       LATERAL (SELECT b.y AS ly FROM orexp_b b WHERE b.x = a.id OR b.y = a.val) l
+WHERE a.id = l.ly OR a.val = 1;
+                                     QUERY PLAN                                     
+------------------------------------------------------------------------------------
+ Nested Loop
+   Join Filter: (((b.x = a.id) OR (b.y = a.val)) AND ((a.id = b.y) OR (a.val = 1)))
+   ->  Seq Scan on orexp_a a
+   ->  Materialize
+         ->  Seq Scan on orexp_b b
+(5 rows)
+
+-- unflattened subquery with grouping
+EXPLAIN (COSTS OFF)
+SELECT a.id, s.n FROM orexp_a a,
+       (SELECT x AS sx, count(*) AS n FROM orexp_b GROUP BY x) s
+WHERE a.id = s.sx OR a.val = s.n;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Nested Loop
+   Join Filter: ((a.id = s.sx) OR (a.val = s.n))
+   ->  Seq Scan on orexp_a a
+   ->  Materialize
+         ->  Subquery Scan on s
+               ->  HashAggregate
+                     Group Key: orexp_b.x
+                     ->  Seq Scan on orexp_b
+(8 rows)
+
+-- append relation from UNION ALL
+EXPLAIN (COSTS OFF)
+SELECT u.id, a.val FROM (SELECT x AS id, val FROM orexp_b
+                         UNION ALL
+                         SELECT id, val FROM orexp_a) u, orexp_a a
+WHERE u.val = a.val OR u.id = a.id;
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Gather
+   Workers Planned: 2
+   ->  Nested Loop
+         Join Filter: ((orexp_a.val = a.val) OR (orexp_a.id = a.id))
+         ->  Parallel Append
+               ->  Parallel Seq Scan on orexp_a
+               ->  Parallel Seq Scan on orexp_b
+         ->  Materialize
+               ->  Seq Scan on orexp_a a
+(9 rows)
+
+-- partitioned table
+CREATE TABLE orexp_part (id int, d int) PARTITION BY RANGE (d);
+CREATE TABLE orexp_part1 PARTITION OF orexp_part FOR VALUES FROM (0) TO (10);
+CREATE TABLE orexp_part2 PARTITION OF orexp_part FOR VALUES FROM (10) TO (20);
+INSERT INTO orexp_part VALUES (1, 5), (2, 15);
+EXPLAIN (COSTS OFF)
+SELECT a.id, p.d FROM orexp_a a, orexp_part p
+WHERE a.id = p.id OR a.val = p.d;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Gather
+   Workers Planned: 2
+   ->  Nested Loop
+         Join Filter: ((a.id = p.id) OR (a.val = p.d))
+         ->  Parallel Append
+               ->  Parallel Seq Scan on orexp_part1 p_1
+               ->  Parallel Seq Scan on orexp_part2 p_2
+         ->  Materialize
+               ->  Seq Scan on orexp_a a
+(9 rows)
+
+-- table inheritance
+CREATE TABLE orexp_inh_parent (id int, val int);
+CREATE TABLE orexp_inh_child () INHERITS (orexp_inh_parent);
+INSERT INTO orexp_inh_parent VALUES (1, 10);
+INSERT INTO orexp_inh_child VALUES (2, 20);
+EXPLAIN (COSTS OFF)
+SELECT a.id, p.val FROM orexp_a a, orexp_inh_parent p
+WHERE a.id = p.id OR a.val = p.val;
+                         QUERY PLAN                          
+-------------------------------------------------------------
+ Gather
+   Workers Planned: 2
+   ->  Nested Loop
+         Join Filter: ((a.id = p.id) OR (a.val = p.val))
+         ->  Parallel Append
+               ->  Parallel Seq Scan on orexp_inh_child p_2
+               ->  Parallel Seq Scan on orexp_inh_parent p_1
+         ->  Materialize
+               ->  Seq Scan on orexp_a a
+(9 rows)
+
+--
+-- Permitted sublinks
+--
+-- uncorrelated scalar sublink
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE (a.id = b.x OR a.id = b.y)
+  AND a.val > (SELECT min(val) FROM orexp_b WHERE val > 150)
+ORDER BY 1, 2;
+                            QUERY PLAN                            
+------------------------------------------------------------------
+ Sort
+   Sort Key: a_1.id, b_1.y
+   InitPlan expr_1
+     ->  Aggregate
+           ->  Seq Scan on orexp_b
+                 Filter: (val > 150)
+   ->  Append
+         ->  Merge Join
+               Merge Cond: (a_1.id = b_1.x)
+               ->  Sort
+                     Sort Key: a_1.id
+                     ->  Seq Scan on orexp_a a_1
+                           Filter: (val > (InitPlan expr_1).col1)
+               ->  Sort
+                     Sort Key: b_1.x
+                     ->  Seq Scan on orexp_b b_1
+         ->  Merge Join
+               Merge Cond: (a_2.id = b_2.y)
+               Join Filter: ((a_2.id = b_2.x) IS NOT TRUE)
+               ->  Sort
+                     Sort Key: a_2.id
+                     ->  Seq Scan on orexp_a a_2
+                           Filter: (val > (InitPlan expr_1).col1)
+               ->  Sort
+                     Sort Key: b_2.y
+                     ->  Seq Scan on orexp_b b_2
+(26 rows)
+
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE (a.id = b.x OR a.id = b.y)
+  AND a.val > (SELECT min(val) FROM orexp_b WHERE val > 150)
+ORDER BY 1, 2;
+ id | y 
+----+---
+(0 rows)
+
+--
+-- Multiple OR clauses in WHERE
+--
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.val FROM orexp_a a, orexp_b b
+WHERE (a.id = b.x OR a.id = b.y) AND (a.val = b.val OR a.val = b.x)
+ORDER BY 1, 2;
+                                                 QUERY PLAN                                                 
+------------------------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: a_1.id, b_1.val
+   ->  Append
+         ->  Merge Join
+               Merge Cond: (b_1.x = a_1.id)
+               Join Filter: ((a_1.val = b_1.val) OR (a_1.val = b_1.x))
+               ->  Sort
+                     Sort Key: b_1.x
+                     ->  Seq Scan on orexp_b b_1
+               ->  Sort
+                     Sort Key: a_1.id
+                     ->  Seq Scan on orexp_a a_1
+         ->  Merge Join
+               Merge Cond: (b_2.y = a_2.id)
+               Join Filter: (((a_2.id = b_2.x) IS NOT TRUE) AND ((a_2.val = b_2.val) OR (a_2.val = b_2.x)))
+               ->  Sort
+                     Sort Key: b_2.y
+                     ->  Seq Scan on orexp_b b_2
+               ->  Sort
+                     Sort Key: a_2.id
+                     ->  Seq Scan on orexp_a a_2
+(21 rows)
+
+SELECT a.id, b.val FROM orexp_a a, orexp_b b
+WHERE (a.id = b.x OR a.id = b.y) AND (a.val = b.val OR a.val = b.x)
+ORDER BY 1, 2;
+ id | val 
+----+-----
+(0 rows)
+
+--
+-- Control via or_expansion_limit
+--
+-- arm count exceeds limit
+SET or_expansion_limit = 2;
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y OR a.val = b.val
+ORDER BY 1, 2;
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Sort
+   Sort Key: a.id, b.y
+   ->  Nested Loop
+         Join Filter: ((a.id = b.x) OR (a.id = b.y) OR (a.val = b.val))
+         ->  Seq Scan on orexp_a a
+         ->  Materialize
+               ->  Seq Scan on orexp_b b
+(7 rows)
+
+-- arm count within limit
+SET or_expansion_limit = 3;
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y OR a.val = b.val
+ORDER BY 1, 2;
+                                           QUERY PLAN                                           
+------------------------------------------------------------------------------------------------
+ Sort
+   Sort Key: a_1.id, b_1.y
+   ->  Append
+         ->  Merge Join
+               Merge Cond: (b_1.x = a_1.id)
+               ->  Sort
+                     Sort Key: b_1.x
+                     ->  Seq Scan on orexp_b b_1
+               ->  Sort
+                     Sort Key: a_1.id
+                     ->  Seq Scan on orexp_a a_1
+         ->  Merge Join
+               Merge Cond: (b_2.y = a_2.id)
+               Join Filter: ((a_2.id = b_2.x) IS NOT TRUE)
+               ->  Sort
+                     Sort Key: b_2.y
+                     ->  Seq Scan on orexp_b b_2
+               ->  Sort
+                     Sort Key: a_2.id
+                     ->  Seq Scan on orexp_a a_2
+         ->  Merge Join
+               Merge Cond: (b_3.val = a_3.val)
+               Join Filter: (((a_3.id = b_3.x) IS NOT TRUE) AND ((a_3.id = b_3.y) IS NOT TRUE))
+               ->  Sort
+                     Sort Key: b_3.val
+                     ->  Seq Scan on orexp_b b_3
+               ->  Sort
+                     Sort Key: a_3.val
+                     ->  Seq Scan on orexp_a a_3
+(29 rows)
+
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y OR a.val = b.val
+ORDER BY 1, 2;
+ id | y  
+----+----
+  1 | 99
+  2 |  2
+  3 |  3
+  4 | 99
+(4 rows)
+
+--
+-- Clean up
+--
+RESET or_expansion_limit;
+DROP VIEW orexp_v;
+DROP TABLE orexp_c, orexp_d;
+DROP TABLE orexp_part;
+DROP TABLE orexp_inh_parent CASCADE;
+NOTICE:  drop cascades to table orexp_inh_child
+DROP TABLE orexp_a, orexp_b;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 75063f87a4a..55921c948a5 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_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain memoize stats predicate numa eager_aggregate planner_est
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain memoize stats predicate numa eager_aggregate planner_est or_expansion
 
 # ----------
 # Another group of parallel tests (compression)
diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql
index 6264f23e081..1ae77201361 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -4806,12 +4806,16 @@ WHERE ss.unique1 = ss.phv AND t1.unique1 < 100;
 -- to SAOP.
 --
 
+SET or_expansion_limit = 0;
+
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) FROM tenk1 t1, tenk1 t2
 WHERE t2.thousand = t1.tenthous OR t2.thousand = t1.unique1 OR t2.thousand = t1.unique2;
 SELECT COUNT(*) FROM tenk1 t1, tenk1 t2
 WHERE t2.thousand = t1.tenthous OR t2.thousand = t1.unique1 OR t2.thousand = t1.unique2;
 
+RESET or_expansion_limit;
+
 EXPLAIN (COSTS OFF)
 SELECT COUNT(*) FROM onek t1 LEFT JOIN tenk1 t2
     ON (t2.thousand = t1.tenthous OR t2.thousand = t1.thousand);
diff --git a/src/test/regress/sql/or_expansion.sql b/src/test/regress/sql/or_expansion.sql
new file mode 100644
index 00000000000..6a4d03ab22f
--- /dev/null
+++ b/src/test/regress/sql/or_expansion.sql
@@ -0,0 +1,388 @@
+--
+-- Tests for OR-clause expansion into UNION ALL Append paths
+--
+
+CREATE TABLE orexp_a (id int, val int);
+CREATE TABLE orexp_b (x int, y int, val int);
+
+INSERT INTO orexp_a VALUES
+  (1, 10),
+  (2, 20),
+  (3, 30),
+  (4, NULL),
+  (5, 50);
+
+INSERT INTO orexp_b VALUES
+  (1, 99, 100),
+  (99, 2, 200),
+  (3, 3, 300),
+  (4, 99, 400),
+  (99, 99, 500),
+  (NULL, NULL, 600);
+
+-- check GUC parameter
+SHOW or_expansion_limit;
+
+--
+-- Disjunctive join conditions
+--
+
+-- disjunction of join keys
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.x, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+ORDER BY a.id, b.x, b.y;
+
+SELECT a.id, b.x, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+ORDER BY a.id, b.x, b.y;
+
+-- ensure results match when disabled
+SET or_expansion_limit = 0;
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.x, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+ORDER BY a.id, b.x, b.y;
+
+SELECT a.id, b.x, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+ORDER BY a.id, b.x, b.y;
+
+RESET or_expansion_limit;
+
+--
+-- Cross-table filter predicates
+--
+
+-- cross-table disjunction with separate join condition
+EXPLAIN (COSTS OFF)
+SELECT a.id, a.val, b.val FROM orexp_a a JOIN orexp_b b ON a.id = b.x
+WHERE a.val = 10 OR b.val = 300
+ORDER BY a.id, a.val, b.val;
+
+SELECT a.id, a.val, b.val FROM orexp_a a JOIN orexp_b b ON a.id = b.x
+WHERE a.val = 10 OR b.val = 300
+ORDER BY a.id, a.val, b.val;
+
+--
+-- Aggregation and grouping
+--
+
+-- GROUP BY over an expanded join
+EXPLAIN (COSTS OFF)
+SELECT a.id, count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY a.id
+ORDER BY a.id;
+
+SELECT a.id, count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY a.id
+ORDER BY a.id;
+
+-- zero-column target
+EXPLAIN (COSTS OFF)
+SELECT count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y;
+
+SELECT count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y;
+
+-- non-aggregate HAVING clause
+EXPLAIN (COSTS OFF)
+SELECT a.id, count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY a.id
+HAVING a.id > 1
+ORDER BY a.id;
+
+-- aggregate HAVING clause
+EXPLAIN (COSTS OFF)
+SELECT a.id, count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY a.id
+HAVING count(*) > 1
+ORDER BY a.id;
+
+-- grouping sets
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.x, grouping (a.id), count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY GROUPING SETS ((a.id), (b.x), ())
+ORDER BY 1, 2, 3, 4;
+
+SELECT a.id, b.x, grouping (a.id), count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y
+GROUP BY GROUPING SETS ((a.id), (b.x), ())
+ORDER BY 1, 2, 3, 4;
+
+--
+-- Subqueries, views, and derived tables
+--
+
+-- pulled-up simple subquery
+EXPLAIN (COSTS OFF)
+SELECT x.id, b.y FROM (SELECT id, val FROM orexp_a) x, orexp_b b
+WHERE x.id = b.x OR x.val = b.y
+ORDER BY 1, 2;
+
+SELECT x.id, b.y FROM (SELECT id, val FROM orexp_a) x, orexp_b b
+WHERE x.id = b.x OR x.val = b.y
+ORDER BY 1, 2;
+
+-- pulled-up subquery containing outer join
+CREATE TABLE orexp_c (id int, val int);
+CREATE TABLE orexp_d (id int, val int);
+INSERT INTO orexp_c VALUES (1, 10), (2, 20), (3, NULL);
+INSERT INTO orexp_d VALUES (1, 100), (3, 300), (5, 500);
+
+EXPLAIN (COSTS OFF)
+SELECT x.cid, b.y FROM (SELECT c.id AS cid, c.val AS cval, d.val + 1 AS dval
+                        FROM orexp_c c LEFT JOIN orexp_d d ON c.id = d.id) x,
+                      orexp_b b
+WHERE x.dval = b.y OR x.cid = b.x
+ORDER BY 1, 2;
+
+SELECT x.cid, b.y FROM (SELECT c.id AS cid, c.val AS cval, d.val + 1 AS dval
+                        FROM orexp_c c LEFT JOIN orexp_d d ON c.id = d.id) x,
+                      orexp_b b
+WHERE x.dval = b.y OR x.cid = b.x
+ORDER BY 1, 2;
+
+-- view expanded into pulled-up subquery
+CREATE VIEW orexp_v AS
+  SELECT a.id AS aid, a.val AS aval, b.x AS bx FROM orexp_a a, orexp_b b;
+
+EXPLAIN (COSTS OFF)
+SELECT v.aid, b.y FROM orexp_v v, orexp_b b
+WHERE v.aval = b.y OR v.bx = b.y
+ORDER BY 1, 2;
+
+SELECT v.aid, b.y FROM orexp_v v, orexp_b b
+WHERE v.aval = b.y OR v.bx = b.y
+ORDER BY 1, 2;
+
+-- non-table RTE_RESULT derived table
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b, (SELECT 1 AS one) x
+WHERE a.id = b.x OR a.id = b.y
+ORDER BY 1, 2;
+
+--
+-- Non-table range table entries
+--
+
+-- VALUES RTE with nulls
+EXPLAIN (COSTS OFF)
+SELECT v.a, b.y FROM (VALUES (1, 99), (2, NULL), (3, 3), (NULL, 4)) v (a, b), orexp_b b
+WHERE v.a = b.x OR v.b = b.y
+ORDER BY 1, 2;
+
+SELECT v.a, b.y FROM (VALUES (1, 99), (2, NULL), (3, 3), (NULL, 4)) v (a, b), orexp_b b
+WHERE v.a = b.x OR v.b = b.y
+ORDER BY 1, 2;
+
+-- set-returning function RTEs
+EXPLAIN (COSTS OFF)
+SELECT g, b.y FROM generate_series(1, 4) g, orexp_b b
+WHERE g = b.x OR g = b.y
+ORDER BY 1, 2;
+
+SELECT g, b.y FROM generate_series(1, 4) g, orexp_b b
+WHERE g = b.x OR g = b.y
+ORDER BY 1, 2;
+
+-- table functions
+EXPLAIN (COSTS OFF)
+SELECT jt.a, b.y FROM json_table('{"a": 1}'::jsonb, '$' COLUMNS (a int PATH '$.a')) jt, orexp_b b
+WHERE jt.a = b.x OR jt.a = b.y;
+
+SELECT jt.a, b.y FROM json_table('{"a": 1}'::jsonb, '$' COLUMNS (a int PATH '$.a')) jt, orexp_b b
+WHERE jt.a = b.x OR jt.a = b.y;
+
+--
+-- Pseudoconstant predicates
+--
+
+-- arm with pseudoconstant qual
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR (b.y = 2 AND now() > '2000-01-01 00:00:00'::timestamp);
+
+SELECT count(*) FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR (b.y = 2 AND now() > '2000-01-01 00:00:00'::timestamp);
+
+-- pseudoconstant arm alongside subquery arm
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE (b.y = 2 AND now() > '2000-01-01 00:00:00'::timestamp)
+   OR (a.id = b.x AND a.val > (SELECT count(*) FROM orexp_b));
+
+SELECT count(*) FROM orexp_a a, orexp_b b
+WHERE (b.y = 2 AND now() > '2000-01-01 00:00:00'::timestamp)
+   OR (a.id = b.x AND a.val > (SELECT count(*) FROM orexp_b));
+
+--
+-- Outer joins
+--
+
+-- FULL JOIN with strict WHERE quals
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a FULL JOIN orexp_b b ON a.id = b.x
+WHERE a.id = b.y OR b.x = a.id
+ORDER BY 1, 2;
+
+SELECT a.id, b.y FROM orexp_a a FULL JOIN orexp_b b ON a.id = b.x
+WHERE a.id = b.y OR b.x = a.id
+ORDER BY 1, 2;
+
+-- LEFT JOIN with cross-relation disjunction in WHERE
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.x, b.y FROM orexp_a a LEFT JOIN orexp_b b ON (a.id = b.x OR a.val = b.y)
+WHERE b.val = 100 OR a.id = 1
+ORDER BY 1, 2, 3;
+
+SELECT a.id, b.x, b.y FROM orexp_a a LEFT JOIN orexp_b b ON (a.id = b.x OR a.val = b.y)
+WHERE b.val = 100 OR a.id = 1
+ORDER BY 1, 2, 3;
+
+--
+-- Queries ineligible for OR expansion
+--
+
+-- volatile set-returning function
+EXPLAIN (COSTS OFF)
+SELECT g, b.y FROM generate_series(1, (random() * 3)::int) g, orexp_b b
+WHERE g = b.x OR g = b.y;
+
+-- volatile VALUES list
+EXPLAIN (COSTS OFF)
+SELECT v.a, b.y FROM (VALUES ((random() * 3)::int)) v (a), orexp_b b
+WHERE v.a = b.x OR v.a = b.y;
+
+-- volatile JSON_TABLE expressions
+EXPLAIN (COSTS OFF)
+SELECT jt.a, b.y FROM json_table(('{"a":' || (random() * 2)::int || '}')::jsonb,
+                                 '$' COLUMNS (a int PATH '$.a')) jt, orexp_b b
+WHERE jt.a = b.x OR jt.a = b.y;
+
+-- volatile function in WHERE
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR (a.id = b.y AND random() < 0.5);
+
+-- correlated SubPlan in WHERE
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE (a.id = b.x OR a.id = b.y)
+  AND a.val > (SELECT max(b2.val) FROM orexp_b b2 WHERE b2.x = a.id);
+
+-- TABLESAMPLE
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b TABLESAMPLE SYSTEM (50)
+WHERE a.id = b.x OR a.id = b.y;
+
+-- unreduced FULL JOIN
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a FULL JOIN orexp_b b ON a.id = b.x
+WHERE a.val IS NULL OR b.val IS NULL;
+
+-- lateral reference
+EXPLAIN (COSTS OFF)
+SELECT a.id, l.ly FROM orexp_a a,
+       LATERAL (SELECT b.y AS ly FROM orexp_b b WHERE b.x = a.id OR b.y = a.val) l
+WHERE a.id = l.ly OR a.val = 1;
+
+-- unflattened subquery with grouping
+EXPLAIN (COSTS OFF)
+SELECT a.id, s.n FROM orexp_a a,
+       (SELECT x AS sx, count(*) AS n FROM orexp_b GROUP BY x) s
+WHERE a.id = s.sx OR a.val = s.n;
+
+-- append relation from UNION ALL
+EXPLAIN (COSTS OFF)
+SELECT u.id, a.val FROM (SELECT x AS id, val FROM orexp_b
+                         UNION ALL
+                         SELECT id, val FROM orexp_a) u, orexp_a a
+WHERE u.val = a.val OR u.id = a.id;
+
+-- partitioned table
+CREATE TABLE orexp_part (id int, d int) PARTITION BY RANGE (d);
+CREATE TABLE orexp_part1 PARTITION OF orexp_part FOR VALUES FROM (0) TO (10);
+CREATE TABLE orexp_part2 PARTITION OF orexp_part FOR VALUES FROM (10) TO (20);
+INSERT INTO orexp_part VALUES (1, 5), (2, 15);
+
+EXPLAIN (COSTS OFF)
+SELECT a.id, p.d FROM orexp_a a, orexp_part p
+WHERE a.id = p.id OR a.val = p.d;
+
+-- table inheritance
+CREATE TABLE orexp_inh_parent (id int, val int);
+CREATE TABLE orexp_inh_child () INHERITS (orexp_inh_parent);
+INSERT INTO orexp_inh_parent VALUES (1, 10);
+INSERT INTO orexp_inh_child VALUES (2, 20);
+
+EXPLAIN (COSTS OFF)
+SELECT a.id, p.val FROM orexp_a a, orexp_inh_parent p
+WHERE a.id = p.id OR a.val = p.val;
+
+--
+-- Permitted sublinks
+--
+
+-- uncorrelated scalar sublink
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE (a.id = b.x OR a.id = b.y)
+  AND a.val > (SELECT min(val) FROM orexp_b WHERE val > 150)
+ORDER BY 1, 2;
+
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE (a.id = b.x OR a.id = b.y)
+  AND a.val > (SELECT min(val) FROM orexp_b WHERE val > 150)
+ORDER BY 1, 2;
+
+--
+-- Multiple OR clauses in WHERE
+--
+
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.val FROM orexp_a a, orexp_b b
+WHERE (a.id = b.x OR a.id = b.y) AND (a.val = b.val OR a.val = b.x)
+ORDER BY 1, 2;
+
+SELECT a.id, b.val FROM orexp_a a, orexp_b b
+WHERE (a.id = b.x OR a.id = b.y) AND (a.val = b.val OR a.val = b.x)
+ORDER BY 1, 2;
+
+--
+-- Control via or_expansion_limit
+--
+
+-- arm count exceeds limit
+SET or_expansion_limit = 2;
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y OR a.val = b.val
+ORDER BY 1, 2;
+
+-- arm count within limit
+SET or_expansion_limit = 3;
+EXPLAIN (COSTS OFF)
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y OR a.val = b.val
+ORDER BY 1, 2;
+
+SELECT a.id, b.y FROM orexp_a a, orexp_b b
+WHERE a.id = b.x OR a.id = b.y OR a.val = b.val
+ORDER BY 1, 2;
+
+--
+-- Clean up
+--
+RESET or_expansion_limit;
+DROP VIEW orexp_v;
+DROP TABLE orexp_c, orexp_d;
+DROP TABLE orexp_part;
+DROP TABLE orexp_inh_parent CASCADE;
+DROP TABLE orexp_a, orexp_b;
-- 
2.34.1

