pg_overexplain \
pg_plan_advice \
pg_prewarm \
+ pg_stash_advice \
pg_stat_statements \
pg_surgery \
pg_trgm \
subdir('pg_plan_advice')
subdir('pg_prewarm')
subdir('pgrowlocks')
+subdir('pg_stash_advice')
subdir('pg_stat_statements')
subdir('pgstattuple')
subdir('pg_surgery')
--- /dev/null
+# contrib/pg_stash_advice/Makefile
+
+MODULE_big = pg_stash_advice
+OBJS = \
+ $(WIN32RES) \
+ pg_stash_advice.o \
+ stashfuncs.o
+
+EXTENSION = pg_stash_advice
+DATA = pg_stash_advice--1.0.sql
+PGFILEDESC = "pg_stash_advice - store and automatically apply plan advice"
+
+REGRESS = pg_stash_advice pg_stash_advice_utf8
+EXTRA_INSTALL = contrib/pg_plan_advice
+
+ifdef USE_PGXS
+PG_CPPFLAGS = -I$(includedir_server)/extension
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+PG_CPPFLAGS = -I$(top_srcdir)/contrib/pg_plan_advice
+subdir = contrib/pg_stash_advice
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
--- /dev/null
+CREATE EXTENSION pg_stash_advice;
+SET compute_query_id = on;
+SET max_parallel_workers_per_gather = 0;
+-- Helper: extract query identifier from EXPLAIN VERBOSE output.
+CREATE OR REPLACE FUNCTION get_query_id(query_text text) RETURNS bigint
+LANGUAGE plpgsql AS $$
+DECLARE
+ line text;
+ qid bigint;
+BEGIN
+ FOR line IN EXECUTE 'EXPLAIN (VERBOSE, FORMAT TEXT) ' || query_text
+ LOOP
+ IF line ~ 'Query Identifier:' THEN
+ qid := regexp_replace(line, '.*Query Identifier:\s*(-?\d+).*', '\1')::bigint;
+ RETURN qid;
+ END IF;
+ END LOOP;
+ RAISE EXCEPTION 'Query Identifier not found in EXPLAIN output';
+END;
+$$;
+CREATE TABLE aa_dim1 (id integer primary key, dim1 text, val1 int)
+ WITH (autovacuum_enabled = false);
+INSERT INTO aa_dim1 (id, dim1, val1)
+ SELECT g, 'some filler text ' || g, (g % 3) + 1
+ FROM generate_series(1,100) g;
+VACUUM ANALYZE aa_dim1;
+CREATE TABLE aa_dim2 (id integer primary key, dim2 text, val2 int)
+ WITH (autovacuum_enabled = false);
+INSERT INTO aa_dim2 (id, dim2, val2)
+ SELECT g, 'some filler text ' || g, (g % 7) + 1
+ FROM generate_series(1,1000) g;
+VACUUM ANALYZE aa_dim2;
+CREATE TABLE aa_fact (
+ id int primary key,
+ dim1_id integer not null references aa_dim1 (id),
+ dim2_id integer not null references aa_dim2 (id)
+) WITH (autovacuum_enabled = false);
+INSERT INTO aa_fact
+ SELECT g, (g%100)+1, (g%100)+1 FROM generate_series(1,100000) g;
+VACUUM ANALYZE aa_fact;
+-- Get the query identifier.
+SELECT get_query_id($$
+SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+$$) AS qid \gset
+-- Create an advice stash and point pg_stash_advice at it.
+SELECT pg_create_advice_stash('regress_stash');
+ pg_create_advice_stash
+------------------------
+
+(1 row)
+
+SET pg_stash_advice.stash_name = 'regress_stash';
+-- Run our test query for the first time with no stashed advice.
+EXPLAIN (COSTS OFF)
+SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+ QUERY PLAN
+------------------------------------------
+ Hash Join
+ Hash Cond: (f.dim1_id = d1.id)
+ -> Hash Join
+ Hash Cond: (f.dim2_id = d2.id)
+ -> Seq Scan on aa_fact f
+ -> Hash
+ -> Seq Scan on aa_dim2 d2
+ Filter: (val2 = 1)
+ -> Hash
+ -> Seq Scan on aa_dim1 d1
+ Filter: (val1 = 1)
+(11 rows)
+
+-- Force an index scan on dim1
+SELECT pg_set_stashed_advice('regress_stash', :'qid',
+ 'INDEX_SCAN(d1 aa_dim1_pkey)');
+ pg_set_stashed_advice
+-----------------------
+
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+ QUERY PLAN
+---------------------------------------------------------
+ Hash Join
+ Hash Cond: (f.dim1_id = d1.id)
+ -> Hash Join
+ Hash Cond: (f.dim2_id = d2.id)
+ -> Seq Scan on aa_fact f
+ -> Hash
+ -> Seq Scan on aa_dim2 d2
+ Filter: (val2 = 1)
+ -> Hash
+ -> Index Scan using aa_dim1_pkey on aa_dim1 d1
+ Filter: (val1 = 1)
+ Supplied Plan Advice:
+ INDEX_SCAN(d1 aa_dim1_pkey) /* matched */
+(13 rows)
+
+-- Force an alternative join order
+SELECT pg_set_stashed_advice('regress_stash', :'qid',
+ 'join_order(f d1 d2)');
+ pg_set_stashed_advice
+-----------------------
+
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+ QUERY PLAN
+------------------------------------------
+ Hash Join
+ Hash Cond: (f.dim2_id = d2.id)
+ -> Hash Join
+ Hash Cond: (f.dim1_id = d1.id)
+ -> Seq Scan on aa_fact f
+ -> Hash
+ -> Seq Scan on aa_dim1 d1
+ Filter: (val1 = 1)
+ -> Hash
+ -> Seq Scan on aa_dim2 d2
+ Filter: (val2 = 1)
+ Supplied Plan Advice:
+ JOIN_ORDER(f d1 d2) /* matched */
+(13 rows)
+
+-- Force an alternative join strategy
+SELECT pg_set_stashed_advice('regress_stash', :'qid',
+ 'NESTED_LOOP_PLAIN(d1)');
+ pg_set_stashed_advice
+-----------------------
+
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+ QUERY PLAN
+---------------------------------------------------
+ Nested Loop
+ -> Hash Join
+ Hash Cond: (f.dim2_id = d2.id)
+ -> Seq Scan on aa_fact f
+ -> Hash
+ -> Seq Scan on aa_dim2 d2
+ Filter: (val2 = 1)
+ -> Index Scan using aa_dim1_pkey on aa_dim1 d1
+ Index Cond: (id = f.dim1_id)
+ Filter: (val1 = 1)
+ Supplied Plan Advice:
+ NESTED_LOOP_PLAIN(d1) /* matched */
+(12 rows)
+
+-- Add a useless extra entry to our test stash. Shouldn't change the result
+-- from the previous test.
+-- (If we're unlucky enough that this ever fails due to query ID actually
+-- being 1, then just put some other constant here. Seems unlikely.)
+SELECT pg_set_stashed_advice('regress_stash', 1, 'SEQ_SCAN(d1)');
+ pg_set_stashed_advice
+-----------------------
+
+(1 row)
+
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+ QUERY PLAN
+---------------------------------------------------
+ Nested Loop
+ -> Hash Join
+ Hash Cond: (f.dim2_id = d2.id)
+ -> Seq Scan on aa_fact f
+ -> Hash
+ -> Seq Scan on aa_dim2 d2
+ Filter: (val2 = 1)
+ -> Index Scan using aa_dim1_pkey on aa_dim1 d1
+ Index Cond: (id = f.dim1_id)
+ Filter: (val1 = 1)
+ Supplied Plan Advice:
+ NESTED_LOOP_PLAIN(d1) /* matched */
+(12 rows)
+
+-- Try an empty stash to be sure it does nothing
+SELECT pg_create_advice_stash('regress_empty_stash');
+ pg_create_advice_stash
+------------------------
+
+(1 row)
+
+SET pg_stash_advice.stash_name = 'regress_empty_stash';
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+ QUERY PLAN
+------------------------------------------
+ Hash Join
+ Hash Cond: (f.dim1_id = d1.id)
+ -> Hash Join
+ Hash Cond: (f.dim2_id = d2.id)
+ -> Seq Scan on aa_fact f
+ -> Hash
+ -> Seq Scan on aa_dim2 d2
+ Filter: (val2 = 1)
+ -> Hash
+ -> Seq Scan on aa_dim1 d1
+ Filter: (val1 = 1)
+(11 rows)
+
+-- Test that we can list each stash individually and all of them together,
+-- but not a nonexistent stash.
+SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name;
+ stash_name | num_entries
+---------------------+-------------
+ regress_empty_stash | 0
+ regress_stash | 2
+(2 rows)
+
+SELECT stash_name, advice_string
+ FROM pg_get_advice_stash_contents('regress_stash') ORDER BY advice_string;
+ stash_name | advice_string
+---------------+-----------------------
+ regress_stash | NESTED_LOOP_PLAIN(d1)
+ regress_stash | SEQ_SCAN(d1)
+(2 rows)
+
+SELECT stash_name, advice_string
+ FROM pg_get_advice_stash_contents('regress_empty_stash')
+ ORDER BY advice_string;
+ stash_name | advice_string
+------------+---------------
+(0 rows)
+
+SELECT stash_name, advice_string
+ FROM pg_get_advice_stash_contents(NULL) ORDER BY advice_string;
+ stash_name | advice_string
+---------------+-----------------------
+ regress_stash | NESTED_LOOP_PLAIN(d1)
+ regress_stash | SEQ_SCAN(d1)
+(2 rows)
+
+SELECT stash_name, advice_string
+ FROM pg_get_advice_stash_contents('no_such_stash')
+ ORDER BY advice_string;
+ERROR: advice stash "no_such_stash" does not exist
+-- Test that we can remove advice.
+SELECT pg_set_stashed_advice('regress_stash', :'qid', null);
+ pg_set_stashed_advice
+-----------------------
+
+(1 row)
+
+SET pg_stash_advice.stash_name = 'regress_stash';
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+ QUERY PLAN
+------------------------------------------
+ Hash Join
+ Hash Cond: (f.dim1_id = d1.id)
+ -> Hash Join
+ Hash Cond: (f.dim2_id = d2.id)
+ -> Seq Scan on aa_fact f
+ -> Hash
+ -> Seq Scan on aa_dim2 d2
+ Filter: (val2 = 1)
+ -> Hash
+ -> Seq Scan on aa_dim1 d1
+ Filter: (val1 = 1)
+(11 rows)
+
+SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name;
+ stash_name | num_entries
+---------------------+-------------
+ regress_empty_stash | 0
+ regress_stash | 1
+(2 rows)
+
+SELECT stash_name, advice_string
+ FROM pg_get_advice_stash_contents('regress_stash') ORDER BY advice_string;
+ stash_name | advice_string
+---------------+---------------
+ regress_stash | SEQ_SCAN(d1)
+(1 row)
+
+-- Can't create a stash that already exists, or drop one that doesn't.
+SELECT pg_create_advice_stash('regress_stash');
+ERROR: advice stash "regress_stash" already exists
+SELECT pg_drop_advice_stash('no_such_stash');
+ERROR: advice stash "no_such_stash" does not exist
+-- Can't add to or remove from a stash that does not exist.
+SELECT pg_set_stashed_advice('no_such_stash', 1, 'SEQ_SCAN(t)');
+ERROR: advice stash "no_such_stash" does not exist
+SELECT pg_set_stashed_advice('no_such_stash', 1, null);
+ERROR: advice stash "no_such_stash" does not exist
+-- Can't use query ID 0.
+SELECT pg_set_stashed_advice('regress_stash', 0, 'SEQ_SCAN(t)');
+ERROR: cannot set advice string for query ID 0
+-- Stash names must be non-empty, ASCII, and not too long, and must look
+-- like identifiers.
+SELECT pg_create_advice_stash('');
+ERROR: advice stash name may not be zero length
+SELECT pg_create_advice_stash('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
+ERROR: advice stash names may not be longer than 63 bytes
+SELECT pg_create_advice_stash(' ');
+ERROR: advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores
+SET pg_stash_advice.stash_name = '99bottles';
+ERROR: invalid value for parameter "pg_stash_advice.stash_name": "99bottles"
+DETAIL: advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores
+-- Clean up state in dynamic shared memory.
+SELECT pg_drop_advice_stash('regress_stash');
+ pg_drop_advice_stash
+----------------------
+
+(1 row)
+
+SELECT pg_drop_advice_stash('regress_empty_stash');
+ pg_drop_advice_stash
+----------------------
+
+(1 row)
+
--- /dev/null
+/*
+ * This test must be run in a database with UTF-8 encoding,
+ * because other encodings don't support all the characters used.
+ */
+SELECT getdatabaseencoding() <> 'UTF8'
+ AS skip_test \gset
+\if :skip_test
+\quit
+\endif
+SET client_encoding = utf8;
+-- Non-ASCII stash names should be rejected.
+SELECT pg_create_advice_stash('café');
+ERROR: advice stash name must not contain non-ASCII characters
+SET pg_stash_advice.stash_name = 'café';
+ERROR: invalid value for parameter "pg_stash_advice.stash_name": "café"
+DETAIL: advice stash name must not contain non-ASCII characters
--- /dev/null
+/*
+ * This test must be run in a database with UTF-8 encoding,
+ * because other encodings don't support all the characters used.
+ */
+SELECT getdatabaseencoding() <> 'UTF8'
+ AS skip_test \gset
+\if :skip_test
+\quit
--- /dev/null
+# Copyright (c) 2022-2026, PostgreSQL Global Development Group
+
+pg_stash_advice_sources = files(
+ 'pg_stash_advice.c',
+ 'stashfuncs.c'
+)
+
+if host_system == 'windows'
+ pg_stash_advice_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+ '--NAME', 'pg_stash_advice',
+ '--FILEDESC', 'pg_stash_advice - store and automatically apply plan advice',])
+endif
+
+pg_stash_advice = shared_module('pg_stash_advice',
+ pg_stash_advice_sources,
+ include_directories: [pg_plan_advice_inc, include_directories('.')],
+ kwargs: contrib_mod_args,
+)
+contrib_targets += pg_stash_advice
+
+install_data(
+ 'pg_stash_advice--1.0.sql',
+ 'pg_stash_advice.control',
+ kwargs: contrib_data_args,
+)
+
+tests += {
+ 'name': 'pg_stash_advice',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'regress': {
+ 'sql': [
+ 'pg_stash_advice',
+ 'pg_stash_advice_utf8',
+ ],
+ },
+}
--- /dev/null
+/* contrib/pg_stash_advice/pg_stash_advice--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pg_stash_advice" to load this file. \quit
+
+CREATE FUNCTION pg_create_advice_stash(stash_name text)
+RETURNS void
+AS 'MODULE_PATHNAME', 'pg_create_advice_stash'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pg_drop_advice_stash(stash_name text)
+RETURNS void
+AS 'MODULE_PATHNAME', 'pg_drop_advice_stash'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pg_set_stashed_advice(stash_name text, query_id bigint,
+ advice_string text)
+RETURNS void
+AS 'MODULE_PATHNAME', 'pg_set_stashed_advice'
+LANGUAGE C;
+
+CREATE FUNCTION pg_get_advice_stashes(
+ OUT stash_name text,
+ OUT num_entries bigint
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_get_advice_stashes'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pg_get_advice_stash_contents(
+ INOUT stash_name text,
+ OUT query_id bigint,
+ OUT advice_string text
+)
+RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'pg_get_advice_stash_contents'
+LANGUAGE C;
+
+REVOKE ALL ON FUNCTION pg_create_advice_stash(text) FROM PUBLIC;
+REVOKE ALL ON FUNCTION pg_drop_advice_stash(text) FROM PUBLIC;
+REVOKE ALL ON FUNCTION pg_get_advice_stash_contents(text) FROM PUBLIC;
+REVOKE ALL ON FUNCTION pg_get_advice_stashes() FROM PUBLIC;
+REVOKE ALL ON FUNCTION pg_set_stashed_advice(text, bigint, text) FROM PUBLIC;
--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * pg_stash_advice.c
+ * core infrastructure for pg_stash_advice contrib module
+ *
+ * Copyright (c) 2016-2026, PostgreSQL Global Development Group
+ *
+ * contrib/pg_stash_advice/pg_stash_advice.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "common/hashfn.h"
+#include "common/string.h"
+#include "nodes/queryjumble.h"
+#include "pg_plan_advice.h"
+#include "pg_stash_advice.h"
+#include "storage/dsm_registry.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+
+PG_MODULE_MAGIC;
+
+/* Shared memory hash table parameters */
+static dshash_parameters pgsa_stash_dshash_parameters = {
+ NAMEDATALEN,
+ sizeof(pgsa_stash),
+ dshash_strcmp,
+ dshash_strhash,
+ dshash_strcpy,
+ LWTRANCHE_INVALID /* gets set at runtime */
+};
+
+static dshash_parameters pgsa_entry_dshash_parameters = {
+ sizeof(pgsa_entry_key),
+ sizeof(pgsa_entry),
+ dshash_memcmp,
+ dshash_memhash,
+ dshash_memcpy,
+ LWTRANCHE_INVALID /* gets set at runtime */
+};
+
+/* GUC variable */
+static char *pg_stash_advice_stash_name = "";
+
+/* Shared memory pointers */
+pgsa_shared_state *pgsa_state;
+dsa_area *pgsa_dsa_area;
+dshash_table *pgsa_stash_dshash;
+dshash_table *pgsa_entry_dshash;
+
+/* Other global variables */
+static MemoryContext pg_stash_advice_mcxt;
+
+/* Function prototypes */
+static char *pgsa_advisor(PlannerGlobal *glob,
+ Query *parse,
+ const char *query_string,
+ int cursorOptions,
+ ExplainState *es);
+static bool pgsa_check_stash_name_guc(char **newval, void **extra,
+ GucSource source);
+static void pgsa_init_shared_state(void *ptr, void *arg);
+static bool pgsa_is_identifier(char *str);
+
+/* Stash name -> stash ID hash table */
+#define SH_PREFIX pgsa_stash_name_table
+#define SH_ELEMENT_TYPE pgsa_stash_name
+#define SH_KEY_TYPE uint64
+#define SH_KEY pgsa_stash_id
+#define SH_HASH_KEY(tb, key) hash_bytes((const unsigned char *) &(key), sizeof(uint64))
+#define SH_EQUAL(tb, a, b) (a == b)
+#define SH_SCOPE extern
+#define SH_DEFINE
+#include "lib/simplehash.h"
+
+/*
+ * Initialize this module.
+ */
+void
+_PG_init(void)
+{
+ void (*add_advisor_fn) (pg_plan_advice_advisor_hook hook);
+
+ /* If compute_query_id = 'auto', we would like query IDs. */
+ EnableQueryId();
+
+ /* Define our GUCs. */
+ DefineCustomStringVariable("pg_stash_advice.stash_name",
+ "Name of the advice stash to be used in this session.",
+ NULL,
+ &pg_stash_advice_stash_name,
+ "",
+ PGC_USERSET,
+ 0,
+ pgsa_check_stash_name_guc,
+ NULL,
+ NULL);
+
+ MarkGUCPrefixReserved("pg_stash_advice");
+
+ /* Tell pg_plan_advice that we want to provide advice strings. */
+ add_advisor_fn =
+ load_external_function("pg_plan_advice", "pg_plan_advice_add_advisor",
+ true, NULL);
+ (*add_advisor_fn) (pgsa_advisor);
+}
+
+/*
+ * Get the advice string that has been configured for this query, if any,
+ * and return it. Otherwise, return NULL.
+ */
+static char *
+pgsa_advisor(PlannerGlobal *glob, Query *parse,
+ const char *query_string, int cursorOptions,
+ ExplainState *es)
+{
+ pgsa_entry_key key;
+ pgsa_entry *entry;
+ char *advice_string;
+ uint64 stash_id;
+
+ /*
+ * Exit quickly if the stash name is empty or there's no query ID.
+ */
+ if (pg_stash_advice_stash_name[0] == '\0' || parse->queryId == 0)
+ return NULL;
+
+ /* Attach to dynamic shared memory if not already done. */
+ if (unlikely(pgsa_entry_dshash == NULL))
+ pgsa_attach();
+
+ /*
+ * Translate pg_stash_advice.stash_name to an integer ID.
+ *
+ * pgsa_check_stash_name_guc() has already validated the advice stash
+ * name, so we don't need to call pgsa_check_stash_name() here.
+ */
+ stash_id = pgsa_lookup_stash_id(pg_stash_advice_stash_name);
+ if (stash_id == 0)
+ return NULL;
+
+ /*
+ * Look up the advice string for the given stash ID + query ID.
+ *
+ * If we find an advice string, we copy it into the current memory
+ * context, presumably short-lived, so that we can release the lock on the
+ * dshash entry. pg_plan_advice only needs the value to remain allocated
+ * long enough for it to be parsed, so this should be good enough.
+ */
+ memset(&key, 0, sizeof(pgsa_entry_key));
+ key.pgsa_stash_id = stash_id;
+ key.queryId = parse->queryId;
+ entry = dshash_find(pgsa_entry_dshash, &key, false);
+ if (entry == NULL)
+ return NULL;
+ if (entry->advice_string == InvalidDsaPointer)
+ advice_string = NULL;
+ else
+ advice_string = pstrdup(dsa_get_address(pgsa_dsa_area,
+ entry->advice_string));
+ dshash_release_lock(pgsa_entry_dshash, entry);
+
+ /* If we found an advice string, emit a debug message. */
+ if (advice_string != NULL)
+ elog(DEBUG2, "supplying automatic advice for stash \"%s\", query ID %" PRId64 ": %s",
+ pg_stash_advice_stash_name, key.queryId, advice_string);
+
+ return advice_string;
+}
+
+/*
+ * Attach to various structures in dynamic shared memory.
+ *
+ * This function is designed to be resilient against errors. That is, if it
+ * fails partway through, it should be possible to call it again, repeat no
+ * work already completed, and potentially succeed or at least get further if
+ * whatever caused the previous failure has been corrected.
+ */
+void
+pgsa_attach(void)
+{
+ bool found;
+ MemoryContext oldcontext;
+
+ /*
+ * Create a memory context to make sure that any control structures
+ * allocated in local memory are sufficiently persistent.
+ */
+ if (pg_stash_advice_mcxt == NULL)
+ pg_stash_advice_mcxt = AllocSetContextCreate(TopMemoryContext,
+ "pg_stash_advice",
+ ALLOCSET_DEFAULT_SIZES);
+ oldcontext = MemoryContextSwitchTo(pg_stash_advice_mcxt);
+
+ /* Attach to the fixed-size state object if not already done. */
+ if (pgsa_state == NULL)
+ pgsa_state = GetNamedDSMSegment("pg_stash_advice",
+ sizeof(pgsa_shared_state),
+ pgsa_init_shared_state,
+ &found, NULL);
+
+ /* Attach to the DSA area if not already done. */
+ if (pgsa_dsa_area == NULL)
+ {
+ dsa_handle area_handle;
+
+ LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
+ area_handle = pgsa_state->area;
+ if (area_handle == DSA_HANDLE_INVALID)
+ {
+ pgsa_dsa_area = dsa_create(pgsa_state->dsa_tranche);
+ dsa_pin(pgsa_dsa_area);
+ pgsa_state->area = dsa_get_handle(pgsa_dsa_area);
+ LWLockRelease(&pgsa_state->lock);
+ }
+ else
+ {
+ LWLockRelease(&pgsa_state->lock);
+ pgsa_dsa_area = dsa_attach(area_handle);
+ }
+ dsa_pin_mapping(pgsa_dsa_area);
+ }
+
+ /* Attach to the stash_name->stash_id hash table if not already done. */
+ if (pgsa_stash_dshash == NULL)
+ {
+ dshash_table_handle stash_handle;
+
+ LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
+ pgsa_stash_dshash_parameters.tranche_id = pgsa_state->stash_tranche;
+ stash_handle = pgsa_state->stash_hash;
+ if (stash_handle == DSHASH_HANDLE_INVALID)
+ {
+ pgsa_stash_dshash = dshash_create(pgsa_dsa_area,
+ &pgsa_stash_dshash_parameters,
+ NULL);
+ pgsa_state->stash_hash =
+ dshash_get_hash_table_handle(pgsa_stash_dshash);
+ LWLockRelease(&pgsa_state->lock);
+ }
+ else
+ {
+ LWLockRelease(&pgsa_state->lock);
+ pgsa_stash_dshash = dshash_attach(pgsa_dsa_area,
+ &pgsa_stash_dshash_parameters,
+ stash_handle, NULL);
+ }
+ }
+
+ /* Attach to the entry hash table if not already done. */
+ if (pgsa_entry_dshash == NULL)
+ {
+ dshash_table_handle entry_handle;
+
+ LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
+ pgsa_entry_dshash_parameters.tranche_id = pgsa_state->entry_tranche;
+ entry_handle = pgsa_state->entry_hash;
+ if (entry_handle == DSHASH_HANDLE_INVALID)
+ {
+ pgsa_entry_dshash = dshash_create(pgsa_dsa_area,
+ &pgsa_entry_dshash_parameters,
+ NULL);
+ pgsa_state->entry_hash =
+ dshash_get_hash_table_handle(pgsa_entry_dshash);
+ LWLockRelease(&pgsa_state->lock);
+ }
+ else
+ {
+ LWLockRelease(&pgsa_state->lock);
+ pgsa_entry_dshash = dshash_attach(pgsa_dsa_area,
+ &pgsa_entry_dshash_parameters,
+ entry_handle, NULL);
+ }
+ }
+
+ /* Restore previous memory context. */
+ MemoryContextSwitchTo(oldcontext);
+}
+
+/*
+ * Check whether an advice stash name is legal, and signal an error if not.
+ *
+ * Keep this in sync with pgsa_check_stash_name_guc, below.
+ */
+void
+pgsa_check_stash_name(char *stash_name)
+{
+ /* Reject empty advice stash name. */
+ if (stash_name[0] == '\0')
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("advice stash name may not be zero length"));
+
+ /* Reject overlong advice stash names. */
+ if (strlen(stash_name) + 1 > NAMEDATALEN)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("advice stash names may not be longer than %d bytes",
+ NAMEDATALEN - 1));
+
+ /*
+ * Reject non-ASCII advice stash names, since advice stashes are visible
+ * across all databases and the encodings of those databases might differ.
+ */
+ if (!pg_is_ascii(stash_name))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("advice stash name must not contain non-ASCII characters"));
+
+ /*
+ * Reject things that do not look like identifiers, since the ability to
+ * create an advice stash with non-printable characters or weird symbols
+ * in the name is not likely to be useful to anyone.
+ */
+ if (!pgsa_is_identifier(stash_name))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores"));
+}
+
+/*
+ * As above, but for the GUC check_hook. We allow the empty string here,
+ * though, as equivalent to disabling the feature.
+ */
+static bool
+pgsa_check_stash_name_guc(char **newval, void **extra, GucSource source)
+{
+ char *stash_name = *newval;
+
+ /* Reject overlong advice stash names. */
+ if (strlen(stash_name) + 1 > NAMEDATALEN)
+ {
+ GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
+ GUC_check_errdetail("advice stash names may not be longer than %d bytes",
+ NAMEDATALEN - 1);
+ return false;
+ }
+
+ /*
+ * Reject non-ASCII advice stash names, since advice stashes are visible
+ * across all databases and the encodings of those databases might differ.
+ */
+ if (!pg_is_ascii(stash_name))
+ {
+ GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
+ GUC_check_errdetail("advice stash name must not contain non-ASCII characters");
+ return false;
+ }
+
+ /*
+ * Reject things that do not look like identifiers, since the ability to
+ * create an advice stash with non-printable characters or weird symbols
+ * in the name is not likely to be useful to anyone.
+ */
+ if (!pgsa_is_identifier(stash_name))
+ {
+ GUC_check_errcode(ERRCODE_INVALID_PARAMETER_VALUE);
+ GUC_check_errdetail("advice stash name must begin with a letter or underscore and contain only letters, digits, and underscores");
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * Create an advice stash.
+ */
+void
+pgsa_create_stash(char *stash_name)
+{
+ pgsa_stash *stash;
+ bool found;
+
+ Assert(LWLockHeldByMeInMode(&pgsa_state->lock, LW_EXCLUSIVE));
+
+ /* Create a stash with this name, unless one already exists. */
+ stash = dshash_find_or_insert(pgsa_stash_dshash, stash_name, &found);
+ if (found)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("advice stash \"%s\" already exists", stash_name));
+ stash->pgsa_stash_id = pgsa_state->next_stash_id++;
+ dshash_release_lock(pgsa_stash_dshash, stash);
+}
+
+/*
+ * Remove any stored advice string for the given advice stash and query ID.
+ */
+void
+pgsa_clear_advice_string(char *stash_name, int64 queryId)
+{
+ pgsa_entry *entry;
+ pgsa_entry_key key;
+ uint64 stash_id;
+ dsa_pointer old_dp;
+
+ Assert(LWLockHeldByMe(&pgsa_state->lock));
+
+ /* Translate the stash name to an integer ID. */
+ if ((stash_id = pgsa_lookup_stash_id(stash_name)) == 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("advice stash \"%s\" does not exist", stash_name));
+
+ /*
+ * Look for an existing entry, and free it. But, be sure to save the
+ * pointer to the associated advice string, if any.
+ */
+ memset(&key, 0, sizeof(pgsa_entry_key));
+ key.pgsa_stash_id = stash_id;
+ key.queryId = queryId;
+ entry = dshash_find(pgsa_entry_dshash, &key, true);
+ if (entry == NULL)
+ old_dp = InvalidDsaPointer;
+ else
+ {
+ old_dp = entry->advice_string;
+ dshash_delete_entry(pgsa_entry_dshash, entry);
+ }
+
+ /* Now we free the advice string as well, if there was one. */
+ if (old_dp != InvalidDsaPointer)
+ dsa_free(pgsa_dsa_area, old_dp);
+}
+
+/*
+ * Drop an advice stash.
+ */
+void
+pgsa_drop_stash(char *stash_name)
+{
+ pgsa_entry *entry;
+ pgsa_stash *stash;
+ dshash_seq_status iterator;
+ uint64 stash_id;
+
+ Assert(LWLockHeldByMeInMode(&pgsa_state->lock, LW_EXCLUSIVE));
+
+ /* Remove the entry for this advice stash. */
+ stash = dshash_find(pgsa_stash_dshash, stash_name, true);
+ if (stash == NULL)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("advice stash \"%s\" does not exist", stash_name));
+ stash_id = stash->pgsa_stash_id;
+ dshash_delete_entry(pgsa_stash_dshash, stash);
+
+ /*
+ * Now remove all the entries. Since pgsa_state->lock must be held at
+ * least in shared mode to insert entries into pgsa_entry_dshash, it
+ * doesn't matter whether we do this before or after deleting the entry
+ * from pgsa_stash_dshash.
+ */
+ dshash_seq_init(&iterator, pgsa_entry_dshash, true);
+ while ((entry = dshash_seq_next(&iterator)) != NULL)
+ {
+ if (stash_id == entry->key.pgsa_stash_id)
+ {
+ if (entry->advice_string != InvalidDsaPointer)
+ dsa_free(pgsa_dsa_area, entry->advice_string);
+ dshash_delete_current(&iterator);
+ }
+ }
+ dshash_seq_term(&iterator);
+}
+
+/*
+ * Initialize shared state when first created.
+ */
+static void
+pgsa_init_shared_state(void *ptr, void *arg)
+{
+ pgsa_shared_state *state = (pgsa_shared_state *) ptr;
+
+ LWLockInitialize(&state->lock,
+ LWLockNewTrancheId("pg_stash_advice_lock"));
+ state->dsa_tranche = LWLockNewTrancheId("pg_stash_advice_dsa");
+ state->stash_tranche = LWLockNewTrancheId("pg_stash_advice_stash");
+ state->entry_tranche = LWLockNewTrancheId("pg_stash_advice_entry");
+ state->next_stash_id = UINT64CONST(1);
+ state->area = DSA_HANDLE_INVALID;
+ state->stash_hash = DSHASH_HANDLE_INVALID;
+ state->entry_hash = DSHASH_HANDLE_INVALID;
+}
+
+/*
+ * Check whether a string looks like a valid identifier. It must contain only
+ * ASCII identifier characters, and must not begin with a digit.
+ */
+static bool
+pgsa_is_identifier(char *str)
+{
+ if (*str >= '0' && *str <= '9')
+ return false;
+
+ while (*str != '\0')
+ {
+ char c = *str++;
+
+ if ((c < '0' || c > '9') && (c < 'a' || c > 'z') &&
+ (c < 'A' || c > 'Z') && c != '_')
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * Look up the integer ID that corresponds to the given stash name.
+ *
+ * Returns 0 if no such stash exists.
+ */
+uint64
+pgsa_lookup_stash_id(char *stash_name)
+{
+ pgsa_stash *stash;
+ uint64 stash_id;
+
+ /* Search the shared hash table. */
+ stash = dshash_find(pgsa_stash_dshash, stash_name, false);
+ if (stash == NULL)
+ return 0;
+ stash_id = stash->pgsa_stash_id;
+ dshash_release_lock(pgsa_stash_dshash, stash);
+
+ return stash_id;
+}
+
+/*
+ * Store a new or updated advice string for the given advice stash and query ID.
+ */
+void
+pgsa_set_advice_string(char *stash_name, int64 queryId, char *advice_string)
+{
+ pgsa_entry *entry;
+ bool found;
+ pgsa_entry_key key;
+ uint64 stash_id;
+ dsa_pointer new_dp;
+ dsa_pointer old_dp;
+
+ /*
+ * The caller must hold our lock, at least in shared mode. This is
+ * important for two reasons.
+ *
+ * First, it holds off interrupts, so that we can't bail out of this code
+ * after allocating DSA memory for the advice string and before storing
+ * the resulting pointer somewhere that others can find it.
+ *
+ * Second, we need to avoid a race against pgsa_drop_stash(). That
+ * function removes a stash_name->stash_id mapping and all the entries for
+ * that stash_id. Without the lock, there's a race condition no matter
+ * which of those things it does first, because as soon as we've looked up
+ * the stash ID, that whole function can execute before we do the rest of
+ * our work, which would result in us adding an entry for a stash that no
+ * longer exists.
+ */
+ Assert(LWLockHeldByMe(&pgsa_state->lock));
+
+ /* Look up the stash ID. */
+ if ((stash_id = pgsa_lookup_stash_id(stash_name)) == 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("advice stash \"%s\" does not exist", stash_name));
+
+ /* Allocate space for the advice string. */
+ new_dp = dsa_allocate(pgsa_dsa_area, strlen(advice_string) + 1);
+ strcpy(dsa_get_address(pgsa_dsa_area, new_dp), advice_string);
+
+ /* Attempt to insert an entry into the hash table. */
+ memset(&key, 0, sizeof(pgsa_entry_key));
+ key.pgsa_stash_id = stash_id;
+ key.queryId = queryId;
+ entry = dshash_find_or_insert_extended(pgsa_entry_dshash, &key, &found,
+ DSHASH_INSERT_NO_OOM);
+
+ /*
+ * If it didn't work, bail out, being careful to free the shared memory
+ * we've already allocated before, since error cleanup will not do so.
+ */
+ if (entry == NULL)
+ {
+ dsa_free(pgsa_dsa_area, new_dp);
+ ereport(ERROR,
+ errcode(ERRCODE_OUT_OF_MEMORY),
+ errmsg("out of memory"),
+ errdetail("could not insert advice string into shared hash table"));
+ }
+
+ /* Update the entry and release the lock. */
+ old_dp = found ? entry->advice_string : InvalidDsaPointer;
+ entry->advice_string = new_dp;
+ dshash_release_lock(pgsa_entry_dshash, entry);
+
+ /*
+ * We're not safe from leaks yet!
+ *
+ * There's now a pointer to new_dp in the entry that we just updated, but
+ * that means that there's no longer anything pointing to old_dp.
+ */
+ if (DsaPointerIsValid(old_dp))
+ dsa_free(pgsa_dsa_area, old_dp);
+}
--- /dev/null
+# pg_stash_advice extension
+comment = 'store and automatically apply plan advice'
+default_version = '1.0'
+module_pathname = '$libdir/pg_stash_advice'
+relocatable = true
--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * pg_stash_advice.h
+ * main header for pg_stash_advice contrib module
+ *
+ * This module allows plan advice strings (as used and generated by
+ * pg_plan_advice) to be "stashed" in dynamic shared memory and, from
+ * there, automatically be applied to queries as they are planned.
+ * You can create any number of advice stashes, each of which is
+ * identified by a human-readable, ASCII identifier, and each of them is
+ * essentially a query ID -> advice_string mapping.
+ *
+ * Copyright (c) 2016-2026, PostgreSQL Global Development Group
+ *
+ * contrib/pg_stash_advice/pg_stash_advice.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_STASH_ADVICE_H
+#define PG_STASH_ADVICE_H
+
+#include "lib/dshash.h"
+#include "storage/lwlock.h"
+
+/*
+ * The key that we use to find a particular stash entry.
+ */
+typedef struct pgsa_entry_key
+{
+ uint64 pgsa_stash_id;
+ int64 queryId;
+} pgsa_entry_key;
+
+/*
+ * A single stash entry.
+ */
+typedef struct pgsa_entry
+{
+ pgsa_entry_key key;
+ dsa_pointer advice_string;
+} pgsa_entry;
+
+/*
+ * The stash itself is just a mapping from a name to a stash ID.
+ */
+typedef struct pgsa_stash
+{
+ char name[NAMEDATALEN];
+ uint64 pgsa_stash_id;
+} pgsa_stash;
+
+/*
+ * Top-level shared state object for pg_stash_advice.
+ */
+typedef struct pgsa_shared_state
+{
+ LWLock lock;
+ int dsa_tranche;
+ int stash_tranche;
+ int entry_tranche;
+ uint64 next_stash_id;
+ dsa_handle area;
+ dshash_table_handle stash_hash;
+ dshash_table_handle entry_hash;
+} pgsa_shared_state;
+
+/* For stash ID -> stash name hash table */
+typedef struct pgsa_stash_name
+{
+ uint32 status;
+ uint64 pgsa_stash_id;
+ char *name;
+} pgsa_stash_name;
+
+/* Declare stash ID -> stash name hash table */
+#define SH_PREFIX pgsa_stash_name_table
+#define SH_ELEMENT_TYPE pgsa_stash_name
+#define SH_KEY_TYPE uint64
+#define SH_SCOPE extern
+#define SH_DECLARE
+#include "lib/simplehash.h"
+
+/* Shared memory pointers */
+extern pgsa_shared_state *pgsa_state;
+extern dsa_area *pgsa_dsa_area;
+extern dshash_table *pgsa_stash_dshash;
+extern dshash_table *pgsa_entry_dshash;
+
+/* Function prototypes */
+extern void pgsa_attach(void);
+extern void pgsa_check_stash_name(char *stash_name);
+extern void pgsa_clear_advice_string(char *stash_name, int64 queryId);
+extern void pgsa_create_stash(char *stash_name);
+extern void pgsa_drop_stash(char *stash_name);
+extern uint64 pgsa_lookup_stash_id(char *stash_name);
+extern void pgsa_set_advice_string(char *stash_name, int64 queryId,
+ char *advice_string);
+
+#endif
--- /dev/null
+CREATE EXTENSION pg_stash_advice;
+SET compute_query_id = on;
+SET max_parallel_workers_per_gather = 0;
+
+-- Helper: extract query identifier from EXPLAIN VERBOSE output.
+CREATE OR REPLACE FUNCTION get_query_id(query_text text) RETURNS bigint
+LANGUAGE plpgsql AS $$
+DECLARE
+ line text;
+ qid bigint;
+BEGIN
+ FOR line IN EXECUTE 'EXPLAIN (VERBOSE, FORMAT TEXT) ' || query_text
+ LOOP
+ IF line ~ 'Query Identifier:' THEN
+ qid := regexp_replace(line, '.*Query Identifier:\s*(-?\d+).*', '\1')::bigint;
+ RETURN qid;
+ END IF;
+ END LOOP;
+ RAISE EXCEPTION 'Query Identifier not found in EXPLAIN output';
+END;
+$$;
+
+CREATE TABLE aa_dim1 (id integer primary key, dim1 text, val1 int)
+ WITH (autovacuum_enabled = false);
+INSERT INTO aa_dim1 (id, dim1, val1)
+ SELECT g, 'some filler text ' || g, (g % 3) + 1
+ FROM generate_series(1,100) g;
+VACUUM ANALYZE aa_dim1;
+
+CREATE TABLE aa_dim2 (id integer primary key, dim2 text, val2 int)
+ WITH (autovacuum_enabled = false);
+INSERT INTO aa_dim2 (id, dim2, val2)
+ SELECT g, 'some filler text ' || g, (g % 7) + 1
+ FROM generate_series(1,1000) g;
+VACUUM ANALYZE aa_dim2;
+
+CREATE TABLE aa_fact (
+ id int primary key,
+ dim1_id integer not null references aa_dim1 (id),
+ dim2_id integer not null references aa_dim2 (id)
+) WITH (autovacuum_enabled = false);
+INSERT INTO aa_fact
+ SELECT g, (g%100)+1, (g%100)+1 FROM generate_series(1,100000) g;
+VACUUM ANALYZE aa_fact;
+
+-- Get the query identifier.
+SELECT get_query_id($$
+SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+$$) AS qid \gset
+
+-- Create an advice stash and point pg_stash_advice at it.
+SELECT pg_create_advice_stash('regress_stash');
+SET pg_stash_advice.stash_name = 'regress_stash';
+
+-- Run our test query for the first time with no stashed advice.
+EXPLAIN (COSTS OFF)
+SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+
+-- Force an index scan on dim1
+SELECT pg_set_stashed_advice('regress_stash', :'qid',
+ 'INDEX_SCAN(d1 aa_dim1_pkey)');
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+
+-- Force an alternative join order
+SELECT pg_set_stashed_advice('regress_stash', :'qid',
+ 'join_order(f d1 d2)');
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+
+-- Force an alternative join strategy
+SELECT pg_set_stashed_advice('regress_stash', :'qid',
+ 'NESTED_LOOP_PLAIN(d1)');
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+
+-- Add a useless extra entry to our test stash. Shouldn't change the result
+-- from the previous test.
+-- (If we're unlucky enough that this ever fails due to query ID actually
+-- being 1, then just put some other constant here. Seems unlikely.)
+SELECT pg_set_stashed_advice('regress_stash', 1, 'SEQ_SCAN(d1)');
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+
+-- Try an empty stash to be sure it does nothing
+SELECT pg_create_advice_stash('regress_empty_stash');
+SET pg_stash_advice.stash_name = 'regress_empty_stash';
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+
+-- Test that we can list each stash individually and all of them together,
+-- but not a nonexistent stash.
+SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name;
+SELECT stash_name, advice_string
+ FROM pg_get_advice_stash_contents('regress_stash') ORDER BY advice_string;
+SELECT stash_name, advice_string
+ FROM pg_get_advice_stash_contents('regress_empty_stash')
+ ORDER BY advice_string;
+SELECT stash_name, advice_string
+ FROM pg_get_advice_stash_contents(NULL) ORDER BY advice_string;
+SELECT stash_name, advice_string
+ FROM pg_get_advice_stash_contents('no_such_stash')
+ ORDER BY advice_string;
+
+-- Test that we can remove advice.
+SELECT pg_set_stashed_advice('regress_stash', :'qid', null);
+SET pg_stash_advice.stash_name = 'regress_stash';
+EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
+ LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
+ LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
+ WHERE val1 = 1 AND val2 = 1;
+SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name;
+SELECT stash_name, advice_string
+ FROM pg_get_advice_stash_contents('regress_stash') ORDER BY advice_string;
+
+-- Can't create a stash that already exists, or drop one that doesn't.
+SELECT pg_create_advice_stash('regress_stash');
+SELECT pg_drop_advice_stash('no_such_stash');
+
+-- Can't add to or remove from a stash that does not exist.
+SELECT pg_set_stashed_advice('no_such_stash', 1, 'SEQ_SCAN(t)');
+SELECT pg_set_stashed_advice('no_such_stash', 1, null);
+
+-- Can't use query ID 0.
+SELECT pg_set_stashed_advice('regress_stash', 0, 'SEQ_SCAN(t)');
+
+-- Stash names must be non-empty, ASCII, and not too long, and must look
+-- like identifiers.
+SELECT pg_create_advice_stash('');
+SELECT pg_create_advice_stash('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
+SELECT pg_create_advice_stash(' ');
+SET pg_stash_advice.stash_name = '99bottles';
+
+-- Clean up state in dynamic shared memory.
+SELECT pg_drop_advice_stash('regress_stash');
+SELECT pg_drop_advice_stash('regress_empty_stash');
--- /dev/null
+/*
+ * This test must be run in a database with UTF-8 encoding,
+ * because other encodings don't support all the characters used.
+ */
+
+SELECT getdatabaseencoding() <> 'UTF8'
+ AS skip_test \gset
+\if :skip_test
+\quit
+\endif
+
+SET client_encoding = utf8;
+
+-- Non-ASCII stash names should be rejected.
+SELECT pg_create_advice_stash('café');
+SET pg_stash_advice.stash_name = 'café';
--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * stashfuncs.c
+ * SQL interface to pg_stash_advice
+ *
+ * Copyright (c) 2016-2026, PostgreSQL Global Development Group
+ *
+ * contrib/pg_stash_advice/stashfuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "common/hashfn.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "pg_stash_advice.h"
+#include "utils/builtins.h"
+#include "utils/tuplestore.h"
+
+PG_FUNCTION_INFO_V1(pg_create_advice_stash);
+PG_FUNCTION_INFO_V1(pg_drop_advice_stash);
+PG_FUNCTION_INFO_V1(pg_get_advice_stash_contents);
+PG_FUNCTION_INFO_V1(pg_get_advice_stashes);
+PG_FUNCTION_INFO_V1(pg_set_stashed_advice);
+
+typedef struct pgsa_stash_count
+{
+ uint32 status;
+ uint64 pgsa_stash_id;
+ int64 num_entries;
+} pgsa_stash_count;
+
+#define SH_PREFIX pgsa_stash_count_table
+#define SH_ELEMENT_TYPE pgsa_stash_count
+#define SH_KEY_TYPE uint64
+#define SH_KEY pgsa_stash_id
+#define SH_HASH_KEY(tb, key) hash_bytes((const unsigned char *) &(key), sizeof(uint64))
+#define SH_EQUAL(tb, a, b) (a == b)
+#define SH_SCOPE static inline
+#define SH_DEFINE
+#define SH_DECLARE
+#include "lib/simplehash.h"
+
+/*
+ * SQL-callable function to create an advice stash
+ */
+Datum
+pg_create_advice_stash(PG_FUNCTION_ARGS)
+{
+ char *stash_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+
+ pgsa_check_stash_name(stash_name);
+ if (unlikely(pgsa_entry_dshash == NULL))
+ pgsa_attach();
+ LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
+ pgsa_create_stash(stash_name);
+ LWLockRelease(&pgsa_state->lock);
+ PG_RETURN_VOID();
+}
+
+/*
+ * SQL-callable function to drop an advice stash
+ */
+Datum
+pg_drop_advice_stash(PG_FUNCTION_ARGS)
+{
+ char *stash_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+
+ pgsa_check_stash_name(stash_name);
+ if (unlikely(pgsa_entry_dshash == NULL))
+ pgsa_attach();
+ LWLockAcquire(&pgsa_state->lock, LW_EXCLUSIVE);
+ pgsa_drop_stash(stash_name);
+ LWLockRelease(&pgsa_state->lock);
+ PG_RETURN_VOID();
+}
+
+/*
+ * SQL-callable function to provide a list of advice stashes
+ */
+Datum
+pg_get_advice_stashes(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ dshash_seq_status iterator;
+ pgsa_entry *entry;
+ pgsa_stash *stash;
+ pgsa_stash_count_table_hash *chash;
+
+ InitMaterializedSRF(fcinfo, 0);
+
+ /* Attach to dynamic shared memory if not already done. */
+ if (unlikely(pgsa_entry_dshash == NULL))
+ pgsa_attach();
+
+ /* Tally up the number of entries per stash. */
+ chash = pgsa_stash_count_table_create(CurrentMemoryContext, 64, NULL);
+ dshash_seq_init(&iterator, pgsa_entry_dshash, true);
+ while ((entry = dshash_seq_next(&iterator)) != NULL)
+ {
+ pgsa_stash_count *c;
+ bool found;
+
+ c = pgsa_stash_count_table_insert(chash,
+ entry->key.pgsa_stash_id,
+ &found);
+ if (!found)
+ c->num_entries = 1;
+ else
+ c->num_entries++;
+ }
+ dshash_seq_term(&iterator);
+
+ /* Emit results. */
+ dshash_seq_init(&iterator, pgsa_stash_dshash, true);
+ while ((stash = dshash_seq_next(&iterator)) != NULL)
+ {
+ Datum values[2];
+ bool nulls[2];
+ pgsa_stash_count *c;
+
+ values[0] = CStringGetTextDatum(stash->name);
+ nulls[0] = false;
+
+ c = pgsa_stash_count_table_lookup(chash, stash->pgsa_stash_id);
+ values[1] = Int64GetDatum(c == NULL ? 0 : c->num_entries);
+ nulls[1] = false;
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values,
+ nulls);
+ }
+ dshash_seq_term(&iterator);
+
+ return (Datum) 0;
+}
+
+/*
+ * SQL-callable function to provide advice stash contents
+ */
+Datum
+pg_get_advice_stash_contents(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ dshash_seq_status iterator;
+ char *stash_name = NULL;
+ pgsa_stash_name_table_hash *nhash = NULL;
+ uint64 stash_id = 0;
+ pgsa_entry *entry;
+
+ InitMaterializedSRF(fcinfo, 0);
+
+ /* Attach to dynamic shared memory if not already done. */
+ if (unlikely(pgsa_entry_dshash == NULL))
+ pgsa_attach();
+
+ /* User can pass NULL for all stashes, or the name of a specific stash. */
+ if (!PG_ARGISNULL(0))
+ {
+ stash_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+ pgsa_check_stash_name(stash_name);
+ stash_id = pgsa_lookup_stash_id(stash_name);
+
+ /* If the user specified a stash name, it should exist. */
+ if (stash_id == 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("advice stash \"%s\" does not exist", stash_name));
+ }
+ else
+ {
+ pgsa_stash *stash;
+
+ /*
+ * If we're dumping data about all stashes, we need an ID->name lookup
+ * table.
+ */
+ nhash = pgsa_stash_name_table_create(CurrentMemoryContext, 64, NULL);
+ dshash_seq_init(&iterator, pgsa_stash_dshash, true);
+ while ((stash = dshash_seq_next(&iterator)) != NULL)
+ {
+ pgsa_stash_name *n;
+ bool found;
+
+ n = pgsa_stash_name_table_insert(nhash,
+ stash->pgsa_stash_id,
+ &found);
+ Assert(!found);
+ n->name = pstrdup(stash->name);
+ }
+ dshash_seq_term(&iterator);
+ }
+
+ /* Now iterate over all the entries. */
+ dshash_seq_init(&iterator, pgsa_entry_dshash, false);
+ while ((entry = dshash_seq_next(&iterator)) != NULL)
+ {
+ Datum values[3];
+ bool nulls[3];
+ char *this_stash_name;
+ char *advice_string;
+
+ /* Skip incomplete entries where the advice string was never set. */
+ if (entry->advice_string == InvalidDsaPointer)
+ continue;
+
+ if (stash_id != 0)
+ {
+ /*
+ * We're only dumping data for one particular stash, so skip
+ * entries for any other stash and use the stash name specified by
+ * the user.
+ */
+ if (stash_id != entry->key.pgsa_stash_id)
+ continue;
+ this_stash_name = stash_name;
+ }
+ else
+ {
+ pgsa_stash_name *n;
+
+ /*
+ * We're dumping data for all stashes, so look up the correct name
+ * to use in the hash table. If nothing is found, which is
+ * possible due to race conditions, make up a string to use.
+ */
+ n = pgsa_stash_name_table_lookup(nhash, entry->key.pgsa_stash_id);
+ if (n != NULL)
+ this_stash_name = n->name;
+ else
+ this_stash_name = psprintf("<stash %" PRIu64 ">",
+ entry->key.pgsa_stash_id);
+ }
+
+ /* Work out tuple values. */
+ values[0] = CStringGetTextDatum(this_stash_name);
+ nulls[0] = false;
+ values[1] = Int64GetDatum(entry->key.queryId);
+ nulls[1] = false;
+ advice_string = dsa_get_address(pgsa_dsa_area, entry->advice_string);
+ values[2] = CStringGetTextDatum(advice_string);
+ nulls[2] = false;
+
+ /* Emit the tuple. */
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values,
+ nulls);
+ }
+ dshash_seq_term(&iterator);
+
+ return (Datum) 0;
+}
+
+/*
+ * SQL-callable function to update an advice stash entry for a particular
+ * query ID
+ *
+ * If the second argument is NULL, we delete any existing advice stash
+ * entry; otherwise, we either create an entry or update it with the new
+ * advice string.
+ */
+Datum
+pg_set_stashed_advice(PG_FUNCTION_ARGS)
+{
+ char *stash_name;
+ int64 queryId;
+
+ if (PG_ARGISNULL(0) || PG_ARGISNULL(1))
+ PG_RETURN_NULL();
+
+ /* Get and check advice stash name. */
+ stash_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+ pgsa_check_stash_name(stash_name);
+
+ /*
+ * Get and check query ID.
+ *
+ * queryID 0 means no query ID was computed, so reject that.
+ */
+ queryId = PG_GETARG_INT64(1);
+ if (queryId == 0)
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set advice string for query ID 0"));
+
+ /* Attach to dynamic shared memory if not already done. */
+ if (unlikely(pgsa_entry_dshash == NULL))
+ pgsa_attach();
+
+ /* Now call the appropriate function to do the real work. */
+ if (PG_ARGISNULL(2))
+ {
+ LWLockAcquire(&pgsa_state->lock, LW_SHARED);
+ pgsa_clear_advice_string(stash_name, queryId);
+ LWLockRelease(&pgsa_state->lock);
+ }
+ else
+ {
+ char *advice_string = text_to_cstring(PG_GETARG_TEXT_PP(2));
+
+ LWLockAcquire(&pgsa_state->lock, LW_SHARED);
+ pgsa_set_advice_string(stash_name, queryId, advice_string);
+ LWLockRelease(&pgsa_state->lock);
+ }
+
+ PG_RETURN_VOID();
+}
&pgplanadvice;
&pgprewarm;
&pgrowlocks;
+ &pgstashadvice;
&pgstatstatements;
&pgstattuple;
&pgsurgery;
<!ENTITY oid2name SYSTEM "oid2name.sgml">
<!ENTITY pageinspect SYSTEM "pageinspect.sgml">
<!ENTITY passwordcheck SYSTEM "passwordcheck.sgml">
+<!ENTITY pgstashadvice SYSTEM "pgstashadvice.sgml">
<!ENTITY pgbuffercache SYSTEM "pgbuffercache.sgml">
<!ENTITY pgcrypto SYSTEM "pgcrypto.sgml">
<!ENTITY pgfreespacemap SYSTEM "pgfreespacemap.sgml">
--- /dev/null
+<!-- doc/src/sgml/pgstashadvice.sgml -->
+
+<sect1 id="pgstashadvice" xreflabel="pg_stash_advice">
+ <title>pg_stash_advice — store and automatically apply plan advice</title>
+
+ <indexterm zone="pgstashadvice">
+ <primary>pg_stash_advice</primary>
+ </indexterm>
+
+ <para>
+ The <filename>pg_stash_advice</filename> extension allows you to stash
+ <link linkend="pgplanadvice">plan advice</link> strings in dynamic
+ shared memory where they can be automatically applied. An
+ <literal>advice stash</literal> is a mapping from
+ <link linkend="guc-compute-query-id">query identifiers</link> to plan advice
+ strings. Whenever a session is asked to plan a query whose query ID appears
+ in the relevant advice stash, the plan advice string is automatically applied
+ to guide planning. Note that advice stashes exist purely in memory. This
+ means both that it is important to be mindful of memory consumption when
+ deciding how much plan advice to stash, and also that advice stashes must
+ be recreated and repopulated whenever the server is restarted.
+ </para>
+
+ <para>
+ In order to use this module, you will need to execute
+ <literal>CREATE EXTENSION pg_stash_advice</literal> in at least
+ one database, so that you have access to the SQL functions to manage
+ advice stashes. You will also need the <literal>pg_stash_advice</literal>
+ module to be loaded in all sessions where you want this module to
+ automatically apply advice. It will usually be best to do this by adding
+ <literal>pg_stash_advice</literal> to
+ <xref linkend="guc-shared-preload-libraries"/> and restarting the server.
+ </para>
+
+ <para>
+ Once you have met the above criteria, you can create advice stashes
+ using the <literal>pg_create_advice_stash</literal> function described
+ below and set the plan advice for a given query ID in a given stash using
+ the <literal>pg_set_stashed_advice</literal> function. Then, you need
+ only configure <literal>pg_stash_advice.stash_name</literal> to point
+ to the chosen advice stash name. For some use cases, rather than setting
+ this on a system-wide basis, you may find it helpful to use
+ <literal>ALTER DATABASE ... SET</literal> or
+ <literal>ALTER ROLE ... SET</literal> to configure values that will apply
+ only to a database or only to a certain role. Likewise, it may sometimes
+ be better to set the stash name in a particular session using
+ <literal>SET</literal>.
+ </para>
+
+ <para>
+ Because <literal>pg_stash_advice</literal> works on the basis of query
+ identifiers, you will need to determine the query identifier for each query
+ whose plan you wish to control. You will also need to determine the advice
+ string that you wish to store for each query. One way to do this is to use
+ <literal>EXPLAIN</literal>: the <literal>VERBOSE</literal> option will
+ show the query ID, and the <literal>PLAN_ADVICE</literal> option will
+ show plan advice. Query identifiers can also be obtained through tools
+ such as <xref linkend="pgstatstatements" /> or
+ <xref linkend="monitoring-pg-stat-activity-view" />, but these tools
+ will not provide plan advice strings. Note that
+ <xref linkend="guc-compute-query-id" /> must be enabled for query
+ identifiers to be computed; if set to <literal>auto</literal>, loading
+ <literal>pg_stash_advice</literal> will enable it automatically.
+ </para>
+
+ <para>
+ Generally, the fact that the planner is able to change query plans as
+ the underlying distribution of data changes is a feature, not a bug.
+ Moreover, applying plan advice can have a noticeable performance cost even
+ when it does not result in a change to the query plan. Therefore, it is
+ a good idea to use this feature only when and to the extent needed.
+ Plan advice strings can be trimmed down to mention only those aspects
+ of the plan that need to be controlled, and used only for queries where
+ there is believed to be a significant risk of planner error.
+ </para>
+
+ <para>
+ Note that <literal>pg_stash_advice</literal> currently lacks a sophisticated
+ security model. Only the superuser, or a user to whom the superuser has
+ granted <literal>EXECUTE</literal> permission on the relevant functions,
+ may create advice stashes or alter their contents, but any user may set
+ <literal>pg_stash_advice.stash_name</literal> for their session, and this
+ may reveal the contents of any advice stash with that name. Users should
+ assume that information embedded in stashed advice strings may become visible
+ to nonprivileged users.
+ </para>
+
+ <sect2 id="pgstashadvice-functions">
+ <title>Functions</title>
+
+ <variablelist>
+
+ <varlistentry>
+ <term>
+ <function>pg_create_advice_stash(stash_name text) returns void</function>
+ <indexterm>
+ <primary>pg_create_advice_stash</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ Creates a new, empty advice stash with the given name.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>pg_drop_advice_stash(stash_name text) returns void</function>
+ <indexterm>
+ <primary>pg_drop_advice_stash</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ Drops the named advice stash and all of its entries.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>pg_set_stashed_advice(stash_name text, query_id bigint,
+ advice_string text) returns void</function>
+ <indexterm>
+ <primary>pg_set_stashed_advice</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ Stores an advice string in the named advice stash, associated with
+ the given query identifier. If an entry for that query identifier
+ already exists in the stash, it is replaced. If
+ <parameter>advice_string</parameter> is <literal>NULL</literal>,
+ any existing entry for that query identifier is removed.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>pg_get_advice_stashes() returns setof (stash_name text,
+ num_entries bigint)</function>
+ <indexterm>
+ <primary>pg_get_advice_stashes</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ Returns one row for each advice stash, showing the stash name and
+ the number of entries it contains.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>
+ <function>pg_get_advice_stash_contents(stash_name text) returns setof
+ (stash_name text, query_id bigint, advice_string text)</function>
+ <indexterm>
+ <primary>pg_get_advice_stash_contents</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ Returns one row for each entry in the named advice stash. If
+ <parameter>stash_name</parameter> is <literal>NULL</literal>, returns
+ entries from all stashes.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect2>
+
+ <sect2 id="pgstashadvice-config-params">
+ <title>Configuration Parameters</title>
+
+ <variablelist>
+
+ <varlistentry>
+ <term>
+ <varname>pg_stash_advice.stash_name</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>pg_stash_advice.stash_name</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+
+ <listitem>
+ <para>
+ Specifies the name of the advice stash to consult during query
+ planning. The default value is the empty string, which disables
+ this module.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+
+ </sect2>
+
+ <sect2 id="pgstashadvice-author">
+ <title>Author</title>
+
+ <para>
+ Robert Haas <email>rhaas@postgresql.org</email>
+ </para>
+ </sect2>
+
+</sect1>
pgpa_trove_result
pgpa_trove_slice
pgpa_unrolled_join
+pgsa_entry
+pgsa_entry_key
+pgsa_shared_state
+pgsa_stash
+pgsa_stash_count
+pgsa_stash_name
pgsocket
pgsql_thing_t
pgssEntry