mirror of
https://github.com/postgres/postgres.git
synced 2025-08-27 07:42:10 +03:00
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
This commit is contained in:
@@ -4032,6 +4032,10 @@ print_path(PlannerInfo *root, Path *path, int indent)
|
||||
ptype = "Material";
|
||||
subpath = ((MaterialPath *) path)->subpath;
|
||||
break;
|
||||
case T_ResultCache:
|
||||
ptype = "ResultCache";
|
||||
subpath = ((ResultCachePath *) path)->subpath;
|
||||
break;
|
||||
case T_UniquePath:
|
||||
ptype = "Unique";
|
||||
subpath = ((UniquePath *) path)->subpath;
|
||||
|
@@ -79,6 +79,7 @@
|
||||
#include "executor/executor.h"
|
||||
#include "executor/nodeAgg.h"
|
||||
#include "executor/nodeHash.h"
|
||||
#include "executor/nodeResultCache.h"
|
||||
#include "miscadmin.h"
|
||||
#include "nodes/makefuncs.h"
|
||||
#include "nodes/nodeFuncs.h"
|
||||
@@ -139,6 +140,7 @@ bool enable_incremental_sort = true;
|
||||
bool enable_hashagg = true;
|
||||
bool enable_nestloop = true;
|
||||
bool enable_material = true;
|
||||
bool enable_resultcache = true;
|
||||
bool enable_mergejoin = true;
|
||||
bool enable_hashjoin = true;
|
||||
bool enable_gathermerge = true;
|
||||
@@ -2402,6 +2404,147 @@ cost_material(Path *path,
|
||||
path->total_cost = startup_cost + run_cost;
|
||||
}
|
||||
|
||||
/*
|
||||
* cost_resultcache_rescan
|
||||
* Determines the estimated cost of rescanning a ResultCache node.
|
||||
*
|
||||
* In order to estimate this, we must gain knowledge of how often we expect to
|
||||
* be called and how many distinct sets of parameters we are likely to be
|
||||
* called with. If we expect a good cache hit ratio, then we can set our
|
||||
* costs to account for that hit ratio, plus a little bit of cost for the
|
||||
* caching itself. Caching will not work out well if we expect to be called
|
||||
* with too many distinct parameter values. The worst-case here is that we
|
||||
* never see any parameter value twice, in which case we'd never get a cache
|
||||
* hit and caching would be a complete waste of effort.
|
||||
*/
|
||||
static void
|
||||
cost_resultcache_rescan(PlannerInfo *root, ResultCachePath *rcpath,
|
||||
Cost *rescan_startup_cost, Cost *rescan_total_cost)
|
||||
{
|
||||
EstimationInfo estinfo;
|
||||
Cost input_startup_cost = rcpath->subpath->startup_cost;
|
||||
Cost input_total_cost = rcpath->subpath->total_cost;
|
||||
double tuples = rcpath->subpath->rows;
|
||||
double calls = rcpath->calls;
|
||||
int width = rcpath->subpath->pathtarget->width;
|
||||
|
||||
double hash_mem_bytes;
|
||||
double est_entry_bytes;
|
||||
double est_cache_entries;
|
||||
double ndistinct;
|
||||
double evict_ratio;
|
||||
double hit_ratio;
|
||||
Cost startup_cost;
|
||||
Cost total_cost;
|
||||
|
||||
/* available cache space */
|
||||
hash_mem_bytes = get_hash_mem() * 1024L;
|
||||
|
||||
/*
|
||||
* Set the number of bytes each cache entry should consume in the cache.
|
||||
* To provide us with better estimations on how many cache entries we can
|
||||
* store at once, we make a call to the executor here to ask it what
|
||||
* memory overheads there are for a single cache entry.
|
||||
*
|
||||
* XXX we also store the cache key, but that's not accounted for here.
|
||||
*/
|
||||
est_entry_bytes = relation_byte_size(tuples, width) +
|
||||
ExecEstimateCacheEntryOverheadBytes(tuples);
|
||||
|
||||
/* estimate on the upper limit of cache entries we can hold at once */
|
||||
est_cache_entries = floor(hash_mem_bytes / est_entry_bytes);
|
||||
|
||||
/* estimate on the distinct number of parameter values */
|
||||
ndistinct = estimate_num_groups(root, rcpath->param_exprs, calls, NULL,
|
||||
&estinfo);
|
||||
|
||||
/*
|
||||
* When the estimation fell back on using a default value, it's a bit too
|
||||
* risky to assume that it's ok to use a Result Cache. The use of a
|
||||
* default could cause us to use a Result Cache when it's really
|
||||
* inappropriate to do so. If we see that this has been done, then we'll
|
||||
* assume that every call will have unique parameters, which will almost
|
||||
* certainly mean a ResultCachePath will never survive add_path().
|
||||
*/
|
||||
if ((estinfo.flags & SELFLAG_USED_DEFAULT) != 0)
|
||||
ndistinct = calls;
|
||||
|
||||
/*
|
||||
* Since we've already estimated the maximum number of entries we can
|
||||
* store at once and know the estimated number of distinct values we'll be
|
||||
* called with, we'll take this opportunity to set the path's est_entries.
|
||||
* This will ultimately determine the hash table size that the executor
|
||||
* will use. If we leave this at zero, the executor will just choose the
|
||||
* size itself. Really this is not the right place to do this, but it's
|
||||
* convenient since everything is already calculated.
|
||||
*/
|
||||
rcpath->est_entries = Min(Min(ndistinct, est_cache_entries),
|
||||
PG_UINT32_MAX);
|
||||
|
||||
/*
|
||||
* When the number of distinct parameter values is above the amount we can
|
||||
* store in the cache, then we'll have to evict some entries from the
|
||||
* cache. This is not free. Here we estimate how often we'll incur the
|
||||
* cost of that eviction.
|
||||
*/
|
||||
evict_ratio = 1.0 - Min(est_cache_entries, ndistinct) / ndistinct;
|
||||
|
||||
/*
|
||||
* In order to estimate how costly a single scan will be, we need to
|
||||
* attempt to estimate what the cache hit ratio will be. To do that we
|
||||
* must look at how many scans are estimated in total for this node and
|
||||
* how many of those scans we expect to get a cache hit.
|
||||
*/
|
||||
hit_ratio = 1.0 / ndistinct * Min(est_cache_entries, ndistinct) -
|
||||
(ndistinct / calls);
|
||||
|
||||
/* Ensure we don't go negative */
|
||||
hit_ratio = Max(hit_ratio, 0.0);
|
||||
|
||||
/*
|
||||
* Set the total_cost accounting for the expected cache hit ratio. We
|
||||
* also add on a cpu_operator_cost to account for a cache lookup. This
|
||||
* will happen regardless of whether it's a cache hit or not.
|
||||
*/
|
||||
total_cost = input_total_cost * (1.0 - hit_ratio) + cpu_operator_cost;
|
||||
|
||||
/* Now adjust the total cost to account for cache evictions */
|
||||
|
||||
/* Charge a cpu_tuple_cost for evicting the actual cache entry */
|
||||
total_cost += cpu_tuple_cost * evict_ratio;
|
||||
|
||||
/*
|
||||
* Charge a 10th of cpu_operator_cost to evict every tuple in that entry.
|
||||
* The per-tuple eviction is really just a pfree, so charging a whole
|
||||
* cpu_operator_cost seems a little excessive.
|
||||
*/
|
||||
total_cost += cpu_operator_cost / 10.0 * evict_ratio * tuples;
|
||||
|
||||
/*
|
||||
* Now adjust for storing things in the cache, since that's not free
|
||||
* either. Everything must go in the cache. We don't proportion this
|
||||
* over any ratio, just apply it once for the scan. We charge a
|
||||
* cpu_tuple_cost for the creation of the cache entry and also a
|
||||
* cpu_operator_cost for each tuple we expect to cache.
|
||||
*/
|
||||
total_cost += cpu_tuple_cost + cpu_operator_cost * tuples;
|
||||
|
||||
/*
|
||||
* Getting the first row must be also be proportioned according to the
|
||||
* expected cache hit ratio.
|
||||
*/
|
||||
startup_cost = input_startup_cost * (1.0 - hit_ratio);
|
||||
|
||||
/*
|
||||
* Additionally we charge a cpu_tuple_cost to account for cache lookups,
|
||||
* which we'll do regardless of whether it was a cache hit or not.
|
||||
*/
|
||||
startup_cost += cpu_tuple_cost;
|
||||
|
||||
*rescan_startup_cost = startup_cost;
|
||||
*rescan_total_cost = total_cost;
|
||||
}
|
||||
|
||||
/*
|
||||
* cost_agg
|
||||
* Determines and returns the cost of performing an Agg plan node,
|
||||
@@ -4142,6 +4285,11 @@ cost_rescan(PlannerInfo *root, Path *path,
|
||||
*rescan_total_cost = run_cost;
|
||||
}
|
||||
break;
|
||||
case T_ResultCache:
|
||||
/* All the hard work is done by cost_resultcache_rescan */
|
||||
cost_resultcache_rescan(root, (ResultCachePath *) path,
|
||||
rescan_startup_cost, rescan_total_cost);
|
||||
break;
|
||||
default:
|
||||
*rescan_startup_cost = path->startup_cost;
|
||||
*rescan_total_cost = path->total_cost;
|
||||
|
@@ -18,10 +18,13 @@
|
||||
|
||||
#include "executor/executor.h"
|
||||
#include "foreign/fdwapi.h"
|
||||
#include "nodes/nodeFuncs.h"
|
||||
#include "optimizer/cost.h"
|
||||
#include "optimizer/optimizer.h"
|
||||
#include "optimizer/pathnode.h"
|
||||
#include "optimizer/paths.h"
|
||||
#include "optimizer/planmain.h"
|
||||
#include "utils/typcache.h"
|
||||
|
||||
/* Hook for plugins to get control in add_paths_to_joinrel() */
|
||||
set_join_pathlist_hook_type set_join_pathlist_hook = NULL;
|
||||
@@ -52,6 +55,9 @@ static void try_partial_mergejoin_path(PlannerInfo *root,
|
||||
static void sort_inner_and_outer(PlannerInfo *root, RelOptInfo *joinrel,
|
||||
RelOptInfo *outerrel, RelOptInfo *innerrel,
|
||||
JoinType jointype, JoinPathExtraData *extra);
|
||||
static inline bool clause_sides_match_join(RestrictInfo *rinfo,
|
||||
RelOptInfo *outerrel,
|
||||
RelOptInfo *innerrel);
|
||||
static void match_unsorted_outer(PlannerInfo *root, RelOptInfo *joinrel,
|
||||
RelOptInfo *outerrel, RelOptInfo *innerrel,
|
||||
JoinType jointype, JoinPathExtraData *extra);
|
||||
@@ -163,6 +169,11 @@ add_paths_to_joinrel(PlannerInfo *root,
|
||||
{
|
||||
case JOIN_SEMI:
|
||||
case JOIN_ANTI:
|
||||
|
||||
/*
|
||||
* XXX it may be worth proving this to allow a ResultCache to be
|
||||
* considered for Nested Loop Semi/Anti Joins.
|
||||
*/
|
||||
extra.inner_unique = false; /* well, unproven */
|
||||
break;
|
||||
case JOIN_UNIQUE_INNER:
|
||||
@@ -354,6 +365,180 @@ allow_star_schema_join(PlannerInfo *root,
|
||||
bms_nonempty_difference(inner_paramrels, outerrelids));
|
||||
}
|
||||
|
||||
/*
|
||||
* paraminfo_get_equal_hashops
|
||||
* Determine if param_info and innerrel's lateral_vars can be hashed.
|
||||
* Returns true the hashing is possible, otherwise return false.
|
||||
*
|
||||
* Additionally we also collect the outer exprs and the hash operators for
|
||||
* each parameter to innerrel. These set in 'param_exprs' and 'operators'
|
||||
* when we return true.
|
||||
*/
|
||||
static bool
|
||||
paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
|
||||
RelOptInfo *outerrel, RelOptInfo *innerrel,
|
||||
List **param_exprs, List **operators)
|
||||
|
||||
{
|
||||
ListCell *lc;
|
||||
|
||||
*param_exprs = NIL;
|
||||
*operators = NIL;
|
||||
|
||||
if (param_info != NULL)
|
||||
{
|
||||
List *clauses = param_info->ppi_clauses;
|
||||
|
||||
foreach(lc, clauses)
|
||||
{
|
||||
RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
|
||||
OpExpr *opexpr;
|
||||
Node *expr;
|
||||
|
||||
/* can't use result cache without a valid hash equals operator */
|
||||
if (!OidIsValid(rinfo->hasheqoperator) ||
|
||||
!clause_sides_match_join(rinfo, outerrel, innerrel))
|
||||
{
|
||||
list_free(*operators);
|
||||
list_free(*param_exprs);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* We already checked that this is an OpExpr with 2 args when
|
||||
* setting hasheqoperator.
|
||||
*/
|
||||
opexpr = (OpExpr *) rinfo->clause;
|
||||
if (rinfo->outer_is_left)
|
||||
expr = (Node *) linitial(opexpr->args);
|
||||
else
|
||||
expr = (Node *) lsecond(opexpr->args);
|
||||
|
||||
*operators = lappend_oid(*operators, rinfo->hasheqoperator);
|
||||
*param_exprs = lappend(*param_exprs, expr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Now add any lateral vars to the cache key too */
|
||||
foreach(lc, innerrel->lateral_vars)
|
||||
{
|
||||
Node *expr = (Node *) lfirst(lc);
|
||||
TypeCacheEntry *typentry;
|
||||
|
||||
/* Reject if there are any volatile functions */
|
||||
if (contain_volatile_functions(expr))
|
||||
{
|
||||
list_free(*operators);
|
||||
list_free(*param_exprs);
|
||||
return false;
|
||||
}
|
||||
|
||||
typentry = lookup_type_cache(exprType(expr),
|
||||
TYPECACHE_HASH_PROC | TYPECACHE_EQ_OPR);
|
||||
|
||||
/* can't use result cache without a valid hash equals operator */
|
||||
if (!OidIsValid(typentry->hash_proc) || !OidIsValid(typentry->eq_opr))
|
||||
{
|
||||
list_free(*operators);
|
||||
list_free(*param_exprs);
|
||||
return false;
|
||||
}
|
||||
|
||||
*operators = lappend_oid(*operators, typentry->eq_opr);
|
||||
*param_exprs = lappend(*param_exprs, expr);
|
||||
}
|
||||
|
||||
/* We're okay to use result cache */
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* get_resultcache_path
|
||||
* If possible, make and return a Result Cache path atop of 'inner_path'.
|
||||
* Otherwise return NULL.
|
||||
*/
|
||||
static Path *
|
||||
get_resultcache_path(PlannerInfo *root, RelOptInfo *innerrel,
|
||||
RelOptInfo *outerrel, Path *inner_path,
|
||||
Path *outer_path, JoinType jointype,
|
||||
JoinPathExtraData *extra)
|
||||
{
|
||||
List *param_exprs;
|
||||
List *hash_operators;
|
||||
ListCell *lc;
|
||||
|
||||
/* Obviously not if it's disabled */
|
||||
if (!enable_resultcache)
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* We can safely not bother with all this unless we expect to perform more
|
||||
* than one inner scan. The first scan is always going to be a cache
|
||||
* miss. This would likely fail later anyway based on costs, so this is
|
||||
* really just to save some wasted effort.
|
||||
*/
|
||||
if (outer_path->parent->rows < 2)
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* We can only have a result cache when there's some kind of cache key,
|
||||
* either parameterized path clauses or lateral Vars. No cache key sounds
|
||||
* more like something a Materialize node might be more useful for.
|
||||
*/
|
||||
if ((inner_path->param_info == NULL ||
|
||||
inner_path->param_info->ppi_clauses == NIL) &&
|
||||
innerrel->lateral_vars == NIL)
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* Currently we don't do this for SEMI and ANTI joins unless they're
|
||||
* marked as inner_unique. This is because nested loop SEMI/ANTI joins
|
||||
* don't scan the inner node to completion, which will mean result cache
|
||||
* cannot mark the cache entry as complete.
|
||||
*
|
||||
* XXX Currently we don't attempt to mark SEMI/ANTI joins as inner_unique
|
||||
* = true. Should we? See add_paths_to_joinrel()
|
||||
*/
|
||||
if (!extra->inner_unique && (jointype == JOIN_SEMI ||
|
||||
jointype == JOIN_ANTI))
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* We can't use a result cache if there are volatile functions in the
|
||||
* inner rel's target list or restrict list. A cache hit could reduce the
|
||||
* number of calls to these functions.
|
||||
*/
|
||||
if (contain_volatile_functions((Node *) innerrel->reltarget))
|
||||
return NULL;
|
||||
|
||||
foreach(lc, innerrel->baserestrictinfo)
|
||||
{
|
||||
RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
|
||||
|
||||
if (contain_volatile_functions((Node *) rinfo))
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Check if we have hash ops for each parameter to the path */
|
||||
if (paraminfo_get_equal_hashops(root,
|
||||
inner_path->param_info,
|
||||
outerrel,
|
||||
innerrel,
|
||||
¶m_exprs,
|
||||
&hash_operators))
|
||||
{
|
||||
return (Path *) create_resultcache_path(root,
|
||||
innerrel,
|
||||
inner_path,
|
||||
param_exprs,
|
||||
hash_operators,
|
||||
extra->inner_unique,
|
||||
outer_path->parent->rows);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* try_nestloop_path
|
||||
* Consider a nestloop join path; if it appears useful, push it into
|
||||
@@ -1471,6 +1656,7 @@ match_unsorted_outer(PlannerInfo *root,
|
||||
foreach(lc2, innerrel->cheapest_parameterized_paths)
|
||||
{
|
||||
Path *innerpath = (Path *) lfirst(lc2);
|
||||
Path *rcpath;
|
||||
|
||||
try_nestloop_path(root,
|
||||
joinrel,
|
||||
@@ -1479,6 +1665,22 @@ match_unsorted_outer(PlannerInfo *root,
|
||||
merge_pathkeys,
|
||||
jointype,
|
||||
extra);
|
||||
|
||||
/*
|
||||
* Try generating a result cache path and see if that makes the
|
||||
* nested loop any cheaper.
|
||||
*/
|
||||
rcpath = get_resultcache_path(root, innerrel, outerrel,
|
||||
innerpath, outerpath, jointype,
|
||||
extra);
|
||||
if (rcpath != NULL)
|
||||
try_nestloop_path(root,
|
||||
joinrel,
|
||||
outerpath,
|
||||
rcpath,
|
||||
merge_pathkeys,
|
||||
jointype,
|
||||
extra);
|
||||
}
|
||||
|
||||
/* Also consider materialized form of the cheapest inner path */
|
||||
@@ -1633,6 +1835,7 @@ consider_parallel_nestloop(PlannerInfo *root,
|
||||
foreach(lc2, innerrel->cheapest_parameterized_paths)
|
||||
{
|
||||
Path *innerpath = (Path *) lfirst(lc2);
|
||||
Path *rcpath;
|
||||
|
||||
/* Can't join to an inner path that is not parallel-safe */
|
||||
if (!innerpath->parallel_safe)
|
||||
@@ -1657,6 +1860,17 @@ consider_parallel_nestloop(PlannerInfo *root,
|
||||
|
||||
try_partial_nestloop_path(root, joinrel, outerpath, innerpath,
|
||||
pathkeys, jointype, extra);
|
||||
|
||||
/*
|
||||
* Try generating a result cache path and see if that makes the
|
||||
* nested loop any cheaper.
|
||||
*/
|
||||
rcpath = get_resultcache_path(root, innerrel, outerrel,
|
||||
innerpath, outerpath, jointype,
|
||||
extra);
|
||||
if (rcpath != NULL)
|
||||
try_partial_nestloop_path(root, joinrel, outerpath, rcpath,
|
||||
pathkeys, jointype, extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -91,6 +91,9 @@ static Result *create_group_result_plan(PlannerInfo *root,
|
||||
static ProjectSet *create_project_set_plan(PlannerInfo *root, ProjectSetPath *best_path);
|
||||
static Material *create_material_plan(PlannerInfo *root, MaterialPath *best_path,
|
||||
int flags);
|
||||
static ResultCache *create_resultcache_plan(PlannerInfo *root,
|
||||
ResultCachePath *best_path,
|
||||
int flags);
|
||||
static Plan *create_unique_plan(PlannerInfo *root, UniquePath *best_path,
|
||||
int flags);
|
||||
static Gather *create_gather_plan(PlannerInfo *root, GatherPath *best_path);
|
||||
@@ -277,6 +280,11 @@ static Sort *make_sort_from_groupcols(List *groupcls,
|
||||
AttrNumber *grpColIdx,
|
||||
Plan *lefttree);
|
||||
static Material *make_material(Plan *lefttree);
|
||||
static ResultCache *make_resultcache(Plan *lefttree, Oid *hashoperators,
|
||||
Oid *collations,
|
||||
List *param_exprs,
|
||||
bool singlerow,
|
||||
uint32 est_entries);
|
||||
static WindowAgg *make_windowagg(List *tlist, Index winref,
|
||||
int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations,
|
||||
int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations,
|
||||
@@ -453,6 +461,11 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
|
||||
(MaterialPath *) best_path,
|
||||
flags);
|
||||
break;
|
||||
case T_ResultCache:
|
||||
plan = (Plan *) create_resultcache_plan(root,
|
||||
(ResultCachePath *) best_path,
|
||||
flags);
|
||||
break;
|
||||
case T_Unique:
|
||||
if (IsA(best_path, UpperUniquePath))
|
||||
{
|
||||
@@ -1566,6 +1579,56 @@ create_material_plan(PlannerInfo *root, MaterialPath *best_path, int flags)
|
||||
return plan;
|
||||
}
|
||||
|
||||
/*
|
||||
* create_resultcache_plan
|
||||
* Create a ResultCache plan for 'best_path' and (recursively) plans
|
||||
* for its subpaths.
|
||||
*
|
||||
* Returns a Plan node.
|
||||
*/
|
||||
static ResultCache *
|
||||
create_resultcache_plan(PlannerInfo *root, ResultCachePath *best_path, int flags)
|
||||
{
|
||||
ResultCache *plan;
|
||||
Plan *subplan;
|
||||
Oid *operators;
|
||||
Oid *collations;
|
||||
List *param_exprs = NIL;
|
||||
ListCell *lc;
|
||||
ListCell *lc2;
|
||||
int nkeys;
|
||||
int i;
|
||||
|
||||
subplan = create_plan_recurse(root, best_path->subpath,
|
||||
flags | CP_SMALL_TLIST);
|
||||
|
||||
param_exprs = (List *) replace_nestloop_params(root, (Node *)
|
||||
best_path->param_exprs);
|
||||
|
||||
nkeys = list_length(param_exprs);
|
||||
Assert(nkeys > 0);
|
||||
operators = palloc(nkeys * sizeof(Oid));
|
||||
collations = palloc(nkeys * sizeof(Oid));
|
||||
|
||||
i = 0;
|
||||
forboth(lc, param_exprs, lc2, best_path->hash_operators)
|
||||
{
|
||||
Expr *param_expr = (Expr *) lfirst(lc);
|
||||
Oid opno = lfirst_oid(lc2);
|
||||
|
||||
operators[i] = opno;
|
||||
collations[i] = exprCollation((Node *) param_expr);
|
||||
i++;
|
||||
}
|
||||
|
||||
plan = make_resultcache(subplan, operators, collations, param_exprs,
|
||||
best_path->singlerow, best_path->est_entries);
|
||||
|
||||
copy_generic_path_info(&plan->plan, (Path *) best_path);
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
/*
|
||||
* create_unique_plan
|
||||
* Create a Unique plan for 'best_path' and (recursively) plans
|
||||
@@ -6452,6 +6515,28 @@ materialize_finished_plan(Plan *subplan)
|
||||
return matplan;
|
||||
}
|
||||
|
||||
static ResultCache *
|
||||
make_resultcache(Plan *lefttree, Oid *hashoperators, Oid *collations,
|
||||
List *param_exprs, bool singlerow, uint32 est_entries)
|
||||
{
|
||||
ResultCache *node = makeNode(ResultCache);
|
||||
Plan *plan = &node->plan;
|
||||
|
||||
plan->targetlist = lefttree->targetlist;
|
||||
plan->qual = NIL;
|
||||
plan->lefttree = lefttree;
|
||||
plan->righttree = NULL;
|
||||
|
||||
node->numKeys = list_length(param_exprs);
|
||||
node->hashOperators = hashoperators;
|
||||
node->collations = collations;
|
||||
node->param_exprs = param_exprs;
|
||||
node->singlerow = singlerow;
|
||||
node->est_entries = est_entries;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
Agg *
|
||||
make_agg(List *tlist, List *qual,
|
||||
AggStrategy aggstrategy, AggSplit aggsplit,
|
||||
@@ -7038,6 +7123,7 @@ is_projection_capable_path(Path *path)
|
||||
{
|
||||
case T_Hash:
|
||||
case T_Material:
|
||||
case T_ResultCache:
|
||||
case T_Sort:
|
||||
case T_IncrementalSort:
|
||||
case T_Unique:
|
||||
@@ -7083,6 +7169,7 @@ is_projection_capable_plan(Plan *plan)
|
||||
{
|
||||
case T_Hash:
|
||||
case T_Material:
|
||||
case T_ResultCache:
|
||||
case T_Sort:
|
||||
case T_Unique:
|
||||
case T_SetOp:
|
||||
|
@@ -33,6 +33,7 @@
|
||||
#include "parser/analyze.h"
|
||||
#include "rewrite/rewriteManip.h"
|
||||
#include "utils/lsyscache.h"
|
||||
#include "utils/typcache.h"
|
||||
|
||||
/* These parameters are set by GUC */
|
||||
int from_collapse_limit;
|
||||
@@ -77,6 +78,7 @@ static bool check_equivalence_delay(PlannerInfo *root,
|
||||
static bool check_redundant_nullability_qual(PlannerInfo *root, Node *clause);
|
||||
static void check_mergejoinable(RestrictInfo *restrictinfo);
|
||||
static void check_hashjoinable(RestrictInfo *restrictinfo);
|
||||
static void check_resultcacheable(RestrictInfo *restrictinfo);
|
||||
|
||||
|
||||
/*****************************************************************************
|
||||
@@ -2208,6 +2210,13 @@ distribute_restrictinfo_to_rels(PlannerInfo *root,
|
||||
*/
|
||||
check_hashjoinable(restrictinfo);
|
||||
|
||||
/*
|
||||
* Likewise, check if the clause is suitable to be used with a
|
||||
* Result Cache node to cache inner tuples during a parameterized
|
||||
* nested loop.
|
||||
*/
|
||||
check_resultcacheable(restrictinfo);
|
||||
|
||||
/*
|
||||
* Add clause to the join lists of all the relevant relations.
|
||||
*/
|
||||
@@ -2450,6 +2459,7 @@ build_implied_join_equality(PlannerInfo *root,
|
||||
/* Set mergejoinability/hashjoinability flags */
|
||||
check_mergejoinable(restrictinfo);
|
||||
check_hashjoinable(restrictinfo);
|
||||
check_resultcacheable(restrictinfo);
|
||||
|
||||
return restrictinfo;
|
||||
}
|
||||
@@ -2697,3 +2707,34 @@ check_hashjoinable(RestrictInfo *restrictinfo)
|
||||
!contain_volatile_functions((Node *) restrictinfo))
|
||||
restrictinfo->hashjoinoperator = opno;
|
||||
}
|
||||
|
||||
/*
|
||||
* check_resultcacheable
|
||||
* If the restrictinfo's clause is suitable to be used for a Result Cache
|
||||
* node, set the hasheqoperator to the hash equality operator that will be
|
||||
* needed during caching.
|
||||
*/
|
||||
static void
|
||||
check_resultcacheable(RestrictInfo *restrictinfo)
|
||||
{
|
||||
TypeCacheEntry *typentry;
|
||||
Expr *clause = restrictinfo->clause;
|
||||
Node *leftarg;
|
||||
|
||||
if (restrictinfo->pseudoconstant)
|
||||
return;
|
||||
if (!is_opclause(clause))
|
||||
return;
|
||||
if (list_length(((OpExpr *) clause)->args) != 2)
|
||||
return;
|
||||
|
||||
leftarg = linitial(((OpExpr *) clause)->args);
|
||||
|
||||
typentry = lookup_type_cache(exprType(leftarg), TYPECACHE_HASH_PROC |
|
||||
TYPECACHE_EQ_OPR);
|
||||
|
||||
if (!OidIsValid(typentry->hash_proc) || !OidIsValid(typentry->eq_opr))
|
||||
return;
|
||||
|
||||
restrictinfo->hasheqoperator = typentry->eq_opr;
|
||||
}
|
||||
|
@@ -752,6 +752,15 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
|
||||
set_hash_references(root, plan, rtoffset);
|
||||
break;
|
||||
|
||||
case T_ResultCache:
|
||||
{
|
||||
ResultCache *rcplan = (ResultCache *) plan;
|
||||
rcplan->param_exprs = fix_scan_list(root, rcplan->param_exprs,
|
||||
rtoffset,
|
||||
NUM_EXEC_TLIST(plan));
|
||||
break;
|
||||
}
|
||||
|
||||
case T_Material:
|
||||
case T_Sort:
|
||||
case T_IncrementalSort:
|
||||
|
@@ -2745,6 +2745,11 @@ finalize_plan(PlannerInfo *root, Plan *plan,
|
||||
/* rescan_param does *not* get added to scan_params */
|
||||
break;
|
||||
|
||||
case T_ResultCache:
|
||||
finalize_primnode((Node *) ((ResultCache *) plan)->param_exprs,
|
||||
&context);
|
||||
break;
|
||||
|
||||
case T_ProjectSet:
|
||||
case T_Hash:
|
||||
case T_Material:
|
||||
|
@@ -1576,6 +1576,56 @@ create_material_path(RelOptInfo *rel, Path *subpath)
|
||||
return pathnode;
|
||||
}
|
||||
|
||||
/*
|
||||
* create_resultcache_path
|
||||
* Creates a path corresponding to a ResultCache plan, returning the
|
||||
* pathnode.
|
||||
*/
|
||||
ResultCachePath *
|
||||
create_resultcache_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
|
||||
List *param_exprs, List *hash_operators,
|
||||
bool singlerow, double calls)
|
||||
{
|
||||
ResultCachePath *pathnode = makeNode(ResultCachePath);
|
||||
|
||||
Assert(subpath->parent == rel);
|
||||
|
||||
pathnode->path.pathtype = T_ResultCache;
|
||||
pathnode->path.parent = rel;
|
||||
pathnode->path.pathtarget = rel->reltarget;
|
||||
pathnode->path.param_info = subpath->param_info;
|
||||
pathnode->path.parallel_aware = false;
|
||||
pathnode->path.parallel_safe = rel->consider_parallel &&
|
||||
subpath->parallel_safe;
|
||||
pathnode->path.parallel_workers = subpath->parallel_workers;
|
||||
pathnode->path.pathkeys = subpath->pathkeys;
|
||||
|
||||
pathnode->subpath = subpath;
|
||||
pathnode->hash_operators = hash_operators;
|
||||
pathnode->param_exprs = param_exprs;
|
||||
pathnode->singlerow = singlerow;
|
||||
pathnode->calls = calls;
|
||||
|
||||
/*
|
||||
* For now we set est_entries to 0. cost_resultcache_rescan() does all
|
||||
* the hard work to determine how many cache entries there are likely to
|
||||
* be, so it seems best to leave it up to that function to fill this field
|
||||
* in. If left at 0, the executor will make a guess at a good value.
|
||||
*/
|
||||
pathnode->est_entries = 0;
|
||||
|
||||
/*
|
||||
* Add a small additional charge for caching the first entry. All the
|
||||
* harder calculations for rescans are performed in
|
||||
* cost_resultcache_rescan().
|
||||
*/
|
||||
pathnode->path.startup_cost = subpath->startup_cost + cpu_tuple_cost;
|
||||
pathnode->path.total_cost = subpath->total_cost + cpu_tuple_cost;
|
||||
pathnode->path.rows = subpath->rows;
|
||||
|
||||
return pathnode;
|
||||
}
|
||||
|
||||
/*
|
||||
* create_unique_path
|
||||
* Creates a path representing elimination of distinct rows from the
|
||||
@@ -3869,6 +3919,17 @@ reparameterize_path(PlannerInfo *root, Path *path,
|
||||
apath->path.parallel_aware,
|
||||
-1);
|
||||
}
|
||||
case T_ResultCache:
|
||||
{
|
||||
ResultCachePath *rcpath = (ResultCachePath *) path;
|
||||
|
||||
return (Path *) create_resultcache_path(root, rel,
|
||||
rcpath->subpath,
|
||||
rcpath->param_exprs,
|
||||
rcpath->hash_operators,
|
||||
rcpath->singlerow,
|
||||
rcpath->calls);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -4087,6 +4148,16 @@ do { \
|
||||
}
|
||||
break;
|
||||
|
||||
case T_ResultCachePath:
|
||||
{
|
||||
ResultCachePath *rcpath;
|
||||
|
||||
FLAT_COPY_PATH(rcpath, path, ResultCachePath);
|
||||
REPARAMETERIZE_CHILD_PATH(rcpath->subpath);
|
||||
new_path = (Path *) rcpath;
|
||||
}
|
||||
break;
|
||||
|
||||
case T_GatherPath:
|
||||
{
|
||||
GatherPath *gpath;
|
||||
|
@@ -217,6 +217,8 @@ make_restrictinfo_internal(PlannerInfo *root,
|
||||
restrictinfo->left_mcvfreq = -1;
|
||||
restrictinfo->right_mcvfreq = -1;
|
||||
|
||||
restrictinfo->hasheqoperator = InvalidOid;
|
||||
|
||||
return restrictinfo;
|
||||
}
|
||||
|
||||
@@ -366,6 +368,7 @@ commute_restrictinfo(RestrictInfo *rinfo, Oid comm_op)
|
||||
result->right_bucketsize = rinfo->left_bucketsize;
|
||||
result->left_mcvfreq = rinfo->right_mcvfreq;
|
||||
result->right_mcvfreq = rinfo->left_mcvfreq;
|
||||
result->hasheqoperator = InvalidOid;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
Reference in New Issue
Block a user