When deleting the plpgsql function, release the CachedPlan of the function
Hi, hackers
I have observed an issue where the CachedPlan corresponding to a function/procedure is not released when we execute the "DROP FUNCTION\PROCEDURE" command. A patch to resolve this problem is attached.
A simple test case is as follows:
Step 1 :
create or replace procedure test_pro() as $$declare
va int default 100;
begin
for i in 1 .. 10 loop
va := va + i;
end loop;
raise notice '%', va;
va := va;
end $$ LANGUAGE plpgsql;
Step 2:
call test_pro();
Step 3:
select * from pg_get_backend_memory_contexts() where parent = 'CacheMemoryContext' and name = 'CachedPlan';
Step 4:
drop procedure test_pro;
Step 5:
select * from pg_get_backend_memory_contexts() where parent = 'CacheMemoryContext' and name = 'CachedPlan';
Step 6:
create or replace procedure test_pro() as $$declare
va int default 100;
begin
for i in 1 .. 10 loop
va := va + i;
end loop;
raise notice '%', va;
va := va;
end $$ LANGUAGE plpgsql;
Step 7:
call test_pro();
Step 8:
select * from pg_get_backend_memory_contexts() where parent = 'CacheMemoryContext' and name = 'CachedPlan';
result:
postgres@zxm-VMware-Virtual-Platform:/data/16$ psqlpsql (16.10)
Type "help" for help.
postgres=# create or replace procedure test_pro() as $$
declare
va int default 100;
begin
for i in 1 .. 10 loop
va := va + i;
end loop;
raise notice '%', va;
va := va;
end $$ LANGUAGE plpgsql;
CREATE PROCEDURE
postgres=# call test_pro();
NOTICE: 155
CALL
postgres=# select * from pg_get_backend_memory_contexts() where parent = 'CacheMemoryContext' and name = 'CachedPlan';
name | ident | parent | level | total_bytes | total_nblocks | free_bytes | free_chunks | used_bytes
------------+--------------+--------------------+-------+-------------+---------------+------------+-------------+------------
CachedPlan | va := va | CacheMemoryContext | 2 | 2048 | 2 | 576 | 0 | 1472
CachedPlan | va | CacheMemoryContext | 2 | 2048 | 2 | 584 | 0 | 1464
CachedPlan | va := va + i | CacheMemoryContext | 2 | 2048 | 2 | 384 | 0 | 1664
CachedPlan | 10 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
CachedPlan | 1 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
CachedPlan | 100 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
(6 rows)
postgres=# drop procedure test_pro;
DROP PROCEDURE
postgres=# select * from pg_get_backend_memory_contexts() where parent = 'CacheMemoryContext' and name = 'CachedPlan';
name | ident | parent | level | total_bytes | total_nblocks | free_bytes | free_chunks | used_bytes
------------+--------------+--------------------+-------+-------------+---------------+------------+-------------+------------
CachedPlan | va := va | CacheMemoryContext | 2 | 2048 | 2 | 576 | 0 | 1472
CachedPlan | va | CacheMemoryContext | 2 | 2048 | 2 | 584 | 0 | 1464
CachedPlan | va := va + i | CacheMemoryContext | 2 | 2048 | 2 | 384 | 0 | 1664
CachedPlan | 10 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
CachedPlan | 1 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
CachedPlan | 100 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
(6 rows)
postgres=# create or replace procedure test_pro() as $$
declare
va int default 100;
begin
for i in 1 .. 10 loop
va := va + i;
end loop;
raise notice '%', va;
va := va;
end $$ LANGUAGE plpgsql;
CREATE PROCEDURE
postgres=# call test_pro();
NOTICE: 155
CALL
postgres=# select * from pg_get_backend_memory_contexts() where parent = 'CacheMemoryContext' and name = 'CachedPlan';
name | ident | parent | level | total_bytes | total_nblocks | free_bytes | free_chunks | used_bytes
------------+--------------+--------------------+-------+-------------+---------------+------------+-------------+------------
CachedPlan | va := va | CacheMemoryContext | 2 | 2048 | 2 | 576 | 0 | 1472
CachedPlan | va | CacheMemoryContext | 2 | 2048 | 2 | 584 | 0 | 1464
CachedPlan | va := va + i | CacheMemoryContext | 2 | 2048 | 2 | 384 | 0 | 1664
CachedPlan | 10 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
CachedPlan | 1 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
CachedPlan | 100 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
CachedPlan | va := va | CacheMemoryContext | 2 | 2048 | 2 | 576 | 0 | 1472
CachedPlan | va | CacheMemoryContext | 2 | 2048 | 2 | 584 | 0 | 1464
CachedPlan | va := va + i | CacheMemoryContext | 2 | 2048 | 2 | 384 | 0 | 1664
CachedPlan | 10 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
CachedPlan | 1 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
CachedPlan | 100 | CacheMemoryContext | 2 | 2048 | 2 | 544 | 0 | 1504
(12 rows)
Attachments:
00001_free_function_memory.patchapplication/octet-stream; charset=gb18030; name=00001_free_function_memory.patchDownload
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index f63b5ef420b..7604f1a0064 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -1301,6 +1301,7 @@ RemoveFunctionById(Oid funcOid)
Relation relation;
HeapTuple tup;
char prokind;
+ remove_function_plancache RemoveFunctionPlanCacheById;
/*
* Delete the pg_proc tuple.
@@ -1311,6 +1312,11 @@ RemoveFunctionById(Oid funcOid)
if (!HeapTupleIsValid(tup)) /* should not happen */
elog(ERROR, "cache lookup failed for function %u", funcOid);
+ /* Try to release plancache */
+ RemoveFunctionPlanCacheById = (remove_function_plancache) *find_rendezvous_variable("RemoveFunctionPlanCacheById");
+ if (RemoveFunctionPlanCacheById != NULL)
+ RemoveFunctionPlanCacheById(funcOid);
+
prokind = ((Form_pg_proc) GETSTRUCT(tup))->prokind;
CatalogTupleDelete(relation, &tup->t_self);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index a443181d416..a8e6388a330 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -233,4 +233,5 @@ extern bool CachedPlanIsSimplyValid(CachedPlanSource *plansource,
extern CachedExpression *GetCachedExpression(Node *expr);
extern void FreeCachedExpression(CachedExpression *cexpr);
+typedef void (*remove_function_plancache) (Oid funcoid);
#endif /* PLANCACHE_H */
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index a341cde2c14..12adcc37ef9 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -67,6 +67,23 @@ typedef struct plpgsql_hashent
PLpgSQL_function *function;
} plpgsql_HashEnt;
+/* ----------
+ * Hash table for recording funcOid and functions
+ * ----------
+ */
+static HTAB *plpgsql_funcOid_HashTable = NULL;
+
+typedef struct PLpgSQL_funcOid_hashkey
+{
+ Oid funcOid;
+} PLpgSQL_funcOid_hashkey;
+
+typedef struct plpgsql_function_hashent
+{
+ PLpgSQL_funcOid_hashkey key;
+ PLpgSQL_function *function;
+} plpgsql_function_hashent;
+
#define FUNCS_PER_USER 128 /* initial table size */
/* ----------
@@ -121,6 +138,9 @@ static void plpgsql_HashTableInsert(PLpgSQL_function *function,
PLpgSQL_func_hashkey *func_key);
static void plpgsql_HashTableDelete(PLpgSQL_function *function);
static void delete_function(PLpgSQL_function *func);
+static PLpgSQL_function *plpgsql_funcOid_HashTableLookup(PLpgSQL_funcOid_hashkey *func_key);
+static void plpgsql_funcOid_HashTableInsert(PLpgSQL_function *function);
+static void plpgsql_funcOid_HashTableDelete(PLpgSQL_funcOid_hashkey *func_key);
/* ----------
* plpgsql_compile Make an execution tree for a PL/pgSQL function.
@@ -223,6 +243,9 @@ recheck:
*/
function = do_compile(fcinfo, procTup, function,
&hashkey, forValidator);
+
+ /* Record the relationship between funcOid and function */
+ plpgsql_funcOid_HashTableInsert(function);
}
ReleaseSysCache(procTup);
@@ -2670,3 +2693,87 @@ plpgsql_HashTableDelete(PLpgSQL_function *function)
/* remove back link, which no longer points to allocated storage */
function->fn_hashkey = NULL;
}
+
+/* Try to delete and release the plan cache from the hash table */
+void
+RemoveFunctionPlanCacheById(Oid funcoid)
+{
+ PLpgSQL_funcOid_hashkey func_key;
+ PLpgSQL_function *function = NULL;
+
+ func_key.funcOid = funcoid;
+ function = plpgsql_funcOid_HashTableLookup(&func_key);
+ if (function)
+ {
+ plpgsql_funcOid_HashTableDelete(&func_key);
+ delete_function(function);
+ }
+}
+
+void
+plpgsql_funcOid_HashTableInit(void)
+{
+ HASHCTL ctl;
+
+ /* don't allow double-initialization */
+ Assert(plpgsql_funcOid_HashTable == NULL);
+
+ ctl.keysize = sizeof(PLpgSQL_funcOid_hashkey);
+ ctl.entrysize = sizeof(plpgsql_function_hashent);
+ plpgsql_funcOid_HashTable = hash_create("PLpgSQL function Oid hash",
+ FUNCS_PER_USER,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS);
+}
+
+static PLpgSQL_function *
+plpgsql_funcOid_HashTableLookup(PLpgSQL_funcOid_hashkey *func_key)
+{
+ plpgsql_function_hashent *hentry;
+
+ hentry = (plpgsql_function_hashent *) hash_search(plpgsql_funcOid_HashTable,
+ func_key,
+ HASH_FIND,
+ NULL);
+ if (hentry)
+ return hentry->function;
+ else
+ return NULL;
+}
+
+static void
+plpgsql_funcOid_HashTableInsert(PLpgSQL_function *function)
+{
+ bool found;
+ plpgsql_function_hashent *hentry;
+ PLpgSQL_funcOid_hashkey func_key;
+ func_key.funcOid = function->fn_oid;
+
+ if (plpgsql_funcOid_HashTableLookup(&func_key))
+ return;
+
+ hentry = (plpgsql_function_hashent *) hash_search(plpgsql_funcOid_HashTable,
+ &func_key,
+ HASH_ENTER,
+ &found);
+
+ hentry->function = function;
+ hentry->key.funcOid = func_key.funcOid;
+}
+
+static void
+plpgsql_funcOid_HashTableDelete(PLpgSQL_funcOid_hashkey *func_key)
+{
+ plpgsql_function_hashent *hentry;
+
+ hentry = (plpgsql_function_hashent *) hash_search(plpgsql_funcOid_HashTable,
+ func_key,
+ HASH_REMOVE,
+ NULL);
+ if (hentry == NULL)
+ elog(WARNING, "trying to delete function that does not exist");
+
+ /* remove back link, which no longer points to allocated storage */
+ hentry->function = NULL;
+ hentry->key.funcOid = InvalidOid;
+}
\ No newline at end of file
diff --git a/src/pl/plpgsql/src/pl_handler.c b/src/pl/plpgsql/src/pl_handler.c
index d8994538b76..7edbf974979 100644
--- a/src/pl/plpgsql/src/pl_handler.c
+++ b/src/pl/plpgsql/src/pl_handler.c
@@ -146,6 +146,7 @@ _PG_init(void)
{
/* Be sure we do initialization only once (should be redundant now) */
static bool inited = false;
+ remove_function_plancache *remove_function_plancache_ptr;
if (inited)
return;
@@ -203,6 +204,10 @@ _PG_init(void)
RegisterXactCallback(plpgsql_xact_cb, NULL);
RegisterSubXactCallback(plpgsql_subxact_cb, NULL);
+ plpgsql_funcOid_HashTableInit();
+ remove_function_plancache_ptr = (remove_function_plancache *) find_rendezvous_variable("RemoveFunctionPlanCacheById");
+ *remove_function_plancache_ptr = &RemoveFunctionPlanCacheById;
+
/* Set up a rendezvous point with optional instrumentation plugin */
plpgsql_plugin_ptr = (PLpgSQL_plugin **) find_rendezvous_variable("PLpgSQL_plugin");
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index cead9eb7263..3ce25635009 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -1263,6 +1263,8 @@ extern PLpgSQL_condition *plpgsql_parse_err_condition(char *condname);
extern void plpgsql_adddatum(PLpgSQL_datum *newdatum);
extern int plpgsql_add_initdatums(int **varnos);
extern void plpgsql_HashTableInit(void);
+extern void plpgsql_funcOid_HashTableInit(void);
+extern void RemoveFunctionPlanCacheById(Oid funcoid);
/*
* Functions in pl_exec.c
Sorry for the garbled characters. I am resending the original test results as an attachment.
Attachments:
Import Notes
Resolved by subject fallback
Sorry for the garbled characters. I am resending the original test results as an attachment.
Attachments:
Import Notes
Resolved by subject fallback
On Mon, 18 Aug 2025 at 08:51, zengman <zengman@halodbtech.com> wrote:
Hi, hackers
I have observed an issue where the CachedPlan corresponding to a function/procedure is not released when we execute the "DROP FUNCTION\PROCEDURE" command. A patch to resolve this problem is attached.
I'm trying to figure out how this patch is supposed to handle
concurrent sessions dropping a procedure that has cached plans. AFAIK,
we don't execute RemoveFunctionById in other sessions, so this would
still leave the plan caches active for other backends, right?
Kind regards,
Matthias van de Meent
Databricks
Matthias van de Meent <boekewurm+postgres@gmail.com> writes:
I'm trying to figure out how this patch is supposed to handle
concurrent sessions dropping a procedure that has cached plans.
It doesn't, which is (one reason) why it's just a crude hack.
A more appropriate solution would be to make plpgsql install
a shared-cache-invalidation callback that would watch for
invalidations on pg_proc and mark relevant function trees as
deletable. It couldn't necessarily delete them right away,
since they might be in use at the moment the inval event
arrives. (That is, an inval might just indicate an update
not a delete. But flushing the function tree would be OK
in either case.)
I wonder if we could make src/backend/utils/cache/funccache.c
handle this, so that SQL functions could also benefit without
duplicated logic.
regards, tom lane
That's correct—this is a simple and blunt patch, and it fails to account for many factors. Initially, I wasn't even sure if this qualified as a distinct issue. Your solution is far more reasonable, and I will rethink the new implementation thoroughly based on your approach.
Thanks,
Zeng Man
Tom Lane<tgl@sss.pgh.pa.us> 在 2025年8月19日 周二 0:38 写道:
> It doesn't, which is (one reason) why it's just a crude hack.
> A more appropriate solution would be to make plpgsql install
> a shared-cache-invalidation callback that would watch for
Import Notes
Resolved by subject fallback
That's correct—this is a simple and blunt patch, and it fails to account for many factors. Initially, I wasn't even sure if this qualified as a distinct issue. Your solution is far more reasonable, and I will rethink the new implementation thoroughly based on your approach.
Thanks,
Zeng Man
The new status of this patch is: Waiting on Author
This patch only addresses the plan cache release for plpgsql (the procedural language) and does not cover SQL functions.
Since the src/backend/utils/cache/funccache.c file was introduced in PostgreSQL 18, I will first upgrade to version 18,
then figure out how to extend the benefit to SQL functions.
Additionally, I’m not entirely certain whether this patch’s modification is reasonable, or if it could lead to performance degradation.
I might need everyone’s help to review it.
Attachments:
00002_free_plpgsql_function_memory.patchapplication/octet-stream; charset=gb18030; name=00002_free_plpgsql_function_memory.patchDownload
diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c
index a341cde2c14..8aad27a9028 100644
--- a/src/pl/plpgsql/src/pl_comp.c
+++ b/src/pl/plpgsql/src/pl_comp.c
@@ -2670,3 +2670,31 @@ plpgsql_HashTableDelete(PLpgSQL_function *function)
/* remove back link, which no longer points to allocated storage */
function->fn_hashkey = NULL;
}
+
+/* Try to delete and release the plan cache from the hash table */
+void
+plpgsql_func_inval_callback(Datum arg, int cacheid, uint32 hashvalue)
+{
+ HASH_SEQ_STATUS scan;
+ plpgsql_HashEnt *entry;
+ uint32 funcOid_hashvalue;
+
+ Assert(cacheid == PROCOID);
+
+ hash_seq_init(&scan, plpgsql_HashTable);
+ while ((entry = (plpgsql_HashEnt *) hash_seq_search(&scan)))
+ {
+ funcOid_hashvalue = GetSysCacheHashValue1(PROCOID,
+ ObjectIdGetDatum(entry->key.funcOid));
+ /* Ignore invalid entries */
+ if (entry->function == NULL)
+ continue;
+
+ /* hashvalue == 0 means a cache reset, must clear all state */
+ if (hashvalue == 0 ||
+ (cacheid == PROCOID && funcOid_hashvalue == hashvalue))
+ {
+ delete_function(entry->function);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/pl/plpgsql/src/pl_handler.c b/src/pl/plpgsql/src/pl_handler.c
index d8994538b76..5b2e50913ba 100644
--- a/src/pl/plpgsql/src/pl_handler.c
+++ b/src/pl/plpgsql/src/pl_handler.c
@@ -26,6 +26,7 @@
#include "utils/lsyscache.h"
#include "utils/syscache.h"
#include "utils/varlena.h"
+#include "utils/inval.h"
static bool plpgsql_extra_checks_check_hook(char **newvalue, void **extra, GucSource source);
static void plpgsql_extra_warnings_assign_hook(const char *newvalue, void *extra);
@@ -202,6 +203,7 @@ _PG_init(void)
plpgsql_HashTableInit();
RegisterXactCallback(plpgsql_xact_cb, NULL);
RegisterSubXactCallback(plpgsql_subxact_cb, NULL);
+ CacheRegisterSyscacheCallback(PROCOID, plpgsql_func_inval_callback, (Datum) 0);
/* Set up a rendezvous point with optional instrumentation plugin */
plpgsql_plugin_ptr = (PLpgSQL_plugin **) find_rendezvous_variable("PLpgSQL_plugin");
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index cead9eb7263..20a006ca331 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -1263,6 +1263,7 @@ extern PLpgSQL_condition *plpgsql_parse_err_condition(char *condname);
extern void plpgsql_adddatum(PLpgSQL_datum *newdatum);
extern int plpgsql_add_initdatums(int **varnos);
extern void plpgsql_HashTableInit(void);
+extern void plpgsql_func_inval_callback(Datum arg, int cacheid, uint32 hashvalue);
/*
* Functions in pl_exec.c
Import Notes
Resolved by subject fallback
/messages/by-id/tencent_0BA97862026BC74E75238899@qq.com
The new status of this patch is: Needs review
Man Zeng писал(а) 2025-08-19 10:50:
/messages/by-id/tencent_0BA97862026BC74E75238899@qq.com
The new status of this patch is: Needs review
Hi!
I read from the beginning till this email and did not
find the description what exactly you are going to fix.
Could you provide more details?
A procedure creates plans and put them to cache for every
SQL query, that it executes (including limits in FOR operator
in your example, that considered as SQL queries). These plans remains
in the cache , when a procedure exits.
These cached plans can be used by this procedure again or can be used by
other procedures or by direct SQL query. It is not clear, why we should
delete them? It is the goal of the cache to use plans in other queries.
--
Best regards,
Vladlen Popolitov.
When a function or stored procedure is created, called, and then dropped,
the resulting CachedPlan is never released and can only be freed by exiting the session.
Meanwhile, if you create another function or stored procedure with the same name and parameters, and then call it,
you'll be able to see two separate CachedPlans via pg_get_backend_memory_contexts.
You may refer to the following test steps.
Step 1 :
create or replace procedure test_pro() as $$
declare
va int default 100;
begin
for i in 1 .. 10 loop
va := va + i;
end loop;
raise notice '%', va;
va := va;
end $$ LANGUAGE plpgsql;
Step 2:
call test_pro();
Step 3:
select * from pg_get_backend_memory_contexts() where parent = 'CacheMemoryContext' and name = 'CachedPlan';
Step 4:
drop procedure test_pro;
Step 5:
select * from pg_get_backend_memory_contexts() where parent = 'CacheMemoryContext' and name = 'CachedPlan';
Step 6:
create or replace procedure test_pro() as $$
declare
va int default 100;
begin
for i in 1 .. 10 loop
va := va + i;
end loop;
raise notice '%', va;
va := va;
end $$ LANGUAGE plpgsql;
Step 7:
call test_pro();
Step 8:
select * from pg_get_backend_memory_contexts() where parent = 'CacheMemoryContext' and name = 'CachedPlan';
So in my opinion, the cached plan will not be reused but will constantly occupy resources.
regards,
Zeng Man
Man Zeng писал(а) 2025-08-19 11:28:
So in my opinion, the cached plan will not be reused but will
constantly occupy resources.regards,
Zeng Man
Hi!
In your example function will be compiled (the tree is created in the
memory)
and executed.
During execution this function creates a plan for very simple query 1
and stores it in the cache, than it creates a plan for query 10 and
store
in in the cache. There is no other queries, no more plans in the cache.
After execution function releases the tree (own code) from memory,
but cached queries are remains in the memory, it is why this cache is
created -
for usage by other queries.
I do not know, what other cache of the stored procedure do you mean. I
suppose,
a stored procedure creates cached plans only for queries inside of the
procedure,
not for itself.
--
Best regards,
Vladlen Popolitov.
Vladlen Popolitov 2025-08-19 08:39:50 wrote:> Hi!
>
> In your example function will be compiled (the tree is created in the
> memory)
> and executed.
> During execution this function creates a plan for very simple query 1
> and stores it in the cache, than it creates a plan for query 10 and
> store
> in in the cache. There is no other queries, no more plans in the cache.
> After execution function releases the tree (own code) from memory,
> but cached queries are remains in the memory, it is why this cache is
> created -
> for usage by other queries.
>
> I do not know, what other cache of the stored procedure do you mean. I
> suppose,
> a stored procedure creates cached plans only for queries inside of the
> procedure,
> not for itself.
>
> --
> Best regards,
>
> Vladlen Popolitov.
I think you misunderstand Man's meaning. In Man's example, the func cache
neither dropped (will cause memory leak) nor reused. So the question here:
1. Drop the cache when func dropped
2. Keep the cache and reused when func recreate
I prefer the 2ed solution.
Regards,
Jet C.X. ZHANG
Halo Tech (www.halodbtech.com)
openHalo (www.openhalo.org)
Import Notes
Resolved by subject fallback
"=?utf-8?B?56ug5pmo5pum?=" <zhangchenxi@halodbtech.com> writes:
I think you misunderstand Man's meaning. In Man's example, the func cache
neither dropped (will cause memory leak) nor reused. So the question here:
1. Drop the cache when func dropped
2. Keep the cache and reused when func recreate
I prefer the 2ed solution.
There is no provision for re-using a plancache entry by discovering
that the query you want to cache matches some existing entry.
So your option 2 is not going to happen, at least not without a
large amount of new code. Furthermore, even if we wrote that code,
would it help much? There's not a lot of reason to think that
a recreated version of the plpgsql function would contain exactly
the same queries as before. There might be more reason to hope
that small plans (like for individual expressions) could be shared
among multiple plpgsql functions, but I'm still skeptical that
it'd be worth doing.
I think that Man's proposal to drop the plpgsql function-cache entry
and the plancache entries that it links to is a reasonable idea,
although of course it will only help in scenarios that may not be
common. (I doubt that typical applications have a lot of run-time
churn in pg_proc.)
BTW, could people try harder to maintain the thread links when
replying? This conversation has already degenerated into several
not-cross-linked threads in the archives. It looks like the
problem is that some of you are using "X-mailer: QQMail 2.x",
which apparently doesn't feel a need to generate References:
or In-reply-to: headers. That's super unfriendly behavior for
mailing lists.
regards, tom lane
章晨曦 писал(а) 2025-08-19 12:31:
I think you misunderstand Man's meaning. In Man's example, the func
cache
neither dropped (will cause memory leak) nor reused. So the question
here:1. Drop the cache when func dropped
2. Keep the cache and reused when func recreate
I prefer the 2ed solution.Regards,
Jet C.X. ZHANG
Hi
It is why I am asking - the patch does not have a detail
description in text, it is not clear what and why is changed.
What do you mean, when speak about function cache?
What object or function allocate it? I wrote
in my previous email, that function does not have any
cache, it deallocates after the execution everything allocated
and does not have cache. Cached plans are not function cache,
it is the cache of plans for future use by any other functions
or simple query with the same generic or custom plan. They all
are deallocated at least when user decides to close the connection.
Could you clarify, what function cache do you mean?
--
Best regards,
Vladlen Popolitov.
Vladlen Popolitov <v.popolitov@postgrespro.ru> writes:
What do you mean, when speak about function cache?
What I'm thinking about is the function parse-tree, which not
incidentally contains a bunch of links to cached plans (via SPI).
Looking at plpgsql_free_function_memory might clarify things
for you.
regards, tom lane
It seems not an easy task. And here is my new work of such task.
The main idea is register a sys cache callback in cached_function_compile
when not registered. So it will effect for all SPL.
And also introduce a new hash table to track the function for cache inval
callback. The procedure in callback to lookup for a function will be:
hashvalue
|
v
[lookup func_key_hashtable]
|
v
func_key
|
v
[lookup func_hashtable]
|
v
function
But still remain lots of work to consider. As we don't know what's the real
operation of this invalidation, that's may cause unnecessary deletion. e.g.
postgres=# create or replace function strtest() returns text as $$
postgres$# begin
postgres$# raise notice 'foo\\bar\041baz';
postgres$# return 'foo\\bar\041baz';
postgres$# end
postgres$# $$ language plpgsql;
WARNING: nonstandard use of \\ in a string literal
LINE 3: raise notice 'foo\\bar\041baz';
^
HINT: Use the escape string syntax for backslashes, e.g., E'\\'.
WARNING: nonstandard use of \\ in a string literal
LINE 4: return 'foo\\bar\041baz';
^
HINT: Use the escape string syntax for backslashes, e.g., E'\\'.
WARNING: nonstandard use of \\ in a string literal
LINE 4: return 'foo\\bar\041baz';
^
HINT: Use the escape string syntax for backslashes, e.g., E'\\'.
CREATE FUNCTION
postgres=# select strtest();
WARNING: nonstandard use of \\ in a string literal <--- redundant warning
HINT: Use the escape string syntax for backslashes, e.g., E'\\'.
WARNING: nonstandard use of \\ in a string literal
HINT: Use the escape string syntax for backslashes, e.g., E'\\'.
NOTICE: foo\bar!baz
WARNING: nonstandard use of \\ in a string literal
LINE 1: 'foo\\bar\041baz'
^
HINT: Use the escape string syntax for backslashes, e.g., E'\\'.
QUERY: 'foo\\bar\041baz'
strtest
-------------
foo\bar!baz
(1 row)
the function works no error, but cause deplicate warning message. i'm still working on this...
Attachments:
001_cleanup_spl_funccache_v1.patchapplication/octet-stream; charset=utf-8; name=001_cleanup_spl_funccache_v1.patchDownload
diff --git a/src/backend/utils/cache/funccache.c b/src/backend/utils/cache/funccache.c
index afc048a051e..06ac6f2a7a9 100644
--- a/src/backend/utils/cache/funccache.c
+++ b/src/backend/utils/cache/funccache.c
@@ -30,6 +30,7 @@
#include "funcapi.h"
#include "utils/funccache.h"
#include "utils/hsearch.h"
+#include "utils/inval.h"
#include "utils/syscache.h"
@@ -37,6 +38,9 @@
* Hash table for cached functions
*/
static HTAB *cfunc_hashtable = NULL;
+static HTAB *cfunc_key_hashtable = NULL;
+
+static bool funccache_inval_registed = false;
typedef struct CachedFunctionHashEntry
{
@@ -44,10 +48,18 @@ typedef struct CachedFunctionHashEntry
CachedFunction *function; /* points to data of language-specific size */
} CachedFunctionHashEntry;
+typedef struct CachedFunctionKeyEntry
+{
+ uint32 keyhash;
+ CachedFunctionHashKey *key;
+} CachedFunctionKeyEntry;
+
#define FUNCS_PER_USER 128 /* initial table size */
static uint32 cfunc_hash(const void *key, Size keysize);
static int cfunc_match(const void *key1, const void *key2, Size keysize);
+static uint32 cfuncoid_hash(Oid funcoid);
+static void InvalidateFuncPlanCacheCallback(Datum arg, int cacheid, uint32 hashvalue);
/*
@@ -67,10 +79,27 @@ cfunc_hashtable_init(void)
ctl.entrysize = sizeof(CachedFunctionHashEntry);
ctl.hash = cfunc_hash;
ctl.match = cfunc_match;
+ ctl.hcxt = CacheMemoryContext;
cfunc_hashtable = hash_create("Cached function hash",
FUNCS_PER_USER,
&ctl,
- HASH_ELEM | HASH_FUNCTION | HASH_COMPARE);
+ HASH_ELEM | HASH_FUNCTION | HASH_COMPARE | HASH_CONTEXT);
+}
+
+static void
+cfunc_key_hashtable_init(void)
+{
+ HASHCTL ctl2;
+
+ Assert(cfunc_key_hashtable == NULL);
+
+ ctl2.keysize = sizeof(uint32);
+ ctl2.entrysize = sizeof(CachedFunctionKeyEntry);
+ ctl2.hcxt = CacheMemoryContext;
+ cfunc_key_hashtable = hash_create("Cached function hash",
+ FUNCS_PER_USER,
+ &ctl2,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
}
/*
@@ -168,7 +197,9 @@ cfunc_hashtable_insert(CachedFunction *function,
CachedFunctionHashKey *func_key)
{
CachedFunctionHashEntry *hentry;
+ CachedFunctionKeyEntry *kentry;
bool found;
+ uint32 keyhash;
if (cfunc_hashtable == NULL)
cfunc_hashtable_init();
@@ -198,6 +229,16 @@ cfunc_hashtable_insert(CachedFunction *function,
/* Set back-link from function to hashtable key */
function->fn_hashkey = &hentry->key;
+
+ /* Insert function key entry */
+ if (cfunc_key_hashtable == NULL)
+ cfunc_key_hashtable_init();
+
+ keyhash = cfuncoid_hash(function->fn_hashkey->funcOid);
+ kentry = (CachedFunctionKeyEntry *) hash_search(cfunc_key_hashtable,
+ &keyhash,
+ HASH_ENTER, NULL);
+ kentry->key = function->fn_hashkey;
}
/*
@@ -208,11 +249,18 @@ cfunc_hashtable_delete(CachedFunction *function)
{
CachedFunctionHashEntry *hentry;
TupleDesc tupdesc;
+ uint32 keyhash;
/* do nothing if not in table */
if (function->fn_hashkey == NULL)
return;
+ /* Release key hash table entry */
+ keyhash = cfuncoid_hash(function->fn_hashkey->funcOid);
+ (void *) hash_search(cfunc_key_hashtable,
+ &keyhash,
+ HASH_REMOVE, NULL);
+
/*
* We need to free the callResultType if present, which is slightly tricky
* because it has to be valid during the hashtable search. Fortunately,
@@ -332,6 +380,18 @@ compute_function_hashkey(FunctionCallInfo fcinfo,
}
}
+static uint32
+cfuncoid_hash(Oid funcoid)
+{
+ uint32 hashValue = 0;
+ uint32 oneHash;
+
+ oneHash = murmurhash32((uint32) funcoid);
+ hashValue ^= oneHash;
+
+ return hashValue;
+}
+
/*
* This is the same as the standard resolve_polymorphic_argtypes() function,
* except that:
@@ -493,6 +553,16 @@ cached_function_compile(FunctionCallInfo fcinfo,
bool hashkey_valid = false;
bool new_function = false;
+ /*
+ * Register catalog invalidate callback
+ */
+ if (!funccache_inval_registed)
+ {
+ funccache_inval_registed = true;
+
+ CacheRegisterSyscacheCallback(PROCOID, InvalidateFuncPlanCacheCallback, (Datum) 0);
+ }
+
/*
* Lookup the pg_proc tuple by Oid; we'll need it in any case
*/
@@ -632,3 +702,47 @@ recheck:
*/
return function;
}
+
+/*
+ * Try to delete and release the plan cache from the hash table.
+ */
+static void
+InvalidateFuncPlanCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
+{
+ Assert(cacheid == PROCOID);
+
+ if (cfunc_hashtable == NULL || cfunc_key_hashtable == NULL)
+ return;
+
+ if (hashvalue == 0)
+ {
+ HASH_SEQ_STATUS scan;
+ CachedFunctionHashEntry *hentry;
+
+ hash_seq_init(&scan, cfunc_hashtable);
+ while ((hentry = (CachedFunctionHashEntry *) hash_seq_search(&scan)))
+ {
+ if (hentry->function == NULL)
+ continue;
+
+ /* ... and free */
+ delete_function(hentry->function);
+ }
+ }
+ else
+ {
+ CachedFunctionKeyEntry *kentry;
+ CachedFunction *function;
+ bool found;
+
+ kentry = (CachedFunctionKeyEntry *) hash_search(cfunc_key_hashtable,
+ &hashvalue, HASH_FIND, &found);
+ if (found)
+ {
+ function = cfunc_hashtable_lookup(kentry->key);
+
+ if (function)
+ delete_function(function);
+ }
+ }
+}
\ No newline at end of file
Attempt to clear the compilation warning, overwrite the plpgsql.out file, and update the patch.
Attachments:
002_cleanup_spl_funccache_v2.patchapplication/octet-stream; charset=gb18030; name=002_cleanup_spl_funccache_v2.patchDownload
diff --git a/src/backend/utils/cache/funccache.c b/src/backend/utils/cache/funccache.c
index afc048a051e..94f7c083df6 100644
--- a/src/backend/utils/cache/funccache.c
+++ b/src/backend/utils/cache/funccache.c
@@ -30,6 +30,7 @@
#include "funcapi.h"
#include "utils/funccache.h"
#include "utils/hsearch.h"
+#include "utils/inval.h"
#include "utils/syscache.h"
@@ -37,6 +38,9 @@
* Hash table for cached functions
*/
static HTAB *cfunc_hashtable = NULL;
+static HTAB *cfunc_key_hashtable = NULL;
+
+static bool funccache_inval_registed = false;
typedef struct CachedFunctionHashEntry
{
@@ -44,11 +48,18 @@ typedef struct CachedFunctionHashEntry
CachedFunction *function; /* points to data of language-specific size */
} CachedFunctionHashEntry;
+typedef struct CachedFunctionKeyEntry
+{
+ uint32 keyhash;
+ CachedFunctionHashKey *key;
+} CachedFunctionKeyEntry;
+
#define FUNCS_PER_USER 128 /* initial table size */
static uint32 cfunc_hash(const void *key, Size keysize);
static int cfunc_match(const void *key1, const void *key2, Size keysize);
-
+static uint32 cfuncoid_hash(Oid funcoid);
+static void InvalidateFuncPlanCacheCallback(Datum arg, int cacheid, uint32 hashvalue);
/*
* Initialize the hash table on first use.
@@ -67,10 +78,27 @@ cfunc_hashtable_init(void)
ctl.entrysize = sizeof(CachedFunctionHashEntry);
ctl.hash = cfunc_hash;
ctl.match = cfunc_match;
+ ctl.hcxt = CacheMemoryContext;
cfunc_hashtable = hash_create("Cached function hash",
FUNCS_PER_USER,
&ctl,
- HASH_ELEM | HASH_FUNCTION | HASH_COMPARE);
+ HASH_ELEM | HASH_FUNCTION | HASH_COMPARE | HASH_CONTEXT);
+}
+
+static void
+cfunc_key_hashtable_init(void)
+{
+ HASHCTL ctl2;
+
+ Assert(cfunc_key_hashtable == NULL);
+
+ ctl2.keysize = sizeof(uint32);
+ ctl2.entrysize = sizeof(CachedFunctionKeyEntry);
+ ctl2.hcxt = CacheMemoryContext;
+ cfunc_key_hashtable = hash_create("Cached function hash",
+ FUNCS_PER_USER,
+ &ctl2,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
}
/*
@@ -168,7 +196,9 @@ cfunc_hashtable_insert(CachedFunction *function,
CachedFunctionHashKey *func_key)
{
CachedFunctionHashEntry *hentry;
+ CachedFunctionKeyEntry *kentry;
bool found;
+ uint32 keyhash;
if (cfunc_hashtable == NULL)
cfunc_hashtable_init();
@@ -198,6 +228,16 @@ cfunc_hashtable_insert(CachedFunction *function,
/* Set back-link from function to hashtable key */
function->fn_hashkey = &hentry->key;
+
+ /* Insert function key entry */
+ if (cfunc_key_hashtable == NULL)
+ cfunc_key_hashtable_init();
+
+ keyhash = cfuncoid_hash(function->fn_hashkey->funcOid);
+ kentry = (CachedFunctionKeyEntry *) hash_search(cfunc_key_hashtable,
+ &keyhash,
+ HASH_ENTER, NULL);
+ kentry->key = function->fn_hashkey;
}
/*
@@ -208,11 +248,16 @@ cfunc_hashtable_delete(CachedFunction *function)
{
CachedFunctionHashEntry *hentry;
TupleDesc tupdesc;
+ uint32 keyhash;
/* do nothing if not in table */
if (function->fn_hashkey == NULL)
return;
+ /* Release key hash table entry */
+ keyhash = cfuncoid_hash(function->fn_hashkey->funcOid);
+ hash_search(cfunc_key_hashtable, &keyhash, HASH_REMOVE, NULL);
+
/*
* We need to free the callResultType if present, which is slightly tricky
* because it has to be valid during the hashtable search. Fortunately,
@@ -332,6 +377,18 @@ compute_function_hashkey(FunctionCallInfo fcinfo,
}
}
+static uint32
+cfuncoid_hash(Oid funcoid)
+{
+ uint32 hashValue = 0;
+ uint32 oneHash;
+
+ oneHash = murmurhash32((uint32) funcoid);
+ hashValue ^= oneHash;
+
+ return hashValue;
+}
+
/*
* This is the same as the standard resolve_polymorphic_argtypes() function,
* except that:
@@ -493,6 +550,16 @@ cached_function_compile(FunctionCallInfo fcinfo,
bool hashkey_valid = false;
bool new_function = false;
+ /*
+ * Register catalog invalidate callback
+ */
+ if (!funccache_inval_registed)
+ {
+ funccache_inval_registed = true;
+
+ CacheRegisterSyscacheCallback(PROCOID, InvalidateFuncPlanCacheCallback, (Datum) 0);
+ }
+
/*
* Lookup the pg_proc tuple by Oid; we'll need it in any case
*/
@@ -632,3 +699,48 @@ recheck:
*/
return function;
}
+
+
+/*
+ * Try to delete and release the plan cache from the hash table.
+ */
+static void
+InvalidateFuncPlanCacheCallback(Datum arg, int cacheid, uint32 hashvalue)
+{
+ Assert(cacheid == PROCOID);
+
+ if (cfunc_hashtable == NULL || cfunc_key_hashtable == NULL)
+ return;
+
+ if (hashvalue == 0)
+ {
+ HASH_SEQ_STATUS scan;
+ CachedFunctionHashEntry *hentry;
+
+ hash_seq_init(&scan, cfunc_hashtable);
+ while ((hentry = (CachedFunctionHashEntry *) hash_seq_search(&scan)))
+ {
+ if (hentry->function == NULL)
+ continue;
+
+ /* ... and free */
+ delete_function(hentry->function);
+ }
+ }
+ else
+ {
+ CachedFunctionKeyEntry *kentry;
+ CachedFunction *function;
+ bool found;
+
+ kentry = (CachedFunctionKeyEntry *) hash_search(cfunc_key_hashtable,
+ &hashvalue, HASH_FIND, &found);
+ if (found)
+ {
+ function = cfunc_hashtable_lookup(kentry->key);
+
+ if (function)
+ delete_function(function);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index d8ce39dba3c..2498a988e12 100644
--- a/src/test/regress/expected/plpgsql.out
+++ b/src/test/regress/expected/plpgsql.out
@@ -4593,6 +4593,10 @@ LINE 4: return 'foo\\bar\041baz';
^
HINT: Use the escape string syntax for backslashes, e.g., E'\\'.
select strtest();
+WARNING: nonstandard use of \\ in a string literal
+HINT: Use the escape string syntax for backslashes, e.g., E'\\'.
+WARNING: nonstandard use of \\ in a string literal
+HINT: Use the escape string syntax for backslashes, e.g., E'\\'.
NOTICE: foo\bar!baz
WARNING: nonstandard use of \\ in a string literal
LINE 1: 'foo\\bar\041baz'