From 3586deb8bbcc25ac0c4de5c7ef6421ffddc93839 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Tue, 26 May 2026 16:07:57 +0200 Subject: [PATCH v1] Speed up extended protocol by using oneshot cached plan DISCLAIMER: Fully written by AI. I still need to check if the code is fully correct. The approach seems sensible though, and meson test passes on my machine. On my laptop on master I get much lower perf with the extended protocol than the simple protocol in a basic pgbench --select-only test: pgbench -i pgbench --select-only -T 10 --protocol simple pgbench --select-only -T 10 --protocol extended For simple I get ~56k TPS and for extended I only get ~49k TPS With this change I get ~53k TPS with extended so a ~8% improvement. --- src/backend/tcop/postgres.c | 57 ++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index dbef734a93f..3ab9d7ab3b2 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -1513,10 +1513,27 @@ exec_parse_message(const char *query_string, /* string to execute */ /* * Create the CachedPlanSource before we do parse analysis, since it - * needs to see the unmodified raw parse tree. + * needs to see the unmodified raw parse tree. For unnamed statements + * use a one-shot plan: this skips the deep copy of the raw parse + * tree, the deep copy of the rewritten query tree in BuildCachedPlan, + * and the SaveCachedPlan reparent into CacheMemoryContext. The + * trade-off is no invalidation support, which is acceptable for the + * common case where the unnamed statement is Parse+Bind+Execute'd in + * a single transaction. */ - psrc = CreateCachedPlan(raw_parse_tree, query_string, - CreateCommandTag(raw_parse_tree->stmt)); + if (is_named) + psrc = CreateCachedPlan(raw_parse_tree, query_string, + CreateCommandTag(raw_parse_tree->stmt)); + else + { + /* + * CreateOneShotCachedPlan doesn't copy query_string, so we must + * stash it in unnamed_stmt_context (CurrentMemoryContext) so it + * outlives the MessageContext reset between Parse and Bind. + */ + psrc = CreateOneShotCachedPlan(raw_parse_tree, pstrdup(query_string), + CreateCommandTag(raw_parse_tree->stmt)); + } /* * Set up a snapshot if parse analysis will need one. @@ -1546,8 +1563,12 @@ exec_parse_message(const char *query_string, /* string to execute */ { /* Empty input string. This is legal. */ raw_parse_tree = NULL; - psrc = CreateCachedPlan(raw_parse_tree, query_string, - CMDTAG_UNKNOWN); + if (is_named) + psrc = CreateCachedPlan(raw_parse_tree, query_string, + CMDTAG_UNKNOWN); + else + psrc = CreateOneShotCachedPlan(raw_parse_tree, pstrdup(query_string), + CMDTAG_UNKNOWN); querytree_list = NIL; } @@ -1584,9 +1605,13 @@ exec_parse_message(const char *query_string, /* string to execute */ else { /* - * We just save the CachedPlanSource into unnamed_stmt_psrc. + * For unnamed statements we use a one-shot CachedPlanSource, so we + * can't SaveCachedPlan it. Instead reparent its context (which is + * unnamed_stmt_context) under CacheMemoryContext so that it survives + * the MessageContext reset between Parse and the following Bind. + * drop_unnamed_stmt() will MemoryContextDelete it explicitly. */ - SaveCachedPlan(psrc); + MemoryContextSetParent(psrc->context, CacheMemoryContext); unnamed_stmt_psrc = psrc; } @@ -2028,8 +2053,18 @@ exec_bind_message(StringInfo input_message) * Obtain a plan from the CachedPlanSource. Any cruft from (re)planning * will be generated in MessageContext. The plan refcount will be * assigned to the Portal, so it will be released at portal destruction. + * + * For one-shot plans (used for unnamed prepared statements), + * BuildCachedPlan does not allocate a dedicated plan_context; the plan + * ends up in CurrentMemoryContext. Switch to the portal's context for + * the call so the plan survives MessageContext resets between Bind and + * Execute (e.g. in pipelined extended-protocol traffic). */ + if (psrc->is_oneshot) + MemoryContextSwitchTo(portal->portalContext); cplan = GetCachedPlan(psrc, params, NULL, NULL); + if (psrc->is_oneshot) + MemoryContextSwitchTo(MessageContext); /* * Now we can define the portal. @@ -2906,8 +2941,16 @@ drop_unnamed_stmt(void) { CachedPlanSource *psrc = unnamed_stmt_psrc; + /* + * For one-shot plans DropCachedPlan does not free the context (it's + * owned by the caller), so capture it and delete it ourselves. + */ + MemoryContext oneshot_ctx = psrc->is_oneshot ? psrc->context : NULL; + unnamed_stmt_psrc = NULL; DropCachedPlan(psrc); + if (oneshot_ctx) + MemoryContextDelete(oneshot_ctx); } } base-commit: 6aa26be288fa811270dfc1e39c015c23a97688b4 -- 2.54.0