mirror of
https://github.com/postgres/postgres.git
synced 2025-07-07 00:36:50 +03:00
Add hash_mem_multiplier GUC.
Add a GUC that acts as a multiplier on work_mem. It gets applied when
sizing executor node hash tables that were previously size constrained
using work_mem alone.
The new GUC can be used to preferentially give hash-based nodes more
memory than the generic work_mem limit. It is intended to enable admin
tuning of the executor's memory usage. Overall system throughput and
system responsiveness can be improved by giving hash-based executor
nodes more memory (especially over sort-based alternatives, which are
often much less sensitive to being memory constrained).
The default value for hash_mem_multiplier is 1.0, which is also the
minimum valid value. This means that hash-based nodes continue to apply
work_mem in the traditional way by default.
hash_mem_multiplier is generally useful. However, it is being added now
due to concerns about hash aggregate performance stability for users
that upgrade to Postgres 13 (which added disk-based hash aggregation in
commit 1f39bce0
). While the old hash aggregate behavior risked
out-of-memory errors, it is nevertheless likely that many users actually
benefited. Hash agg's previous indifference to work_mem during query
execution was not just faster; it also accidentally made aggregation
resilient to grouping estimate problems (at least in cases where this
didn't create destabilizing memory pressure).
hash_mem_multiplier can provide a certain kind of continuity with the
behavior of Postgres 12 hash aggregates in cases where the planner
incorrectly estimates that all groups (plus related allocations) will
fit in work_mem/hash_mem. This seems necessary because hash-based
aggregation is usually much slower when only a small fraction of all
groups can fit. Even when it isn't possible to totally avoid hash
aggregates that spill, giving hash aggregation more memory will reliably
improve performance (the same cannot be said for external sort
operations, which appear to be almost unaffected by memory availability
provided it's at least possible to get a single merge pass).
The PostgreSQL 13 release notes should advise users that increasing
hash_mem_multiplier can help with performance regressions associated
with hash aggregation. That can be taken care of by a later commit.
Author: Peter Geoghegan
Reviewed-By: Álvaro Herrera, Jeff Davis
Discussion: https://postgr.es/m/20200625203629.7m6yvut7eqblgmfo@alap3.anarazel.de
Discussion: https://postgr.es/m/CAH2-WzmD%2Bi1pG6rc1%2BCjc4V6EaFJ_qSuKCCHVnH%3DoruqD-zqow%40mail.gmail.com
Backpatch: 13-, where disk-based hash aggregation was introduced.
This commit is contained in:
@ -3525,7 +3525,7 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
|
||||
* Get hash table size that executor would use for inner relation.
|
||||
*
|
||||
* XXX for the moment, always assume that skew optimization will be
|
||||
* performed. As long as SKEW_WORK_MEM_PERCENT is small, it's not worth
|
||||
* performed. As long as SKEW_HASH_MEM_PERCENT is small, it's not worth
|
||||
* trying to determine that for sure.
|
||||
*
|
||||
* XXX at some point it might be interesting to try to account for skew
|
||||
@ -3534,7 +3534,7 @@ initial_cost_hashjoin(PlannerInfo *root, JoinCostWorkspace *workspace,
|
||||
ExecChooseHashTableSize(inner_path_rows_total,
|
||||
inner_path->pathtarget->width,
|
||||
true, /* useskew */
|
||||
parallel_hash, /* try_combined_work_mem */
|
||||
parallel_hash, /* try_combined_hash_mem */
|
||||
outer_path->parallel_workers,
|
||||
&space_allowed,
|
||||
&numbuckets,
|
||||
@ -3597,6 +3597,7 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
|
||||
Cost run_cost = workspace->run_cost;
|
||||
int numbuckets = workspace->numbuckets;
|
||||
int numbatches = workspace->numbatches;
|
||||
int hash_mem;
|
||||
Cost cpu_per_tuple;
|
||||
QualCost hash_qual_cost;
|
||||
QualCost qp_qual_cost;
|
||||
@ -3715,16 +3716,17 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path,
|
||||
}
|
||||
|
||||
/*
|
||||
* If the bucket holding the inner MCV would exceed work_mem, we don't
|
||||
* If the bucket holding the inner MCV would exceed hash_mem, we don't
|
||||
* want to hash unless there is really no other alternative, so apply
|
||||
* disable_cost. (The executor normally copes with excessive memory usage
|
||||
* by splitting batches, but obviously it cannot separate equal values
|
||||
* that way, so it will be unable to drive the batch size below work_mem
|
||||
* that way, so it will be unable to drive the batch size below hash_mem
|
||||
* when this is true.)
|
||||
*/
|
||||
hash_mem = get_hash_mem();
|
||||
if (relation_byte_size(clamp_row_est(inner_path_rows * innermcvfreq),
|
||||
inner_path->pathtarget->width) >
|
||||
(work_mem * 1024L))
|
||||
(hash_mem * 1024L))
|
||||
startup_cost += disable_cost;
|
||||
|
||||
/*
|
||||
|
@ -4196,16 +4196,17 @@ consider_groupingsets_paths(PlannerInfo *root,
|
||||
double dNumGroups)
|
||||
{
|
||||
Query *parse = root->parse;
|
||||
int hash_mem = get_hash_mem();
|
||||
|
||||
/*
|
||||
* If we're not being offered sorted input, then only consider plans that
|
||||
* can be done entirely by hashing.
|
||||
*
|
||||
* We can hash everything if it looks like it'll fit in work_mem. But if
|
||||
* We can hash everything if it looks like it'll fit in hash_mem. But if
|
||||
* the input is actually sorted despite not being advertised as such, we
|
||||
* prefer to make use of that in order to use less memory.
|
||||
*
|
||||
* If none of the grouping sets are sortable, then ignore the work_mem
|
||||
* If none of the grouping sets are sortable, then ignore the hash_mem
|
||||
* limit and generate a path anyway, since otherwise we'll just fail.
|
||||
*/
|
||||
if (!is_sorted)
|
||||
@ -4257,10 +4258,10 @@ consider_groupingsets_paths(PlannerInfo *root,
|
||||
|
||||
/*
|
||||
* gd->rollups is empty if we have only unsortable columns to work
|
||||
* with. Override work_mem in that case; otherwise, we'll rely on the
|
||||
* with. Override hash_mem in that case; otherwise, we'll rely on the
|
||||
* sorted-input case to generate usable mixed paths.
|
||||
*/
|
||||
if (hashsize > work_mem * 1024L && gd->rollups)
|
||||
if (hashsize > hash_mem * 1024L && gd->rollups)
|
||||
return; /* nope, won't fit */
|
||||
|
||||
/*
|
||||
@ -4379,7 +4380,7 @@ consider_groupingsets_paths(PlannerInfo *root,
|
||||
{
|
||||
List *rollups = NIL;
|
||||
List *hash_sets = list_copy(gd->unsortable_sets);
|
||||
double availspace = (work_mem * 1024.0);
|
||||
double availspace = (hash_mem * 1024.0);
|
||||
ListCell *lc;
|
||||
|
||||
/*
|
||||
@ -4400,7 +4401,7 @@ consider_groupingsets_paths(PlannerInfo *root,
|
||||
|
||||
/*
|
||||
* We treat this as a knapsack problem: the knapsack capacity
|
||||
* represents work_mem, the item weights are the estimated memory
|
||||
* represents hash_mem, the item weights are the estimated memory
|
||||
* usage of the hashtables needed to implement a single rollup,
|
||||
* and we really ought to use the cost saving as the item value;
|
||||
* however, currently the costs assigned to sort nodes don't
|
||||
@ -4441,7 +4442,7 @@ consider_groupingsets_paths(PlannerInfo *root,
|
||||
rollup->numGroups);
|
||||
|
||||
/*
|
||||
* If sz is enormous, but work_mem (and hence scale) is
|
||||
* If sz is enormous, but hash_mem (and hence scale) is
|
||||
* small, avoid integer overflow here.
|
||||
*/
|
||||
k_weights[i] = (int) Min(floor(sz / scale),
|
||||
|
@ -200,7 +200,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
|
||||
* XXX If an ANY subplan is uncorrelated, build_subplan may decide to hash
|
||||
* its output. In that case it would've been better to specify full
|
||||
* retrieval. At present, however, we can only check hashability after
|
||||
* we've made the subplan :-(. (Determining whether it'll fit in work_mem
|
||||
* we've made the subplan :-(. (Determining whether it'll fit in hash_mem
|
||||
* is the really hard part.) Therefore, we don't want to be too
|
||||
* optimistic about the percentage of tuples retrieved, for fear of
|
||||
* selecting a plan that's bad for the materialization case.
|
||||
@ -278,7 +278,7 @@ make_subplan(PlannerInfo *root, Query *orig_subquery,
|
||||
|
||||
plan = create_plan(subroot, best_path);
|
||||
|
||||
/* Now we can check if it'll fit in work_mem */
|
||||
/* Now we can check if it'll fit in hash_mem */
|
||||
/* XXX can we check this at the Path stage? */
|
||||
if (subplan_is_hashable(plan))
|
||||
{
|
||||
@ -716,16 +716,17 @@ static bool
|
||||
subplan_is_hashable(Plan *plan)
|
||||
{
|
||||
double subquery_size;
|
||||
int hash_mem = get_hash_mem();
|
||||
|
||||
/*
|
||||
* The estimated size of the subquery result must fit in work_mem. (Note:
|
||||
* The estimated size of the subquery result must fit in hash_mem. (Note:
|
||||
* we use heap tuple overhead here even though the tuples will actually be
|
||||
* stored as MinimalTuples; this provides some fudge factor for hashtable
|
||||
* overhead.)
|
||||
*/
|
||||
subquery_size = plan->plan_rows *
|
||||
(MAXALIGN(plan->plan_width) + MAXALIGN(SizeofHeapTupleHeader));
|
||||
if (subquery_size > work_mem * 1024L)
|
||||
if (subquery_size > hash_mem * 1024L)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
@ -1018,6 +1018,7 @@ choose_hashed_setop(PlannerInfo *root, List *groupClauses,
|
||||
const char *construct)
|
||||
{
|
||||
int numGroupCols = list_length(groupClauses);
|
||||
int hash_mem = get_hash_mem();
|
||||
bool can_sort;
|
||||
bool can_hash;
|
||||
Size hashentrysize;
|
||||
@ -1049,15 +1050,17 @@ choose_hashed_setop(PlannerInfo *root, List *groupClauses,
|
||||
|
||||
/*
|
||||
* Don't do it if it doesn't look like the hashtable will fit into
|
||||
* work_mem.
|
||||
* hash_mem.
|
||||
*/
|
||||
hashentrysize = MAXALIGN(input_path->pathtarget->width) + MAXALIGN(SizeofMinimalTupleHeader);
|
||||
|
||||
if (hashentrysize * dNumGroups > work_mem * 1024L)
|
||||
if (hashentrysize * dNumGroups > hash_mem * 1024L)
|
||||
return false;
|
||||
|
||||
/*
|
||||
* See if the estimated cost is no more than doing it the other way.
|
||||
* See if the estimated cost is no more than doing it the other way. We
|
||||
* deliberately give the hash case more memory when hash_mem exceeds
|
||||
* standard work mem (i.e. when hash_mem_multiplier exceeds 1.0).
|
||||
*
|
||||
* We need to consider input_plan + hashagg versus input_plan + sort +
|
||||
* group. Note that the actual result plan might involve a SetOp or
|
||||
|
@ -1720,8 +1720,9 @@ create_unique_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath,
|
||||
* planner.c).
|
||||
*/
|
||||
int hashentrysize = subpath->pathtarget->width + 64;
|
||||
int hash_mem = get_hash_mem();
|
||||
|
||||
if (hashentrysize * pathnode->path.rows > work_mem * 1024L)
|
||||
if (hashentrysize * pathnode->path.rows > hash_mem * 1024L)
|
||||
{
|
||||
/*
|
||||
* We should not try to hash. Hack the SpecialJoinInfo to
|
||||
|
Reference in New Issue
Block a user