diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 5d515842dc..cb397c6012 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -636,6 +636,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 		root->wt_param_id = -1;
 	root->non_recursive_path = NULL;
 	root->partColsUpdated = false;
+	root->actual_var_ranges = NIL;
 
 	/*
 	 * If there is a WITH list, process each WITH query and either convert it
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 9ff2b595a8..9672934534 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -936,6 +936,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	subroot->hasRecursion = false;
 	subroot->wt_param_id = -1;
 	subroot->non_recursive_path = NULL;
+	subroot->actual_var_ranges = NIL;
 
 	/* No CTEs to worry about */
 	Assert(subquery->cteList == NIL);
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index e96bfce4dc..a6e73d0b8f 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -5585,9 +5585,12 @@ get_actual_variable_range(PlannerInfo *root, VariableStatData *vardata,
 						  Datum *min, Datum *max)
 {
 	bool		have_data = false;
+	bool		have_cache = false;
+	ActualVarRangeInfo *cache = NULL;
 	RelOptInfo *rel = vardata->rel;
 	RangeTblEntry *rte;
 	ListCell   *lc;
+	PlannerInfo *proot;
 
 	/* No hope if no relation or it doesn't have indexes */
 	if (rel == NULL || rel->indexlist == NIL)
@@ -5648,7 +5651,37 @@ get_actual_variable_range(PlannerInfo *root, VariableStatData *vardata,
 		}
 
 		/*
-		 * Found a suitable index to extract data from.  Set up some data that
+		 * Found a suitable index to extract data from.
+		 * Do we have the extremal values on PlannerInfo cache?
+		 *
+		 * If this is a subquery, traverse to the top-level PlannerInfo
+		 * to read/store ActualVarRangeInfos.
+		 */
+		proot = root;
+		while (proot->parent_root != NULL)
+			proot = proot->parent_root;
+
+		foreach(lc, proot->actual_var_ranges)
+		{
+			cache = (ActualVarRangeInfo *) lfirst(lc);
+
+			if (cache->indexoid != index->indexoid)
+				continue;
+			have_cache = true;
+
+			if (min && cache->min)
+				*min = datumCopy(cache->min, cache->typByVal, cache->typLen);
+			if (max && cache->max)
+				*max = datumCopy(cache->max, cache->typByVal, cache->typLen);
+
+			if ((min && !cache->min) || (max && !cache->max))
+				break;
+
+			return have_cache;
+		}
+
+		/*
+		 * Nope, read actual range from index. Set up some data that
 		 * can be used by both invocations of get_actual_variable_endpoint.
 		 */
 		{
@@ -5689,7 +5722,7 @@ get_actual_variable_range(PlannerInfo *root, VariableStatData *vardata,
 								   (Datum) 0);	/* constant */
 
 			/* If min is requested ... */
-			if (min)
+			if (min && (!have_cache || !cache->min))
 			{
 				have_data = get_actual_variable_endpoint(heapRel,
 														 indexRel,
@@ -5708,7 +5741,7 @@ get_actual_variable_range(PlannerInfo *root, VariableStatData *vardata,
 			}
 
 			/* If max is requested, and we didn't find the index is empty */
-			if (max && have_data)
+			if (max && have_data && (!have_cache || !cache->max))
 			{
 				/* scan in the opposite direction; all else is the same */
 				have_data = get_actual_variable_endpoint(heapRel,
@@ -5731,6 +5764,36 @@ get_actual_variable_range(PlannerInfo *root, VariableStatData *vardata,
 			MemoryContextSwitchTo(oldcontext);
 			MemoryContextDelete(tmpcontext);
 
+			/*
+			 * Store variable range in PlannerInfo cache for subsequent calls.
+			 *
+			 * If there's already a ActualVarRangeInfo for this index, just
+			 * fill the missing information.
+			 */
+			if (have_data && !have_cache)
+			{
+				oldcontext = MemoryContextSwitchTo(proot->planner_cxt);
+				cache = (ActualVarRangeInfo *) palloc(sizeof(ActualVarRangeInfo));
+
+				cache->indexoid = index->indexoid;
+				cache->min = PointerGetDatum(NULL);
+				cache->max = PointerGetDatum(NULL);
+				cache->typByVal = typByVal;
+				cache->typLen = typLen;
+
+				proot->actual_var_ranges = lappend(proot->actual_var_ranges,
+												   cache);
+				MemoryContextSwitchTo(oldcontext);
+			}
+
+			if (have_data)
+			{
+				if (min && !cache->min)
+					cache->min = datumCopy(*min, typByVal, typLen);
+				if (max && !cache->max)
+					cache->max = datumCopy(*max, typByVal, typLen);
+			}
+
 			/* And we're done */
 			break;
 		}
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 69150e46eb..d92f169b76 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -362,6 +362,9 @@ struct PlannerInfo
 
 	/* Does this query modify any partition key columns? */
 	bool		partColsUpdated;
+
+	/* List of ActualVarRangeInfos, a cache for get_actual_variable_range */
+	List	   *actual_var_ranges;
 };
 
 
@@ -2333,6 +2336,24 @@ typedef struct MinMaxAggInfo
 	Param	   *param;			/* param for subplan's output */
 } MinMaxAggInfo;
 
+
+/*
+ * Store both extremal values of an index.
+ *
+ * The planner stores ActualVarRangeInfos in PlannerInfo to avoid repeated
+ * executions of get_actual_variable_range that can be very expensive,
+ * particularly if the index is bloated on any of the extremes.
+ */
+typedef struct ActualVarRangeInfo
+{
+	Datum	min;				/* index minimal value */
+	Datum	max;				/* index maximal value */
+	Oid		indexoid;
+	int16	typLen;
+	bool	typByVal;
+} ActualVarRangeInfo;
+
+
 /*
  * At runtime, PARAM_EXEC slots are used to pass values around from one plan
  * node to another.  They can be used to pass values down into subqueries (for
