From ff8630a06eee0fc39dc03dc7f8eed0ea271f29ff Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Wed, 10 May 2017 13:29:09 +0300
Subject: [PATCH 3/3] Add a Param -> result cache to SubPlan nodes.

---
 src/backend/executor/nodeSubplan.c     | 466 ++++++++++++++++++++++++++++++++-
 src/backend/optimizer/plan/subselect.c |  86 +++++-
 src/include/nodes/execnodes.h          |  25 +-
 src/include/nodes/primnodes.h          |   5 +
 4 files changed, 568 insertions(+), 14 deletions(-)

diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 909f77905c..287010dee2 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -17,6 +17,17 @@
  * called, and the subquery's results are set as the current values of
  * executor Params, as indicated by SubPlan->setParam.
  *
+ * For correlated subplans, which need to be re-evaluated every time the
+ * outer parameter values change, we optionally keep a cache of the results.
+ * If the subplan is called again with the same set of parameters, we can
+ * avoid re-executing the subquery, and return the result from the cache
+ * instead.  This helps a lot if the subplan is expensive, and the same
+ * parameters are used many times.
+ *
+ * TODO: The cache is currently only used in ExecSubPlan, not
+ * ExecSetParamPlan.
+ *
+ *
  * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
@@ -38,10 +49,14 @@
 #include "access/htup_details.h"
 #include "executor/executor.h"
 #include "executor/nodeSubplan.h"
+#include "lib/ilist.h"
 #include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
 #include "miscadmin.h"
 #include "optimizer/clauses.h"
 #include "utils/array.h"
+#include "utils/datum.h"
+#include "utils/hashutils.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 
@@ -58,6 +73,60 @@ static bool findPartialMatch(TupleHashTable hashtable, TupleTableSlot *slot,
 static bool slotAllNulls(TupleTableSlot *slot);
 static bool slotNoNulls(TupleTableSlot *slot);
 
+/*
+ * Subplan result cache.
+ *
+ * If the subplan doesn't contain volatile expressions, we can avoid
+ * executing it repeatedly for the same parameters, by caching the results.
+ *
+ * The cache is a hash table.  The cache key consists of the parameter values.
+ * The number of parameters depends on the subquery, so we keep them at the
+ * end of the struct.
+ *
+ * The cached result is the final result of ExecSubPlan, after evaluating any
+ * 'testexpr' or other sublink-specific processing.  For ARRAY type sublink,
+ * it is the fully formed array, for EXISTS and ROWCOMPARE, it is a boolean.
+ *
+ * The cache is of fixed size.  If it fills up, we throw away old entries,
+ * using a simple LRU policy.
+ */
+typedef struct
+{
+	Datum		result;			/* result of the SubPlan with this param */
+	bool		resultNull;
+
+	dlist_node	lru_link;		/* link in the LRU list. */
+
+	/*
+	 * SubPlan parameters.  These form the cache key.
+	 *
+	 * After the 'params', there is a corresponding array of 'isnull' flags.
+	 * Use SUBPLAN_RESULT_CACHE_ENTRY_GET_NULLS to access it.
+	 */
+	Datum		params[FLEXIBLE_ARRAY_MEMBER];
+
+	/* bool		paramNulls[FLEXIBLE_ARRAY_MEMBER]; */
+
+} SubPlanResultCacheEntry;
+
+/* Macros to work with SubPlanResultCacheEntry */
+#define SUBPLAN_RESULT_CACHE_KEY_SIZE(numparams) \
+	((numparams) * sizeof(Datum) + (numparams) * sizeof(bool))
+#define SUBPLAN_RESULT_CACHE_ENTRY_SIZE(numparams) \
+	(offsetof(SubPlanResultCacheEntry, params) + \
+	 SUBPLAN_RESULT_CACHE_KEY_SIZE(numparams))
+
+#define SUBPLAN_RESULT_CACHE_ENTRY_GET_NULLS(entry, numparams) \
+	((bool *) &(entry)->params[numparams])
+
+static void init_result_cache(SubPlanState *sstate);
+static SubPlanResultCacheEntry * lookup_result_cache(SubPlanState *sstate,
+					SubPlanResultCacheEntry *key, bool *found);
+static uint32 result_cache_hash(const void *key, Size keysize);
+static int result_cache_match(const void *key1, const void *key2, Size keysize);
+static void *result_cache_keycopy(void *dest, const void *src, Size keysize);
+
+static SubPlanState *curr_sstate;
 
 /* ----------------------------------------------------------------
  *		ExecSubPlan
@@ -230,6 +299,10 @@ ExecScanSubPlan(SubPlanState *node,
 	ListCell   *pvar;
 	ListCell   *l;
 	ArrayBuildStateAny *astate = NULL;
+	SubPlanResultCacheEntry *resultCacheEntry = NULL;
+	int			i;
+	Datum	   *currParams;
+	bool	   *currParamNulls;
 
 	/*
 	 * MULTIEXPR subplans, when "executed", just return NULL; but first we
@@ -263,6 +336,12 @@ ExecScanSubPlan(SubPlanState *node,
 									CurrentMemoryContext, true);
 
 	/*
+	 * If first time through, initialize the result cache.
+	 */
+	if (subplan->useResultCache && node->resultcachetab == NULL)
+		init_result_cache(node);
+
+	/*
 	 * We are probably in a short-lived expression-evaluation context. Switch
 	 * to the per-query context for manipulating the child plan's chgParam,
 	 * calling ExecProcNode on it, etc.
@@ -276,6 +355,9 @@ ExecScanSubPlan(SubPlanState *node,
 	 */
 	Assert(list_length(subplan->parParam) == list_length(node->args));
 
+	i = 0;
+	currParams = ((SubPlanResultCacheEntry *) node->currCacheKey)->params;
+	currParamNulls = SUBPLAN_RESULT_CACHE_ENTRY_GET_NULLS((SubPlanResultCacheEntry *) node->currCacheKey, node->numparams);
 	forboth(l, subplan->parParam, pvar, node->args)
 	{
 		int			paramid = lfirst_int(l);
@@ -285,6 +367,51 @@ ExecScanSubPlan(SubPlanState *node,
 											   econtext,
 											   &(prm->isnull));
 		planstate->chgParam = bms_add_member(planstate->chgParam, paramid);
+
+		if (subplan->useResultCache)
+		{
+			currParams[i] = prm->value;
+			currParamNulls[i] = prm->isnull;
+		}
+
+		i++;
+	}
+
+	/*
+	 * If we're caching the subplan results, check the cache first.
+	 *
+	 * On cache miss, this creates a new entry for this set of parameters.
+	 * We'll fill it with the subplan's result later.
+	 */
+	if (subplan->useResultCache)
+	{
+		bool		found;
+
+		resultCacheEntry = lookup_result_cache(node, node->currCacheKey,
+											   &found);
+		if (found)
+		{
+			/*
+			 * Cache hit. If the result datatype is pass-by-ref, make a copy
+			 * of it in a short-lived memory context.
+			 */
+			if (resultCacheEntry->resultNull)
+			{
+				result = (Datum) 0;
+				*isNull = true;
+			}
+			else
+			{
+				result = datumCopy(resultCacheEntry->result,
+								   node->resultByVal,
+								   node->resultLen);
+				*isNull = false;
+			}
+
+			return result;
+		}
+
+		/* Not found. Fall through to execute the subplan. */
 	}
 
 	/*
@@ -456,6 +583,31 @@ ExecScanSubPlan(SubPlanState *node,
 		}
 	}
 
+	/*
+	 * If we're using the result cache, copy the result of the subplan into
+	 * the cache entry.
+	 */
+	if (resultCacheEntry)
+	{
+		if (*isNull)
+		{
+			resultCacheEntry->result = (Datum) 0;
+			resultCacheEntry->resultNull = true;
+		}
+		else
+		{
+			MemoryContext oldcxt = MemoryContextSwitchTo(node->hashtablecxt);
+
+			resultCacheEntry->result = datumCopy(result,
+												 node->resultByVal,
+												 node->resultLen);
+			resultCacheEntry->resultNull = false;
+			MemoryContextSwitchTo(oldcxt);
+			if (!node->resultByVal)
+				node->memUsed += GetMemoryChunkSpace(DatumGetPointer(resultCacheEntry->result));
+		}
+	}
+
 	return result;
 }
 
@@ -831,6 +983,24 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent)
 	}
 
 	/*
+	 * Initialize memory contexts used by the initplan hash table or the
+	 * result cache.
+	 */
+	if (subplan->useHashTable || subplan->useResultCache)
+	{
+		/* We need a memory context to hold the hash table(s) */
+		sstate->hashtablecxt =
+			AllocSetContextCreate(CurrentMemoryContext,
+								  "Subplan HashTable Context",
+								  ALLOCSET_DEFAULT_SIZES);
+		/* and a small one for the hash tables to use as temp storage */
+		sstate->hashtempcxt =
+			AllocSetContextCreate(CurrentMemoryContext,
+								  "Subplan HashTable Temp Context",
+								  ALLOCSET_SMALL_SIZES);
+	}
+
+	/*
 	 * If we are going to hash the subquery output, initialize relevant stuff.
 	 * (We don't create the hashtable until needed, though.)
 	 */
@@ -846,18 +1016,9 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent)
 				   *righttlist;
 		ListCell   *l;
 
-		/* We need a memory context to hold the hash table(s) */
-		sstate->hashtablecxt =
-			AllocSetContextCreate(CurrentMemoryContext,
-								  "Subplan HashTable Context",
-								  ALLOCSET_DEFAULT_SIZES);
-		/* and a small one for the hash tables to use as temp storage */
-		sstate->hashtempcxt =
-			AllocSetContextCreate(CurrentMemoryContext,
-								  "Subplan HashTable Temp Context",
-								  ALLOCSET_SMALL_SIZES);
-		/* and a short-lived exprcontext for function evaluation */
+		/* a short-lived exprcontext for function evaluation */
 		sstate->innerecontext = CreateExprContext(estate);
+
 		/* Silly little array of column numbers 1..n */
 		ncols = list_length(subplan->paramIds);
 		sstate->keyColIdx = (AttrNumber *) palloc(ncols * sizeof(AttrNumber));
@@ -980,6 +1141,30 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent)
 
 	}
 
+	/*
+	 * If we are going to use the result cache, which is keyed by the
+	 * parameters, initialize the equality and hash functions for the
+	 * parameter datatypes. (We don't create the cache itself until needed,
+	 * though.)
+	 */
+	if (subplan->useResultCache)
+	{
+		int			i;
+
+		Assert (!subplan->useHashTable);
+
+		sstate->numparams = list_length(subplan->args);
+		sstate->param_hash_funcs =
+			(FmgrInfo *) palloc(sstate->numparams * sizeof(FmgrInfo));
+		sstate->param_eq_funcs =
+			(FmgrInfo *) palloc(sstate->numparams * sizeof(FmgrInfo));
+		for (i = 0; i < sstate->numparams; i++)
+		{
+			fmgr_info(subplan->paramEqFuncs[i], &sstate->param_eq_funcs[i]);
+			fmgr_info(subplan->paramHashFuncs[i], &sstate->param_hash_funcs[i]);
+		}
+	}
+
 	return sstate;
 }
 
@@ -1291,3 +1476,262 @@ ExecAlternativeSubPlan(AlternativeSubPlanState *node,
 
 	return ExecSubPlan(activesp, econtext, isNull);
 }
+
+
+
+/*-------------------
+ * SubPlan result cache
+ *-------------------
+ */
+
+static void
+init_result_cache(SubPlanState *sstate)
+{
+	HASHCTL		hash_ctl;
+	Size		entrysize = SUBPLAN_RESULT_CACHE_ENTRY_SIZE(sstate->numparams);
+	long		nbuckets;
+	int			i;
+	ListCell   *l;
+	MemoryContext oldcxt;
+
+	nbuckets = 100; /* TODO? */
+
+	/* Limit initial table size request to not more than work_mem */
+	nbuckets = Min(nbuckets, (long) ((work_mem * 1024L) / entrysize));
+
+	/* TODO: should we use a hash_iv like execGrouping.c does? */
+
+	oldcxt = MemoryContextSwitchTo(sstate->hashtablecxt);
+
+	memset(&hash_ctl, 0, sizeof(hash_ctl));
+	hash_ctl.keysize = SUBPLAN_RESULT_CACHE_KEY_SIZE(sstate->numparams);
+	hash_ctl.entrysize = SUBPLAN_RESULT_CACHE_ENTRY_SIZE(sstate->numparams);
+	hash_ctl.hash = result_cache_hash;
+	hash_ctl.match = result_cache_match;
+	hash_ctl.keycopy = result_cache_keycopy;
+	hash_ctl.hcxt = sstate->hashtablecxt;
+
+	sstate->resultcachetab = hash_create("subplan result cache",
+										 100 /* XXX */,
+										 &hash_ctl,
+										 HASH_ELEM | HASH_FUNCTION | HASH_COMPARE | HASH_KEYCOPY | HASH_CONTEXT);
+
+	sstate->paramLens = palloc(sstate->numparams * sizeof(int16));
+	sstate->paramByVals = palloc(sstate->numparams * sizeof(bool));
+
+	sstate->memUsed = 0;
+	sstate->memLimit = work_mem * 1024L;
+
+	i = 0;
+	foreach(l, sstate->subplan->args)
+	{
+		Expr	   *arg = (Expr *) lfirst(l);
+
+		get_typlenbyval(exprType((Node *) arg),
+						&sstate->paramLens[i],
+						&sstate->paramByVals[i]);
+		i++;
+	}
+
+	/*
+	 * Also initialize this temporary cache key, to hold the parameter
+	 * values, while we look up in the cache.
+	 */
+	sstate->currCacheKey = (void *) palloc0(SUBPLAN_RESULT_CACHE_ENTRY_SIZE(sstate->numparams));
+
+	/*
+	 * For all sublink types except EXPR_SUBLINK and ARRAY_SUBLINK, the result is
+	 * a boolean as are the results fo the combining operators.  The cache will store
+	 * the final result, after evaluating any combining operators.
+	 */
+	if (sstate->subplan->subLinkType == EXPR_SUBLINK)
+	{
+		get_typlenbyval(sstate->subplan->firstColType,
+						&sstate->resultLen,
+						&sstate->resultByVal);
+	}
+	else if (sstate->subplan->subLinkType == ARRAY_SUBLINK)
+	{
+		/* array type. They're all varlenas */
+		sstate->resultLen = -1;
+		sstate->resultByVal = false;
+	}
+	else
+	{
+		/* boolean */
+		sstate->resultLen = 1;
+		sstate->resultByVal = true;
+	}
+
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * Insert or return existing entry in the cache.
+ */
+static SubPlanResultCacheEntry *
+lookup_result_cache(SubPlanState *sstate, SubPlanResultCacheEntry *key, bool *found)
+{
+	MemoryContext oldContext;
+	SubPlanResultCacheEntry *newentry;
+	int			i;
+	SubPlanState *prev_sstate;
+	Datum	   *params = key->params;
+	bool	   *isnulls = SUBPLAN_RESULT_CACHE_ENTRY_GET_NULLS(key, sstate->numparams);
+
+	/* Need to run the hash functions in short-lived context */
+	oldContext = MemoryContextSwitchTo(sstate->hashtempcxt);
+
+	/*
+	 * To keep memory use in check, if there are too many entries in the
+	 * cache, remove the oldest entry to make space.
+	 */
+	while (sstate->memUsed > sstate->memLimit)
+	{
+		SubPlanResultCacheEntry *oldentry;
+		bool		foundold;
+		Datum	   *oldparams;
+		bool	   *oldparamnulls;
+
+		/* Remove the oldest entry */
+		oldentry = dlist_tail_element(SubPlanResultCacheEntry, lru_link, &sstate->lru_list);
+
+		dlist_delete(&oldentry->lru_link);
+
+		if (!sstate->resultByVal && !oldentry->resultNull)
+		{
+			sstate->memUsed -= GetMemoryChunkSpace(DatumGetPointer(oldentry->result));
+			pfree(DatumGetPointer(oldentry->result));
+		}
+
+		/* Remove the entry from the hash table. */
+		prev_sstate = curr_sstate;
+		curr_sstate = sstate;
+		hash_search(sstate->resultcachetab, oldentry, HASH_REMOVE, &foundold);
+		if (!foundold)
+			elog(ERROR, "subplan cache is corrupt");
+		curr_sstate = prev_sstate;
+
+		sstate->memUsed -= SUBPLAN_RESULT_CACHE_ENTRY_SIZE(sstate->numparams);
+
+		/*
+		 * XXX we assume the key is still valid, even though we've removed it from
+		 * the hash table.
+		 */
+		oldparams = oldentry->params;
+		oldparamnulls = SUBPLAN_RESULT_CACHE_ENTRY_GET_NULLS(oldentry, sstate->numparams);
+		for (i = 0; i < sstate->numparams; i++)
+		{
+			if (!sstate->paramByVals[i] && !oldparamnulls[i])
+			{
+				sstate->memUsed -= GetMemoryChunkSpace(DatumGetPointer(oldparams[i]));
+				pfree(DatumGetPointer(oldparams[i]));
+			}
+		}
+	}
+
+	prev_sstate = curr_sstate;
+	curr_sstate = sstate;
+	newentry = hash_search(sstate->resultcachetab, key, HASH_ENTER, found);
+	curr_sstate = prev_sstate;
+
+	if (!(*found))
+	{
+		/*
+		 * We created a new entry.
+		 */
+		Datum	   *newparams = newentry->params;
+
+		sstate->memUsed += SUBPLAN_RESULT_CACHE_ENTRY_SIZE(sstate->numparams);
+
+		dlist_push_head(&sstate->lru_list, &newentry->lru_link);
+
+		/*
+		 * Need to make a copy of the key in the long-lived memory context.
+		 */
+		MemoryContextSwitchTo(sstate->hashtablecxt);
+
+		for (i = 0; i < sstate->numparams; i++)
+		{
+			if (!sstate->paramByVals[i] && !isnulls[i])
+			{
+				newparams[i] = datumCopy(params[i],
+										 sstate->paramByVals[i],
+										 sstate->paramLens[i]);
+				sstate->memUsed += GetMemoryChunkSpace(DatumGetPointer(newparams[i]));
+			}
+		}
+	}
+	else
+	{
+		dlist_delete(&newentry->lru_link);
+		dlist_push_head(&sstate->lru_list, &newentry->lru_link);
+	}
+
+	MemoryContextSwitchTo(oldContext);
+
+	return newentry;
+}
+
+static uint32
+result_cache_hash(const void *key, Size keysize)
+{
+	const SubPlanResultCacheEntry *entry = (const SubPlanResultCacheEntry *) key;
+	SubPlanState *sstate = curr_sstate;
+	int			i;
+	uint32		hash = 0;
+	const Datum *params = entry->params;
+	const bool  *isnulls = SUBPLAN_RESULT_CACHE_ENTRY_GET_NULLS(entry, sstate->numparams);
+
+	for (i = 0; i < sstate->numparams; i++)
+	{
+		uint32		thishash;
+
+		if (isnulls[i])
+			thishash = 0;
+		else
+			thishash = DatumGetUInt32(FunctionCall1(&sstate->param_hash_funcs[i],
+													params[i]));
+		hash = hash_combine(hash, thishash);
+	}
+
+	return hash;
+}
+
+static int
+result_cache_match(const void *key1, const void *key2, Size keysize)
+{
+	const SubPlanResultCacheEntry *entry1 = (const SubPlanResultCacheEntry *) key1;
+	const SubPlanResultCacheEntry *entry2 = (const SubPlanResultCacheEntry *) key2;
+	SubPlanState *sstate = curr_sstate;
+	const Datum *params1 = entry1->params;
+	const bool *isnulls1 = SUBPLAN_RESULT_CACHE_ENTRY_GET_NULLS(entry1, sstate->numparams);
+	const Datum *params2 = entry2->params;
+	const bool *isnulls2 = SUBPLAN_RESULT_CACHE_ENTRY_GET_NULLS(entry2, sstate->numparams);
+	int			i;
+
+	for (i = 0; i < sstate->numparams; i++)
+	{
+		if (isnulls1[i] != isnulls2[i])
+			return 1;
+		else if (!isnulls1[i])
+		{
+			if (!DatumGetBool(FunctionCall2(&sstate->param_eq_funcs[0],
+											params1[i],
+											params2[i])))
+				return 1;
+		}
+	}
+	return 0;	/* they're equal */
+}
+
+static void *
+result_cache_keycopy(void *dest, const void *src, Size keysize)
+{
+	SubPlanResultCacheEntry *destentry = (SubPlanResultCacheEntry *) dest;
+	const SubPlanResultCacheEntry *srcentry = (const SubPlanResultCacheEntry *) src;
+
+	memcpy(destentry->params, srcentry->params, keysize);
+
+	return destentry;
+}
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index 83008d7661..1fd419be5c 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -33,6 +33,7 @@
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
+#include "utils/typcache.h"
 
 
 typedef struct convert_testexpr_context
@@ -70,6 +71,7 @@ static Node *convert_testexpr_mutator(Node *node,
 						 convert_testexpr_context *context);
 static bool subplan_is_hashable(Plan *plan);
 static bool testexpr_is_hashable(Node *testexpr);
+static bool params_are_hashable(SubPlan *splan);
 static bool hash_ok_operator(OpExpr *expr);
 static bool simplify_EXISTS_query(PlannerInfo *root, Query *query);
 static Query *convert_EXISTS_to_ANY(PlannerInfo *root, Query *subselect,
@@ -844,7 +846,6 @@ build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
 			subplan_is_hashable(plan) &&
 			testexpr_is_hashable(splan->testexpr))
 			splan->useHashTable = true;
-
 		/*
 		 * Otherwise, we have the option to tack a Material node onto the top
 		 * of the subplan, to reduce the cost of reading it repeatedly.  This
@@ -859,6 +860,44 @@ build_subplan(PlannerInfo *root, Plan *plan, PlannerInfo *subroot,
 				 !ExecMaterializesOutput(nodeTag(plan)))
 			plan = materialize_finished_plan(plan);
 
+		/*
+		 * If it's a correlated subplan, we can't use an initPlan, but we
+		 * might be able to cache the subplan's result for each combination of
+		 * correlation params.
+		 *
+		 * If the subplan or testexpr contains volatile expressions, caching
+		 * is not possible. Also, if the 'testexpr' is contains any Vars from
+		 * the outer query, no caching, because the result would depend not
+		 * only on the subquery, but also on the Vars. (We could still use
+		 * the cache if we used the outer Vars as part of the cache key, but
+		 * we don't do that.)
+		 *
+		 * Also, the parameters have to be hashable, so that we can use them
+		 * as the key for the hash table to implement the cache.
+		 */
+		if (splan->parParam &&
+			!contain_volatile_functions((Node *) subroot->parse) &&
+			!contain_volatile_functions((Node *) splan->testexpr) &&
+			!contain_var_clause((Node *) splan->testexpr) &&
+			params_are_hashable(splan))
+		{
+			Cost		lookup_cost;
+			int			i;
+
+			Assert(!splan->useHashTable);
+
+			lookup_cost = 0;
+			for (i = 0; i < list_length(splan->args); i++)
+			{
+				lookup_cost += get_func_cost(splan->paramHashFuncs[i]);
+				lookup_cost += get_func_cost(splan->paramEqFuncs[i]);
+			}
+
+			/* Assume a 10% hit ratio. */
+			if (plan->total_cost > lookup_cost + 0.9 * plan->total_cost)
+				splan->useResultCache = true;
+		}
+
 		result = (Node *) splan;
 		isInitPlan = false;
 	}
@@ -1145,6 +1184,51 @@ hash_ok_operator(OpExpr *expr)
 	}
 }
 
+/*
+ * Check if all params are hashable
+ */
+static bool
+params_are_hashable(SubPlan *splan)
+{
+	ListCell   *lc;
+	Oid		   *hash_funcs;
+	Oid		   *eq_funcs;
+	int			i;
+
+	hash_funcs = (Oid *) palloc(list_length(splan->args) * sizeof(Oid));
+	eq_funcs = (Oid *) palloc(list_length(splan->args) * sizeof(Oid));
+
+	i = 0;
+	foreach(lc, splan->args)
+	{
+		Node	   *expr = lfirst(lc);
+		Oid			eq_opr;
+		Oid			hash_proc;
+		TypeCacheEntry *typentry;
+
+		typentry = lookup_type_cache(exprType(expr),
+									 TYPECACHE_EQ_OPR | TYPECACHE_HASH_PROC);
+		eq_opr = typentry->eq_opr;
+		hash_proc = typentry->hash_proc;
+		if (!OidIsValid(eq_opr) || !OidIsValid(hash_proc))
+		{
+			pfree(hash_funcs);
+			pfree(eq_funcs);
+			return false;
+		}
+
+		hash_funcs[i] = hash_proc;
+		eq_funcs[i] = get_opcode(eq_opr);
+
+		i++;
+	}
+
+	/* Note: as a side-effect, we filled in the paramHashFuncs and paramEqFuncs arrays. */
+	splan->paramHashFuncs = hash_funcs;
+	splan->paramEqFuncs = eq_funcs;
+
+	return true;
+}
 
 /*
  * SS_process_ctes: process a query's WITH list
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 1f3c786b30..7aef33b83b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -18,6 +18,7 @@
 #include "access/heapam.h"
 #include "access/tupconvert.h"
 #include "executor/instrument.h"
+#include "lib/ilist.h"
 #include "lib/pairingheap.h"
 #include "nodes/params.h"
 #include "nodes/plannodes.h"
@@ -824,6 +825,10 @@ typedef struct SubPlanState
 	List	   *args;			/* states of argument expression(s) */
 	HeapTuple	curTuple;		/* copy of most recent tuple from subplan */
 	Datum		curArray;		/* most recent array from ARRAY() subplan */
+
+	MemoryContext hashtablecxt; /* memory context containing hash tables */
+	MemoryContext hashtempcxt;	/* temp memory context for hash tables */
+
 	/* these are used when hashing the subselect's output: */
 	TupleDesc	descRight;		/* subselect desc after projection */
 	ProjectionInfo *projLeft;	/* for projecting lefthand exprs */
@@ -832,8 +837,6 @@ typedef struct SubPlanState
 	TupleHashTable hashnulls;	/* hash table for rows with null(s) */
 	bool		havehashrows;	/* true if hashtable is not empty */
 	bool		havenullrows;	/* true if hashnulls is not empty */
-	MemoryContext hashtablecxt; /* memory context containing hash tables */
-	MemoryContext hashtempcxt;	/* temp memory context for hash tables */
 	ExprContext *innerecontext; /* econtext for computing inner tuples */
 	AttrNumber *keyColIdx;		/* control data for hash tables */
 	Oid		   *tab_eq_funcoids;	/* equality func oids for table
@@ -842,6 +845,24 @@ typedef struct SubPlanState
 	FmgrInfo   *lhs_hash_funcs; /* hash functions for lefthand datatype(s) */
 	FmgrInfo   *cur_eq_funcs;	/* equality functions for LHS vs. table */
 	ExprState  *cur_eq_comp;	/* equality comparator for LHS vs. table */
+
+	/* these are for the result cache: */
+	int			numparams;		/* number of input parameters for subplan */
+
+	HTAB	   *resultcachetab;	/* hash table forming the result cache */
+	dlist_head	lru_list;		/* LRU list for cache replacement */
+	size_t		memUsed;		/* amount of memory used by the cache */
+	size_t		memLimit;		/* max cache size, in bytes */
+
+	void	   *currCacheKey;	/* temporary cache key variable */
+
+	bool	   *paramByVals;	/* parameter type information */
+	int16	   *paramLens;
+	FmgrInfo   *param_hash_funcs;	/* hash funcs for param datatype(s) */
+	FmgrInfo   *param_eq_funcs; 	/* equality funcs for param datatype(s) */
+
+	bool		resultByVal;	/* result type information, for the cache */
+	int16		resultLen;
 } SubPlanState;
 
 /* ----------------
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index f90aa7b2a1..7ca89dae21 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -695,12 +695,17 @@ typedef struct SubPlan
 	int32		firstColTypmod; /* Typmod of first column of subplan result */
 	Oid			firstColCollation;	/* Collation of first column of subplan
 									 * result */
+	/* Information about parameter types, for caching subplan's results */
+	Oid		   *paramHashFuncs;	/* hash functions for Params */
+	Oid		   *paramEqFuncs;	/* equality operators to compare Params with */
 	/* Information about execution strategy: */
 	bool		useHashTable;	/* true to store subselect output in a hash
 								 * table (implies we are doing "IN") */
 	bool		unknownEqFalse; /* true if it's okay to return FALSE when the
 								 * spec result is UNKNOWN; this allows much
 								 * simpler handling of null values */
+	bool		useResultCache;	/* true to maintain a cache subselect's output
+								 * for params (implies we are doing "EXPR") */
 	bool		parallel_safe;	/* is the subplan parallel-safe? */
 	/* Note: parallel_safe does not consider contents of testexpr or args */
 	/* Information for passing params into and out of the subselect: */
-- 
2.11.0

