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,
https://www.postgresql.org/message-id/67e353e8-c20e-7980-a282-538779edf25b%40iki.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
>From d3a738ea6427df908b61a5fdd3ee14a7b78b724a Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakan...@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
>From fcf040b589ffc318cbbc060deec296577b3ff24b Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakan...@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
>From ff8630a06eee0fc39dc03dc7f8eed0ea271f29ff Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <heikki.linnakan...@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