From 963c206ae335371a79d8cf552225b830eb353893 Mon Sep 17 00:00:00 2001 From: William Bernbaum Date: Tue, 1 Sep 2026 01:06:44 -0700 Subject: [PATCH] Price hashed aggregation for hash tables that exceed CPU cache capacity cost_agg() charges cpu_operator_cost per grouping column for hashing each input tuple, but does not charge anything for the corresponding table probe. Measured cost per input tuple grows with hash table size, because an increasing fraction of probes miss the CPU cache. Add effective_cpu_cache_size and charge each input tuple per doubling of the resident hash table. --- doc/src/sgml/config.sgml | 29 ++++++++++++ src/backend/optimizer/path/costsize.c | 45 +++++++++++++++++++ src/backend/utils/misc/guc_parameters.dat | 10 +++++ src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/optimizer/cost.h | 1 + src/include/optimizer/optimizer.h | 1 + 6 files changed, 87 insertions(+) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 0165eb9ec02..c79cb536da5 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -6542,6 +6542,35 @@ ANY num_sync ( + effective_cpu_cache_size (integer) + + effective_cpu_cache_size configuration parameter + + + + + Sets the planner's assumption about how much of a hash table stays in + the CPU's data cache. Hashed aggregation is charged + for the additional memory traffic of a hash table larger than this, so + a higher value makes hashed aggregation more likely to be chosen over + sorted aggregation for queries with many groups, and a lower value less + likely. Setting it to 0 removes the extra cost + entirely. + + + + Set this to the cache available to a single query, which is + smaller than the machine's total last-level cache when several queries + or parallel workers run concurrently, each building its own hash table. + This parameter is used only for estimation purposes; it neither + reserves cache nor limits the size of any hash table. + If this value is specified without units, it is taken as kilobytes. + The default is 8 megabytes (8MB). + + + + min_eager_agg_group_size (floating point) diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 7bbddb8bee4..9d70c805c74 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -120,6 +120,13 @@ */ #define APPEND_CPU_COST_MULTIPLIER 0.5 +/* + * Extra cpu_tuple_cost charged per input tuple for each doubling of a hash + * table beyond effective_cpu_cache_size, covering the probes that miss CPU + * cache. + */ +#define HASH_RESIDENCY_PENALTY 4.0 + /* * Maximum value for row estimates. We cap row estimates to this to help * ensure that costs based on these estimates remain within the range of what @@ -138,6 +145,7 @@ double parallel_setup_cost = DEFAULT_PARALLEL_SETUP_COST; double recursive_worktable_factor = DEFAULT_RECURSIVE_WORKTABLE_FACTOR; int effective_cache_size = DEFAULT_EFFECTIVE_CACHE_SIZE; +int effective_cpu_cache_size = DEFAULT_EFFECTIVE_CPU_CACHE_SIZE; Cost disable_cost = 1.0e10; @@ -2806,6 +2814,7 @@ cost_agg(Path *path, PlannerInfo *root, double output_tuples; Cost startup_cost; Cost total_cost; + Cost hash_entry_cost; const AggClauseCosts dummy_aggcosts = {0}; /* Use all-zero per-aggregate costs if NULL is passed */ @@ -2815,6 +2824,36 @@ cost_agg(Path *path, PlannerInfo *root, aggcosts = &dummy_aggcosts; } + /* Only the hashed strategies probe a hash table. */ + hash_entry_cost = 0; + if ((aggstrategy == AGG_HASHED || aggstrategy == AGG_MIXED) && + effective_cpu_cache_size > 0) + { + double entrysize; + double table_bytes; + double cache_bytes = effective_cpu_cache_size * 1024.0; + Size mem_limit; + uint64 ngroups_limit; + int num_partitions; + + entrysize = hash_agg_entry_size(list_length(root->aggtransinfos), + input_width, + aggcosts->transitionSpace); + + /* + * The disk costs below already charge for spilling past the + * in-memory limit. Clamping bounds the cost when numGroups is + * overestimated. + */ + hash_agg_set_limits(entrysize, numGroups, 0, &mem_limit, + &ngroups_limit, &num_partitions); + table_bytes = Min(entrysize * numGroups, (double) mem_limit); + + if (table_bytes > cache_bytes) + hash_entry_cost = cpu_tuple_cost * HASH_RESIDENCY_PENALTY + * LOG2(table_bytes / cache_bytes) * input_tuples; + } + /* * The transCost.per_tuple component of aggcosts should be charged once * per input tuple, corresponding to the costs of evaluating the aggregate @@ -2828,6 +2867,10 @@ cost_agg(Path *path, PlannerInfo *root, * We will produce a single output tuple if not grouping, and a tuple per * group otherwise. We charge cpu_tuple_cost for each output tuple. * + * AGG_HASHED and AGG_MIXED additionally pay hash_entry_cost per input + * tuple for the memory traffic of building a hash table too large to stay + * in cache. This cost is zero for a table that fits. + * * Note: in this cost model, AGG_SORTED and AGG_HASHED have exactly the * same total CPU cost, but AGG_SORTED has lower startup cost. If the * input path is already sorted appropriately, AGG_SORTED should be @@ -2859,6 +2902,7 @@ cost_agg(Path *path, PlannerInfo *root, total_cost += aggcosts->transCost.startup; total_cost += aggcosts->transCost.per_tuple * input_tuples; total_cost += (cpu_operator_cost * numGroupCols) * input_tuples; + total_cost += hash_entry_cost; total_cost += aggcosts->finalCost.startup; total_cost += aggcosts->finalCost.per_tuple * numGroups; total_cost += cpu_tuple_cost * numGroups; @@ -2892,6 +2936,7 @@ cost_agg(Path *path, PlannerInfo *root, startup_cost += aggcosts->transCost.per_tuple * input_tuples; /* cost of computing hash value */ startup_cost += (cpu_operator_cost * numGroupCols) * input_tuples; + startup_cost += hash_entry_cost; startup_cost += aggcosts->finalCost.startup; total_cost = startup_cost; diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index 3c5e16ad1e7..864dc10bba2 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -844,6 +844,16 @@ max => 'INT_MAX', }, +{ name => 'effective_cpu_cache_size', type => 'int', context => 'PGC_USERSET', group => 'QUERY_TUNING_COST', + short_desc => 'Sets the planner\'s assumption about the size of the CPU data cache.', + long_desc => 'Hash tables larger than this are charged for the additional memory traffic of probes that miss cache. 0 removes the extra cost.', + flags => 'GUC_UNIT_KB | GUC_EXPLAIN', + variable => 'effective_cpu_cache_size', + boot_val => 'DEFAULT_EFFECTIVE_CPU_CACHE_SIZE', + min => '0', + max => 'MAX_KILOBYTES', +}, + { name => 'effective_io_concurrency', type => 'int', context => 'PGC_USERSET', group => 'RESOURCES_IO', short_desc => 'Number of simultaneous requests that can be handled efficiently by the disk subsystem.', long_desc => '0 disables simultaneous requests.', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index e759f06b50f..720c5b016e5 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -465,6 +465,7 @@ #min_parallel_table_scan_size = 8MB #min_parallel_index_scan_size = 512kB #effective_cache_size = 4GB +#effective_cpu_cache_size = 8MB #min_eager_agg_group_size = 8.0 #jit_above_cost = 100000 # perform JIT compilation if available diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h index 14255c900fc..b047eb732f5 100644 --- a/src/include/optimizer/cost.h +++ b/src/include/optimizer/cost.h @@ -32,6 +32,7 @@ /* defaults for non-Cost parameters */ #define DEFAULT_RECURSIVE_WORKTABLE_FACTOR 10.0 #define DEFAULT_EFFECTIVE_CACHE_SIZE 524288 /* measured in pages */ +#define DEFAULT_EFFECTIVE_CPU_CACHE_SIZE 8192 /* measured in kilobytes */ typedef enum { diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h index cb6241e2bdd..7cf2948ad51 100644 --- a/src/include/optimizer/optimizer.h +++ b/src/include/optimizer/optimizer.h @@ -79,6 +79,7 @@ extern PGDLLIMPORT double parallel_tuple_cost; extern PGDLLIMPORT double parallel_setup_cost; extern PGDLLIMPORT double recursive_worktable_factor; extern PGDLLIMPORT int effective_cache_size; +extern PGDLLIMPORT int effective_cpu_cache_size; extern double clamp_row_est(double nrows); extern int32 clamp_width_est(int64 tuple_width);