summaryrefslogtreecommitdiff
path: root/src/include
diff options
context:
space:
mode:
authorDavid Rowley2021-03-31 23:32:22 +0000
committerDavid Rowley2021-03-31 23:32:22 +0000
commitb6002a796dc0bfe721db5eaa54ba9d24fd9fd416 (patch)
treeb724302e02aca3c48b09eede7987e86250991219 /src/include
parent6ec578e60101c3c02533f99715945a0400fb3286 (diff)
Add Result Cache executor node
Here we add a new executor node type named "Result Cache". The planner can include this node type in the plan to have the executor cache the results from the inner side of parameterized nested loop joins. This allows caching of tuples for sets of parameters so that in the event that the node sees the same parameter values again, it can just return the cached tuples instead of rescanning the inner side of the join all over again. Internally, result cache uses a hash table in order to quickly find tuples that have been previously cached. For certain data sets, this can significantly improve the performance of joins. The best cases for using this new node type are for join problems where a large portion of the tuples from the inner side of the join have no join partner on the outer side of the join. In such cases, hash join would have to hash values that are never looked up, thus bloating the hash table and possibly causing it to multi-batch. Merge joins would have to skip over all of the unmatched rows. If we use a nested loop join with a result cache, then we only cache tuples that have at least one join partner on the outer side of the join. The benefits of using a parameterized nested loop with a result cache increase when there are fewer distinct values being looked up and the number of lookups of each value is large. Also, hash probes to lookup the cache can be much faster than the hash probe in a hash join as it's common that the result cache's hash table is much smaller than the hash join's due to result cache only caching useful tuples rather than all tuples from the inner side of the join. This variation in hash probe performance is more significant when the hash join's hash table no longer fits into the CPU's L3 cache, but the result cache's hash table does. The apparent "random" access of hash buckets with each hash probe can cause a poor L3 cache hit ratio for large hash tables. Smaller hash tables generally perform better. The hash table used for the cache limits itself to not exceeding work_mem * hash_mem_multiplier in size. We maintain a dlist of keys for this cache and when we're adding new tuples and realize we've exceeded the memory budget, we evict cache entries starting with the least recently used ones until we have enough memory to add the new tuples to the cache. For parameterized nested loop joins, we now consider using one of these result cache nodes in between the nested loop node and its inner node. We determine when this might be useful based on cost, which is primarily driven off of what the expected cache hit ratio will be. Estimating the cache hit ratio relies on having good distinct estimates on the nested loop's parameters. For now, the planner will only consider using a result cache for parameterized nested loop joins. This works for both normal joins and also for LATERAL type joins to subqueries. It is possible to use this new node for other uses in the future. For example, to cache results from correlated subqueries. However, that's not done here due to some difficulties obtaining a distinct estimation on the outer plan to calculate the estimated cache hit ratio. Currently we plan the inner plan before planning the outer plan so there is no good way to know if a result cache would be useful or not since we can't estimate the number of times the subplan will be called until the outer plan is generated. The functionality being added here is newly introducing a dependency on the return value of estimate_num_groups() during the join search. Previously, during the join search, we only ever needed to perform selectivity estimations. With this commit, we need to use estimate_num_groups() in order to estimate what the hit ratio on the result cache will be. In simple terms, if we expect 10 distinct values and we expect 1000 outer rows, then we'll estimate the hit ratio to be 99%. Since cache hits are very cheap compared to scanning the underlying nodes on the inner side of the nested loop join, then this will significantly reduce the planner's cost for the join. However, it's fairly easy to see here that things will go bad when estimate_num_groups() incorrectly returns a value that's significantly lower than the actual number of distinct values. If this happens then that may cause us to make use of a nested loop join with a result cache instead of some other join type, such as a merge or hash join. Our distinct estimations have been known to be a source of trouble in the past, so the extra reliance on them here could cause the planner to choose slower plans than it did previous to having this feature. Distinct estimations are also fairly hard to estimate accurately when several tables have been joined already or when a WHERE clause filters out a set of values that are correlated to the expressions we're estimating the number of distinct value for. For now, the costing we perform during query planning for result caches does put quite a bit of faith in the distinct estimations being accurate. When these are accurate then we should generally see faster execution times for plans containing a result cache. However, in the real world, we may find that we need to either change the costings to put less trust in the distinct estimations being accurate or perhaps even disable this feature by default. There's always an element of risk when we teach the query planner to do new tricks that it decides to use that new trick at the wrong time and causes a regression. Users may opt to get the old behavior by turning the feature off using the enable_resultcache GUC. Currently, this is enabled by default. It remains to be seen if we'll maintain that setting for the release. Additionally, the name "Result Cache" is the best name I could think of for this new node at the time I started writing the patch. Nobody seems to strongly dislike the name. A few people did suggest other names but no other name seemed to dominate in the brief discussion that there was about names. Let's allow the beta period to see if the current name pleases enough people. If there's some consensus on a better name, then we can change it before the release. Please see the 2nd discussion link below for the discussion on the "Result Cache" name. Author: David Rowley Reviewed-by: Andy Fan, Justin Pryzby, Zhihong Yu Tested-By: Konstantin Knizhnik Discussion: https://postgr.es/m/CAApHDvrPcQyQdWERGYWx8J%2B2DLUNgXu%2BfOSbQ1UscxrunyXyrQ%40mail.gmail.com Discussion: https://postgr.es/m/CAApHDvq=yQXr5kqhRviT2RhNKwToaWr9JAN5t+5_PzhuRJ3wvg@mail.gmail.com
Diffstat (limited to 'src/include')
-rw-r--r--src/include/executor/executor.h7
-rw-r--r--src/include/executor/nodeResultCache.h31
-rw-r--r--src/include/lib/ilist.h19
-rw-r--r--src/include/nodes/execnodes.h66
-rw-r--r--src/include/nodes/nodes.h3
-rw-r--r--src/include/nodes/pathnodes.h22
-rw-r--r--src/include/nodes/plannodes.h21
-rw-r--r--src/include/optimizer/cost.h1
-rw-r--r--src/include/optimizer/pathnode.h7
9 files changed, 177 insertions, 0 deletions
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 34dd861eff0..26dcc4485eb 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -275,6 +275,13 @@ extern ExprState *ExecBuildGroupingEqual(TupleDesc ldesc, TupleDesc rdesc,
const Oid *eqfunctions,
const Oid *collations,
PlanState *parent);
+extern ExprState *ExecBuildParamSetEqual(TupleDesc desc,
+ const TupleTableSlotOps *lops,
+ const TupleTableSlotOps *rops,
+ const Oid *eqfunctions,
+ const Oid *collations,
+ const List *param_exprs,
+ PlanState *parent);
extern ProjectionInfo *ExecBuildProjectionInfo(List *targetList,
ExprContext *econtext,
TupleTableSlot *slot,
diff --git a/src/include/executor/nodeResultCache.h b/src/include/executor/nodeResultCache.h
new file mode 100644
index 00000000000..df671d16f96
--- /dev/null
+++ b/src/include/executor/nodeResultCache.h
@@ -0,0 +1,31 @@
+/*-------------------------------------------------------------------------
+ *
+ * nodeResultCache.h
+ *
+ *
+ *
+ * Portions Copyright (c) 2021, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/executor/nodeResultCache.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef NODERESULTCACHE_H
+#define NODERESULTCACHE_H
+
+#include "nodes/execnodes.h"
+
+extern ResultCacheState *ExecInitResultCache(ResultCache *node, EState *estate, int eflags);
+extern void ExecEndResultCache(ResultCacheState *node);
+extern void ExecReScanResultCache(ResultCacheState *node);
+extern double ExecEstimateCacheEntryOverheadBytes(double ntuples);
+extern void ExecResultCacheEstimate(ResultCacheState *node,
+ ParallelContext *pcxt);
+extern void ExecResultCacheInitializeDSM(ResultCacheState *node,
+ ParallelContext *pcxt);
+extern void ExecResultCacheInitializeWorker(ResultCacheState *node,
+ ParallelWorkerContext *pwcxt);
+extern void ExecResultCacheRetrieveInstrumentation(ResultCacheState *node);
+
+#endif /* NODERESULTCACHE_H */
diff --git a/src/include/lib/ilist.h b/src/include/lib/ilist.h
index aa196428ed9..ddbdb207afa 100644
--- a/src/include/lib/ilist.h
+++ b/src/include/lib/ilist.h
@@ -395,6 +395,25 @@ dlist_move_head(dlist_head *head, dlist_node *node)
}
/*
+ * Move element from its current position in the list to the tail position in
+ * the same list.
+ *
+ * Undefined behaviour if 'node' is not already part of the list.
+ */
+static inline void
+dlist_move_tail(dlist_head *head, dlist_node *node)
+{
+ /* fast path if it's already at the tail */
+ if (head->head.prev == node)
+ return;
+
+ dlist_delete(node);
+ dlist_push_tail(head, node);
+
+ dlist_check(head);
+}
+
+/*
* Check whether 'node' has a following node.
* Caution: unreliable if 'node' is not in the list.
*/
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 3b39369a492..52d1fa018b5 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -17,6 +17,7 @@
#include "access/tupconvert.h"
#include "executor/instrument.h"
#include "fmgr.h"
+#include "lib/ilist.h"
#include "lib/pairingheap.h"
#include "nodes/params.h"
#include "nodes/plannodes.h"
@@ -2037,6 +2038,71 @@ typedef struct MaterialState
Tuplestorestate *tuplestorestate;
} MaterialState;
+struct ResultCacheEntry;
+struct ResultCacheTuple;
+struct ResultCacheKey;
+
+typedef struct ResultCacheInstrumentation
+{
+ uint64 cache_hits; /* number of rescans where we've found the
+ * scan parameter values to be cached */
+ uint64 cache_misses; /* number of rescans where we've not found the
+ * scan parameter values to be cached. */
+ uint64 cache_evictions; /* number of cache entries removed due to
+ * the need to free memory */
+ uint64 cache_overflows; /* number of times we've had to bypass the
+ * cache when filling it due to not being
+ * able to free enough space to store the
+ * current scan's tuples. */
+ uint64 mem_peak; /* peak memory usage in bytes */
+} ResultCacheInstrumentation;
+
+/* ----------------
+ * Shared memory container for per-worker resultcache information
+ * ----------------
+ */
+typedef struct SharedResultCacheInfo
+{
+ int num_workers;
+ ResultCacheInstrumentation sinstrument[FLEXIBLE_ARRAY_MEMBER];
+} SharedResultCacheInfo;
+
+/* ----------------
+ * ResultCacheState information
+ *
+ * resultcache nodes are used to cache recent and commonly seen results
+ * from a parameterized scan.
+ * ----------------
+ */
+typedef struct ResultCacheState
+{
+ ScanState ss; /* its first field is NodeTag */
+ int rc_status; /* value of ExecResultCache state machine */
+ int nkeys; /* number of cache keys */
+ struct resultcache_hash *hashtable; /* hash table for cache entries */
+ TupleDesc hashkeydesc; /* tuple descriptor for cache keys */
+ TupleTableSlot *tableslot; /* min tuple slot for existing cache entries */
+ TupleTableSlot *probeslot; /* virtual slot used for hash lookups */
+ ExprState *cache_eq_expr; /* Compare exec params to hash key */
+ ExprState **param_exprs; /* exprs containing the parameters to this
+ * node */
+ FmgrInfo *hashfunctions; /* lookup data for hash funcs nkeys in size */
+ Oid *collations; /* collation for comparisons nkeys in size */
+ uint64 mem_used; /* bytes of memory used by cache */
+ uint64 mem_limit; /* memory limit in bytes for the cache */
+ MemoryContext tableContext; /* memory context to store cache data */
+ dlist_head lru_list; /* least recently used entry list */
+ struct ResultCacheTuple *last_tuple; /* Used to point to the last tuple
+ * returned during a cache hit and
+ * the tuple we last stored when
+ * populating the cache. */
+ struct ResultCacheEntry *entry; /* the entry that 'last_tuple' belongs to
+ * or NULL if 'last_tuple' is NULL. */
+ bool singlerow; /* true if the cache entry is to be marked as
+ * complete after caching the first tuple. */
+ ResultCacheInstrumentation stats; /* execution statistics */
+ SharedResultCacheInfo *shared_info; /* statistics for parallel workers */
+} ResultCacheState;
/* ----------------
* When performing sorting by multiple keys, it's possible that the input
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 704f00fd300..2051abbbf92 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -74,6 +74,7 @@ typedef enum NodeTag
T_MergeJoin,
T_HashJoin,
T_Material,
+ T_ResultCache,
T_Sort,
T_IncrementalSort,
T_Group,
@@ -132,6 +133,7 @@ typedef enum NodeTag
T_MergeJoinState,
T_HashJoinState,
T_MaterialState,
+ T_ResultCacheState,
T_SortState,
T_IncrementalSortState,
T_GroupState,
@@ -242,6 +244,7 @@ typedef enum NodeTag
T_MergeAppendPath,
T_GroupResultPath,
T_MaterialPath,
+ T_ResultCachePath,
T_UniquePath,
T_GatherPath,
T_GatherMergePath,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index e4e1c15986f..a65bda7e3c6 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1495,6 +1495,25 @@ typedef struct MaterialPath
} MaterialPath;
/*
+ * ResultCachePath represents a ResultCache plan node, i.e., a cache that
+ * caches tuples from parameterized paths to save the underlying node from
+ * having to be rescanned for parameter values which are already cached.
+ */
+typedef struct ResultCachePath
+{
+ Path path;
+ Path *subpath; /* outerpath to cache tuples from */
+ List *hash_operators; /* hash operators for each key */
+ List *param_exprs; /* cache keys */
+ bool singlerow; /* true if the cache entry is to be marked as
+ * complete after caching the first record. */
+ double calls; /* expected number of rescans */
+ uint32 est_entries; /* The maximum number of entries that the
+ * planner expects will fit in the cache, or 0
+ * if unknown */
+} ResultCachePath;
+
+/*
* UniquePath represents elimination of distinct rows from the output of
* its subpath.
*
@@ -2091,6 +2110,9 @@ typedef struct RestrictInfo
Selectivity right_bucketsize; /* avg bucketsize of right side */
Selectivity left_mcvfreq; /* left side's most common val's freq */
Selectivity right_mcvfreq; /* right side's most common val's freq */
+
+ /* hash equality operator used for result cache, else InvalidOid */
+ Oid hasheqoperator;
} RestrictInfo;
/*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 623dc450ee4..1678bd66fed 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -780,6 +780,27 @@ typedef struct Material
} Material;
/* ----------------
+ * result cache node
+ * ----------------
+ */
+typedef struct ResultCache
+{
+ Plan plan;
+
+ int numKeys; /* size of the two arrays below */
+
+ Oid *hashOperators; /* hash operators for each key */
+ Oid *collations; /* cache keys */
+ List *param_exprs; /* exprs containing parameters */
+ bool singlerow; /* true if the cache entry should be marked as
+ * complete after we store the first tuple in
+ * it. */
+ uint32 est_entries; /* The maximum number of entries that the
+ * planner expects will fit in the cache, or 0
+ * if unknown */
+} ResultCache;
+
+/* ----------------
* sort node
* ----------------
*/
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index a3fd93fe07f..0fe60d82e43 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT bool enable_incremental_sort;
extern PGDLLIMPORT bool enable_hashagg;
extern PGDLLIMPORT bool enable_nestloop;
extern PGDLLIMPORT bool enable_material;
+extern PGDLLIMPORT bool enable_resultcache;
extern PGDLLIMPORT bool enable_mergejoin;
extern PGDLLIMPORT bool enable_hashjoin;
extern PGDLLIMPORT bool enable_gathermerge;
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index d539bc27830..53261ee91fd 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -82,6 +82,13 @@ extern GroupResultPath *create_group_result_path(PlannerInfo *root,
PathTarget *target,
List *havingqual);
extern MaterialPath *create_material_path(RelOptInfo *rel, Path *subpath);
+extern ResultCachePath *create_resultcache_path(PlannerInfo *root,
+ RelOptInfo *rel,
+ Path *subpath,
+ List *param_exprs,
+ List *hash_operators,
+ bool singlerow,
+ double calls);
extern UniquePath *create_unique_path(PlannerInfo *root, RelOptInfo *rel,
Path *subpath, SpecialJoinInfo *sjinfo);
extern GatherPath *create_gather_path(PlannerInfo *root,