Subplan result caching

Started by Heikki Linnakangasover 7 years ago21 messages
#1Heikki Linnakangas
hlinnaka@iki.fi
3 attachment(s)

Hi,

I've been working on a patch to add a little cache to SubPlans, to speed
up queries with correlated subqueries, where the same subquery is
currently executed multiple times with the same parameters. The idea is
to cache the result of the subplan, with the correlation vars as the
cache key.

That helps a lot, if you happen to have that kind of a query. I bumped
into this while looking at TPC-DS query 6:

select a.ca_state state, count(*) cnt
from customer_address a
,customer c
,store_sales s
,date_dim d
,item i
where a.ca_address_sk = c.c_current_addr_sk
and c.c_customer_sk = s.ss_customer_sk
and s.ss_sold_date_sk = d.d_date_sk
and s.ss_item_sk = i.i_item_sk
and d.d_month_seq =
(select distinct (d_month_seq)
from date_dim
where d_year = 2000
and d_moy = 5 )
and i.i_current_price > 1.2 *
(select avg(j.i_current_price)
from item j
where j.i_category = i.i_category)
group by a.ca_state
having count(*) >= 10
order by cnt

The first subquery is uncorrelated, and is already handled efficiently
as an InitPlan. This patch helps with the second subquery. There are
only 11 different categories, but we currently re-execute it for every
row of the outer query, over 26000 times. (I think I have about 1 GB of
data in my little database I've been testing with, I'm not sure how this
would scale with the amount of data.) With this patch, it's only
executed 11 times, the cache avoids the rest of the executions. That
brings the runtime, on my laptop, from about 30 s to 120 ms.

For this particular query, I actually wish we could pull up that
subquery, instead. I did some investigation into that last summer,
/messages/by-id/67e353e8-c20e-7980-a282-538779edf25b@iki.fi,
but that's a much bigger project. In any case, even if the planner was
able to pull up subqueries in more cases, a cache like this would still
be helpful for those cases where pulling up was still not possible.

Thoughts?

- Heikki

Attachments:

0001-Add-comment-to-explain-that-SubPlan-can-return-its-r.patchtext/x-patch; name=0001-Add-comment-to-explain-that-SubPlan-can-return-its-r.patchDownload
From d3a738ea6427df908b61a5fdd3ee14a7b78b724a Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Tue, 22 May 2018 08:59:33 +0300
Subject: [PATCH 1/3] Add comment to explain that SubPlan can return its
 results in two ways.

It took me a while to understand this, I hope this helps the next person
reading the code to grok it faster.
---
 src/backend/executor/nodeSubplan.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index 44f551bcf1..aab1c7942a 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -10,6 +10,12 @@
  * direct correlation variables from the parent plan level), and "regular"
  * subplans, which are re-evaluated every time their result is required.
  *
+ * There are two ways a SubPlan node can return the result.  The first is
+ * that ExecSubPlan() runs the subquery, and returns its result like any
+ * other expression.  The second way is to return the result in executor
+ * Params.  In this method, the execution happens when ExecSetParamPlan() is
+ * called, and the subquery's results are set as the current values of
+ * executor Params, as indicated by SubPlan->setParam.
  *
  * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
-- 
2.11.0

0002-Remove-some-unused-code.patchtext/x-patch; name=0002-Remove-some-unused-code.patchDownload
From fcf040b589ffc318cbbc060deec296577b3ff24b Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakangas@iki.fi>
Date: Tue, 22 May 2018 09:05:38 +0300
Subject: [PATCH 2/3] Remove some unused code.

---
 src/backend/executor/nodeSubplan.c | 10 ----------
 src/include/nodes/execnodes.h      |  1 -
 2 files changed, 11 deletions(-)

diff --git a/src/backend/executor/nodeSubplan.c b/src/backend/executor/nodeSubplan.c
index aab1c7942a..909f77905c 100644
--- a/src/backend/executor/nodeSubplan.c
+++ b/src/backend/executor/nodeSubplan.c
@@ -802,7 +802,6 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent)
 	sstate->keyColIdx = NULL;
 	sstate->tab_eq_funcoids = NULL;
 	sstate->tab_hash_funcs = NULL;
-	sstate->tab_eq_funcs = NULL;
 	sstate->lhs_hash_funcs = NULL;
 	sstate->cur_eq_funcs = NULL;
 
@@ -900,7 +899,6 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent)
 		lefttlist = righttlist = NIL;
 		sstate->tab_eq_funcoids = (Oid *) palloc(ncols * sizeof(Oid));
 		sstate->tab_hash_funcs = (FmgrInfo *) palloc(ncols * sizeof(FmgrInfo));
-		sstate->tab_eq_funcs = (FmgrInfo *) palloc(ncols * sizeof(FmgrInfo));
 		sstate->lhs_hash_funcs = (FmgrInfo *) palloc(ncols * sizeof(FmgrInfo));
 		sstate->cur_eq_funcs = (FmgrInfo *) palloc(ncols * sizeof(FmgrInfo));
 		i = 1;
@@ -909,7 +907,6 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent)
 			OpExpr	   *opexpr = lfirst_node(OpExpr, l);
 			Expr	   *expr;
 			TargetEntry *tle;
-			Oid			rhs_eq_oper;
 			Oid			left_hashfn;
 			Oid			right_hashfn;
 
@@ -936,13 +933,6 @@ ExecInitSubPlan(SubPlan *subplan, PlanState *parent)
 			fmgr_info(opexpr->opfuncid, &sstate->cur_eq_funcs[i - 1]);
 			fmgr_info_set_expr((Node *) opexpr, &sstate->cur_eq_funcs[i - 1]);
 
-			/* Look up the equality function for the RHS type */
-			if (!get_compatible_hash_operators(opexpr->opno,
-											   NULL, &rhs_eq_oper))
-				elog(ERROR, "could not find compatible hash operator for operator %u",
-					 opexpr->opno);
-			fmgr_info(get_opcode(rhs_eq_oper), &sstate->tab_eq_funcs[i - 1]);
-
 			/* Lookup the associated hash functions */
 			if (!get_op_hash_functions(opexpr->opno,
 									   &left_hashfn, &right_hashfn))
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index da7f52cab0..1f3c786b30 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -839,7 +839,6 @@ typedef struct SubPlanState
 	Oid		   *tab_eq_funcoids;	/* equality func oids for table
 									 * datatype(s) */
 	FmgrInfo   *tab_hash_funcs; /* hash functions for table datatype(s) */
-	FmgrInfo   *tab_eq_funcs;	/* equality functions for table datatype(s) */
 	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 */
-- 
2.11.0

0003-Add-a-Param-result-cache-to-SubPlan-nodes.patchtext/x-patch; name=0003-Add-a-Param-result-cache-to-SubPlan-nodes.patchDownload
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

#2Laurenz Albe
laurenz.albe@cybertec.at
In reply to: Heikki Linnakangas (#1)
Re: Subplan result caching

Heikki Linnakangas wrote:

I've been working on a patch to add a little cache to SubPlans, to speed
up queries with correlated subqueries, where the same subquery is
currently executed multiple times with the same parameters. The idea is
to cache the result of the subplan, with the correlation vars as the
cache key.

I like the idea.

I think the cache should be limited in size, perhaps by work_mem.
Also, there probably should be a GUC for this, defaulting to "off".

Yours,
Laurenz Albe

#3Vladimir Sitnikov
sitnikov.vladimir@gmail.com
In reply to: Laurenz Albe (#2)
Re: Subplan result caching

I like the idea.

+1

Also, there probably should be a GUC for this, defaulting to "off".

I think the feature could be on by default provided it can properly
identify "volatile" functions/tables hidden behind views.

Vladimir

#4David Rowley
david.rowley@2ndquadrant.com
In reply to: Heikki Linnakangas (#1)
Re: Subplan result caching

On 23 May 2018 at 21:31, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

I've been working on a patch to add a little cache to SubPlans, to speed up
queries with correlated subqueries, where the same subquery is currently
executed multiple times with the same parameters. The idea is to cache the
result of the subplan, with the correlation vars as the cache key.

...

Thoughts?

G'day Sir,

I'm in favour of making improvements here. I had a think about this
and just glanced at the patch to check if you'd done it the way I'd
thought..

I'd thought this might be done with some sort of "LazyMaterialize"
node that could take params and store multiple rows per param set.
That node type could contain all the LRU logic to get rid of the
lesser used items when work_mem begin to fill. If you did it this way
then nodeSubplan.c would need to know nothing about this. The planner
would simply just inject one of these when it thinks some caching
would be wise, similar to how it does with Materialize.
LazyMaterialize would simply check the cache and return those rows, if
they exist, otherwise consult its only subplan to get the rows and
then cache them. If you did it this way, as a followup we could go
plug it into parameterised nested loops to speed up repeated lookups
of the inner side plan. The gains there are probably similar to what
you've mentioned.

What do you think?

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#5Tom Lane
tgl@sss.pgh.pa.us
In reply to: Heikki Linnakangas (#1)
Re: Subplan result caching

Heikki Linnakangas <hlinnaka@iki.fi> writes:

I've been working on a patch to add a little cache to SubPlans, to speed
up queries with correlated subqueries, where the same subquery is
currently executed multiple times with the same parameters. The idea is
to cache the result of the subplan, with the correlation vars as the
cache key.

I find this pretty bogus as it stands, because it assumes without proof
that the subquery will deliver identical results for any two parameter
values that are considered equal by the datatype's default equality
operator. An easy counterexample is a subquery whose result depends on
the text conversion of a float8 parameter: zero and minus zero have
different text forms, but are equal according to float8eq. To make this
patch safe, I think you'd need to grovel through the subquery and make
sure that the parameters are only used as inputs to operators that belong
to the type's default btree or hash opfamily. (Many other cases would
work in practice, but we have no semantic knowledge that would let us be
sure of that.)

That's doable no doubt, but I wonder whether that leaves you in a place
that's any better than the plan-time-decorrelation approach you proposed
in the earlier thread. I liked that better TBH; this one seems like
a very ad-hoc reinvention of a hash join. I don't especially like the
unpredictable number of executions of the subquery that it results in,
either.

regards, tom lane

#6Heikki Linnakangas
hlinnaka@iki.fi
In reply to: Tom Lane (#5)
Re: Subplan result caching

On 23/05/18 19:25, Tom Lane wrote:

Heikki Linnakangas <hlinnaka@iki.fi> writes:

I've been working on a patch to add a little cache to SubPlans, to speed
up queries with correlated subqueries, where the same subquery is
currently executed multiple times with the same parameters. The idea is
to cache the result of the subplan, with the correlation vars as the
cache key.

I find this pretty bogus as it stands, because it assumes without proof
that the subquery will deliver identical results for any two parameter
values that are considered equal by the datatype's default equality
operator. An easy counterexample is a subquery whose result depends on
the text conversion of a float8 parameter: zero and minus zero have
different text forms, but are equal according to float8eq.

Good point.

To make this
patch safe, I think you'd need to grovel through the subquery and make
sure that the parameters are only used as inputs to operators that belong
to the type's default btree or hash opfamily. (Many other cases would
work in practice, but we have no semantic knowledge that would let us be
sure of that.)

Hmm. First thing that comes to mind is to use the raw bytes as cache
key, only treating Datums as equal if their binary representation is
identical.

That's doable no doubt, but I wonder whether that leaves you in a place
that's any better than the plan-time-decorrelation approach you proposed
in the earlier thread. I liked that better TBH; this one seems like
a very ad-hoc reinvention of a hash join. I don't especially like the
unpredictable number of executions of the subquery that it results in,
either.

I'd certainly prefer the plan-time-decorrelation, too. But I suspect
that there's always going to be some cases that cannot be decorrelated,
where a cache like this would help.

- Heikki

#7Tom Lane
tgl@sss.pgh.pa.us
In reply to: Heikki Linnakangas (#6)
Re: Subplan result caching

Heikki Linnakangas <hlinnaka@iki.fi> writes:

On 23/05/18 19:25, Tom Lane wrote:

To make this
patch safe, I think you'd need to grovel through the subquery and make
sure that the parameters are only used as inputs to operators that belong
to the type's default btree or hash opfamily. (Many other cases would
work in practice, but we have no semantic knowledge that would let us be
sure of that.)

Hmm. First thing that comes to mind is to use the raw bytes as cache
key, only treating Datums as equal if their binary representation is
identical.

Ah. That would work, though it'd make the number of subquery executions
even less predictable (since some logically-equal values would compare
as physically unequal).

regards, tom lane

#8Andres Freund
andres@anarazel.de
In reply to: Tom Lane (#7)
Re: Subplan result caching

On 2018-05-23 12:51:38 -0400, Tom Lane wrote:

Heikki Linnakangas <hlinnaka@iki.fi> writes:

On 23/05/18 19:25, Tom Lane wrote:

To make this
patch safe, I think you'd need to grovel through the subquery and make
sure that the parameters are only used as inputs to operators that belong
to the type's default btree or hash opfamily. (Many other cases would
work in practice, but we have no semantic knowledge that would let us be
sure of that.)

Hmm. First thing that comes to mind is to use the raw bytes as cache
key, only treating Datums as equal if their binary representation is
identical.

Ah. That would work, though it'd make the number of subquery executions
even less predictable (since some logically-equal values would compare
as physically unequal).

As long as there's no volatile functions, would anybody care?

Greetings,

Andres Freund

#9David Rowley
david.rowley@2ndquadrant.com
In reply to: Tom Lane (#5)
Re: Subplan result caching

On 24 May 2018 at 04:25, Tom Lane <tgl@sss.pgh.pa.us> wrote:

That's doable no doubt, but I wonder whether that leaves you in a place
that's any better than the plan-time-decorrelation approach you proposed
in the earlier thread. I liked that better TBH; this one seems like
a very ad-hoc reinvention of a hash join. I don't especially like the
unpredictable number of executions of the subquery that it results in,
either.

Decorrelation is not always going to be the answer. There's going to
be plenty of cases where that makes the plan worse.

Consider:

SELECT * FROM sometable s WHERE rarelytrue AND y = (SELECT MAX(x) FROM
bigtable b WHERE b.z = s.z);

If the planner went and re-wrote that to execute as the following would;

SELECT * FROM sometable s LEFT JOIN (SELECT z,MAX(x) max FROM bigtable
GROUP BY z) b ON b.z = s.z
WHERE rarelytrue AND y = b.max;

then we've probably gone and built most of the groups for nothing.

The planner would have do this based on estimated costs. Having the
ability to apply either of these optimisations would be useful,
providing the planner applied them correctly. However, I don't think
Heikki should be touching the decorrelation as part of this effort.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#10Robert Haas
robertmhaas@gmail.com
In reply to: Tom Lane (#7)
Re: Subplan result caching

On Wed, May 23, 2018 at 12:51 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:

Ah. That would work, though it'd make the number of subquery executions
even less predictable (since some logically-equal values would compare
as physically unequal).

In most cases that seems fine. It might not be fine with the subquery
contains volatile functions, though. I think I'd be sad if I wrote a
query expecting random() to be executing 26000 times and it got
executed 11 times instead. But if the optimizer finds a way to
execute int4pl fewer times, that seems like a good thing.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#11Robert Haas
robertmhaas@gmail.com
In reply to: Laurenz Albe (#2)
Re: Subplan result caching

On Wed, May 23, 2018 at 6:03 AM, Laurenz Albe <laurenz.albe@cybertec.at> wrote:

I think the cache should be limited in size, perhaps by work_mem.
Also, there probably should be a GUC for this, defaulting to "off".

Eh, why? Generally we want performance optimizations turned on by
default; otherwise only highly-knowledgeable users end up getting any
benefit. An exception is when there's some significant downside to
the optimization, but I don't think I understand what the downside of
this could be. Maybe it's bad if we populate the cache and never use
it? But until the patch is written we don't know whether there's
really a case that regresses like that, and if there is, it would be
better to fix it so it doesn't rather than make the feature
off-by-default.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#12Peter Eisentraut
peter.eisentraut@2ndquadrant.com
In reply to: Heikki Linnakangas (#1)
Re: Subplan result caching

On 23.05.18 11:31, Heikki Linnakangas wrote:

I've been working on a patch to add a little cache to SubPlans, to speed
up queries with correlated subqueries, where the same subquery is
currently executed multiple times with the same parameters. The idea is
to cache the result of the subplan, with the correlation vars as the
cache key.

It looks like this patch might be "returned with feedback" for the moment?

--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

#13David Rowley
david.rowley@2ndquadrant.com
In reply to: Heikki Linnakangas (#1)
Re: Subplan result caching

On 23 May 2018 at 21:31, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

I've been working on a patch to add a little cache to SubPlans, to speed up
queries with correlated subqueries, where the same subquery is currently
executed multiple times with the same parameters. The idea is to cache the
result of the subplan, with the correlation vars as the cache key.

Hi,

This seems like an interesting area to make improvements, so I've
signed up to review the patch.

From looking at the code I see that the caching is being done inside
nodeSubplan.c. I don't think this is the right approach to the
problem. The problem exists for any parameterized path, so I think a
more general approach would be much better.

We already have Materialize nodes to cache the results of an entire
subplan, and this seems to have quite a bit in common with that, only
we'd want to cache multiple results with a key to determine which
result set should be returned. Due to the similarities with
Materialize, I think that the cache should be a node itself and not
bury the cache logic in some other node type that's meant for some
other purpose.

"LazyMaterialize" seems like a good option for a name. It seems better
than "LazyHash" since you may not want to restrict it to a hash table
based cache in the future. A binary search tree may be a good option
for types that cannot be hashed.

Materialize nodes are injected above the inner side node of MergeJoins
based on cost, so I think this node type could just do the same. Maybe
something like estimate_num_groups(<exprs being compared to params>) /
path->rows is below some defined constant, perhaps something like 0.5.
Although experimentation would be required. It might be good to take
into account some other cost factors too.

I imagine we'd want to only allow this optimisation for hashjoinable
types. This seems pretty natural since your cache implementation is a
hash table, so, of course, we're going to need a hash function.

Wondering your thoughts on this idea.

I'll mark as waiting on author in the meantime.

It's great to see someone working on this.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#14Robert Haas
robertmhaas@gmail.com
In reply to: David Rowley (#13)
Re: Subplan result caching

On Mon, Jul 9, 2018 at 5:08 AM, David Rowley
<david.rowley@2ndquadrant.com> wrote:

From looking at the code I see that the caching is being done inside
nodeSubplan.c. I don't think this is the right approach to the
problem. The problem exists for any parameterized path, so I think a
more general approach would be much better.

Yeah, perhaps, though sometimes a more specific problem is easier to solve.

"LazyMaterialize" seems like a good option for a name. It seems better
than "LazyHash" since you may not want to restrict it to a hash table
based cache in the future. A binary search tree may be a good option
for types that cannot be hashed.

I think that's not too clear, actually. The difference between a
Materialize and a LazyMaterialize is not that this is lazy and that's
not. It's that this can cache multiple result sets for various
parameter values and that can only cache one result set.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#15Michael Paquier
michael@paquier.xyz
In reply to: David Rowley (#13)
Re: Subplan result caching

On Mon, Jul 09, 2018 at 09:08:07PM +1200, David Rowley wrote:

I'll mark as waiting on author in the meantime.

It's great to see someone working on this.

Not that actively unfortunately. The patch status has remained
unchanged since, so I am marking it as returned with feedback.
--
Michael

#16David Rowley
david.rowley@2ndquadrant.com
In reply to: Robert Haas (#14)
Re: Subplan result caching

On 19 July 2018 at 06:27, Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Jul 9, 2018 at 5:08 AM, David Rowley

"LazyMaterialize" seems like a good option for a name. It seems better
than "LazyHash" since you may not want to restrict it to a hash table
based cache in the future. A binary search tree may be a good option
for types that cannot be hashed.

I think that's not too clear, actually. The difference between a
Materialize and a LazyMaterialize is not that this is lazy and that's
not. It's that this can cache multiple result sets for various
parameter values and that can only cache one result set.

Okay. I'm not going to argue with the name of a node type that does
not exist yet. I was more suggesting that it should be a modular
component that we can plug into whatever plan types suit it. My
suggestions for naming was admittedly more of a sales tactic to gain
support for the idea, which perhaps failed.

--
David Rowley http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#17Andy Fan
zhihui.fan1213@gmail.com
In reply to: David Rowley (#13)
Re: Subplan result caching

On Sun, Apr 26, 2020 at 2:11 PM David Rowley <david.rowley@2ndquadrant.com>
wrote:

On 23 May 2018 at 21:31, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

I've been working on a patch to add a little cache to SubPlans, to speed

up

queries with correlated subqueries, where the same subquery is currently
executed multiple times with the same parameters. The idea is to cache

the

result of the subplan, with the correlation vars as the cache key.

Hi,

This seems like an interesting area to make improvements, so I've
signed up to review the patch.

From looking at the code I see that the caching is being done inside
nodeSubplan.c. I don't think this is the right approach to the
problem. The problem exists for any parameterized path, so I think a
more general approach would be much better.

I'm +1 on this suggestion. Actually I'm working on the query like this:

select j1o.i, j2_v.sum_5
from j1 j1o
inner join lateral
(select im100, sum(im5) as sum_5
from j2
where j1o.im100 = im100
and j1o.i = 1
group by im100) j2_v
on true
where j1o.i = 1;

I hope the we have a result cache for j2_v. But this nothing with SubPlan.

If we want to handle this case as well, one of the changes would
be it needs to cache multi records for one input parameter, or return
one row each time but return mutli times for one input parameter,
Tuplestore may be a good option for this case since its full functionalities
like tuple_puttuple, tuple_gettuple. But if we implement it with tuplestore,
the next question is how to control the memory usage for this Node.
We can use the dedicated memory context to know how many memory
this node used in total, but we can't stop the tuplestore from using more
memory. Or we can force set both current tuplestore->state to
TTS_WRITEFILE
and set the allowedMem to 0 for the following tuplestore, after we find too
memory is used. However this looks a bit of hack.

As a summary of my thinking about this topic before, I'd write another
incomplete options to handle this case. 1) we just use the the Material
Path to cache the result for last parameter only. if the following value
is
same as the last one, we use it. Or else, we rescan the Material path.
2). We can consider order the rows by input params key. That will be
much cache friendly.

BTW, I see may node->chgParams was changed without checking if the
datum changed from the previous one, this may lost some optimization
opportunities (for example during the agg node rescan case, if the params
is not changed, the we use the hash table we build before). So if we we
add new parameter to node->chgParams only if the datum really changed,
will it still be semantic correctly.

It's great to see someone working on this.

I'd like to have a try.

Best Regards
Andy Fan

#18David Rowley
dgrowleyml@gmail.com
In reply to: Andy Fan (#17)
Re: Subplan result caching

On Sun, 26 Apr 2020 at 19:08, Andy Fan <zhihui.fan1213@gmail.com> wrote:

If we want to handle this case as well, one of the changes would
be it needs to cache multi records for one input parameter, or return
one row each time but return mutli times for one input parameter,
Tuplestore may be a good option for this case since its full functionalities
like tuple_puttuple, tuple_gettuple. But if we implement it with tuplestore,
the next question is how to control the memory usage for this Node.
We can use the dedicated memory context to know how many memory
this node used in total, but we can't stop the tuplestore from using more
memory. Or we can force set both current tuplestore->state to TTS_WRITEFILE
and set the allowedMem to 0 for the following tuplestore, after we find too
memory is used. However this looks a bit of hack.

I didn't imagine a tuplestore would be that useful for this. A node
like this will do its best work when the ratio of n_values /
distinct_values of the parameters is high. The planner can often not
be that great at knowing the number of distinct values, especially so
when there is more than one expression to estimate the number of
distinct values for. (we added extended statistics to try to help with
that). I think this node will do its best when the time spent for a
cache miss it bearly any more expensive than scanning the subnode to
get the results. If we can do that then we'll see fewer regressions
for when we inject one of these nodes where it'll do no good, e.g when
we'll never get a repeated value. If we start spilling these tuples
out to disk then it adds overhead which might never pay off.

I'd suggest a hash table to act as an MRU cache. We'd just evict old
values when we run out of space, i.e consume all of work_mem.

I've got a bunch of code locally which is still a work in progress to
do this. I'll finish it off and post it here.

David

#19Andy Fan
zhihui.fan1213@gmail.com
In reply to: David Rowley (#18)
Re: Subplan result caching

On Sun, Apr 26, 2020 at 5:49 PM David Rowley <dgrowleyml@gmail.com> wrote:

On Sun, 26 Apr 2020 at 19:08, Andy Fan <zhihui.fan1213@gmail.com> wrote:

If we want to handle this case as well, one of the changes would
be it needs to cache multi records for one input parameter, or return
one row each time but return mutli times for one input parameter,
Tuplestore may be a good option for this case since its full

functionalities

like tuple_puttuple, tuple_gettuple. But if we implement it with

tuplestore,

the next question is how to control the memory usage for this Node.
We can use the dedicated memory context to know how many memory
this node used in total, but we can't stop the tuplestore from using more
memory. Or we can force set both current tuplestore->state to

TTS_WRITEFILE

and set the allowedMem to 0 for the following tuplestore, after we find

too

memory is used. However this looks a bit of hack.

I didn't imagine a tuplestore would be that useful for this. A node
like this will do its best work when the ratio of n_values /
distinct_values of the parameters is high. The planner can often not
be that great at knowing the number of distinct values, especially so
when there is more than one expression to estimate the number of
distinct values for. (we added extended statistics to try to help with
that). I think this node will do its best when the time spent for a
cache miss it bearly any more expensive than scanning the subnode to
get the results. If we can do that then we'll see fewer regressions
for when we inject one of these nodes where it'll do no good, e.g when
we'll never get a repeated value. If we start spilling these tuples
out to disk then it adds overhead which might never pay off.

I'd suggest a hash table to act as an MRU cache. We'd just evict old
values when we run out of space, i.e consume all of work_mem.

I've got a bunch of code locally which is still a work in progress to
do this. I'll finish it off and post it here.

I was feeling that we may have to maintain some extra status if we use hash
table rather than tuple store, but that might be not a major concern. I can
wait and see your patch.

Best Regards
Andy Fan

#20David Rowley
dgrowleyml@gmail.com
In reply to: Andy Fan (#19)
Re: Subplan result caching

On Mon, 27 Apr 2020 at 00:37, Andy Fan <zhihui.fan1213@gmail.com> wrote:

I was feeling that we may have to maintain some extra status if we use hash
table rather than tuple store, but that might be not a major concern. I can
wait and see your patch.

I've posted the patch and lots of details about it in
/messages/by-id/CAApHDvrPcQyQdWERGYWx8J+2DLUNgXu+fOSbQ1UscxrunyXyrQ@mail.gmail.com

David

#21Andy Fan
zhihui.fan1213@gmail.com
In reply to: David Rowley (#20)
Re: Subplan result caching

On Wed, May 20, 2020 at 7:47 PM David Rowley <dgrowleyml@gmail.com> wrote:

On Mon, 27 Apr 2020 at 00:37, Andy Fan <zhihui.fan1213@gmail.com> wrote:

I was feeling that we may have to maintain some extra status if we use

hash

table rather than tuple store, but that might be not a major concern. I

can

wait and see your patch.

I've posted the patch and lots of details about it in

/messages/by-id/CAApHDvrPcQyQdWERGYWx8J+2DLUNgXu+fOSbQ1UscxrunyXyrQ@mail.gmail.com

Amazing! I will start to study it today.

--
Best Regards
Andy Fan