Hi!

My colleagues found that a queries like that:

\timing

DROP TABLE IF EXISTS t;

CREATE TABLE t (
    id int,
    a int,
    b int,
    g text GENERATED ALWAYS AS (
        lpad((CASE WHEN a <> 0 THEN a ELSE b END)::text, 10, '0') ||
        lpad((CASE WHEN a <> 0 THEN a ELSE b END)::text, 10, '0') ||
        lpad((CASE WHEN a <> 0 THEN a ELSE b END)::text, 10, '0') ||
        lpad((CASE WHEN a <> 0 THEN a ELSE b END)::text, 10, '0') ||
        lpad((CASE WHEN a <> 0 THEN a ELSE b END)::text, 10, '0') ||
        lpad((CASE WHEN a <> 0 THEN a ELSE b END)::text, 10, '0') ||
        lpad((CASE WHEN a <> 0 THEN a ELSE b END)::text, 10, '0') ||
        lpad((CASE WHEN a <> 0 THEN a ELSE b END)::text, 10, '0') ||
        lpad((CASE WHEN a <> 0 THEN a ELSE b END)::text, 10, '0') ||
        lpad((CASE WHEN a <> 0 THEN a ELSE b END)::text, 10, '0')
    ) STORED
);

INSERT INTO t
SELECT 1, 100, 0
FROM generate_series(1, 1000000);

UPDATE t SET id = 2; -- < problem query

lead to excessive memory consumption up to 10Gb in this example and
query execution time up to ~1,5min.

Bisect shows that the problem appeared after commit 83ea6c540
(Virtual generated columns).

Before this commit the update query took only ~8s and the memory
consumption did not exceed 150Mb for this backend.
MemoryContextStats reports only a small amount of memory usage, while
malloc_stats() confirms large allocations outside PostgreSQL memory
contexts.

With help of massif tool i found repeated allocations originating from:

    ExecInitGenerated
      → build_column_default
        → stringToNode

This indicates that generated expressions are reparsed multiple times,
once per row to be updated instead of being reused.

There is a problem call stack during UPDATE t SET id = 2;
execution: see attached bt.txt, please.

Before the above-mentioned commit, ExecInitGenerated() was effectively
invoked once per ResultRelInfo, so this behavior was not observable.

I would like to propose a fix that add a caching of the the parsed
expression trees (Node *) in ResultRelInfo, so that build_column_default()
and stringToNode() are executed at most once per attribute per query.

With this fix, the query execution time
and memory consumption return to normal:

postgres=# UPDATE t SET id = 2;
UPDATE 1000000
Time: 11522,621 ms (00:11,523)


A patch for this approach for current master is attached here.


Would be glad for any feedback.

Best regards,

--
Anton A. Melnikov
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company
(gdb) bt full
#0  ExecInitGenerated (resultRelInfo=0x556c9d23fa48, estate=0x556c9d23f500, 
cmdtype=CMD_UPDATE) at nodeModifyTable.c:488
        expr = 0x66dea1c0
        attgenerated = 115 's'
        i = 3
        rel = 0x7f6fca5024f8
        tupdesc = 0x7f6fca502708
        natts = 4
        ri_GeneratedExprs = 0x556c9d2212a8
        ri_NumGeneratedNeeded = 0
        updatedCols = 0x556c9d11c258
        oldContext = 0x556c9d23f400
#1  0x0000556c720bb1d9 in ExecComputeStoredGenerated 
(resultRelInfo=0x556c9d23fa48, estate=0x556c9d23f500, slot=0x556c9d220948, 
cmdtype=CMD_UPDATE) at nodeModifyTable.c:579
        rel = 0x7f6fca5024f8
        tupdesc = 0x7f6fca502708
        natts = 4
        econtext = 0x556c9d221198
        ri_GeneratedExprs = 0x556c9d2209c0
        oldContext = 0x7f6fca502708
        values = 0x0
        nulls = 0x0
#2  0x0000556c720c39ed in ExecUpdatePrepareSlot (resultRelInfo=0x556c9d23fa48, 
slot=0x556c9d220948, estate=0x556c9d23f500) at nodeModifyTable.c:2181
        resultRelationDesc = 0x7f6fca5024f8
#3  0x0000556c720c327e in ExecUpdateAct (context=0x7fff66dea550, 
resultRelInfo=0x556c9d23fa48, tupleid=0x7fff66dea51a, oldtuple=0x0, 
slot=0x556c9d220948, canSetTag=true, updateCxt=0x7fff66dea404) at 
nodeModifyTable.c:2218
        estate = 0x556c9d23f500
        resultRelationDesc = 0x7f6fca5024f8
        partition_constraint_failed = false
        result = 2636255368
#4  0x0000556c720c0758 in ExecUpdate (context=0x7fff66dea550, 
resultRelInfo=0x556c9d23fa48, tupleid=0x7fff66dea51a, oldtuple=0x0, 
oldSlot=0x556c9d220838, slot=0x556c9d220948, canSetTag=true) at 
nodeModifyTable.c:2560
        lockedtid = {ip_blkid = {bi_hi = 0, bi_lo = 0}, ip_posid = 6}
        estate = 0x556c9d23f500
        resultRelationDesc = 0x7f6fca5024f8
        updateCxt = {crossPartUpdate = false, updateIndexes = TU_None, lockmode 
= LockTupleKeyShare}
        result = 2636253512
#5  0x0000556c720bdcd7 in ExecModifyTable (pstate=0x556c9d23f838) at 
nodeModifyTable.c:4727
        node = 0x556c9d23f838
        context = {mtstate = 0x556c9d23f838, epqstate = 0x556c9d23f920, estate 
= 0x556c9d23f500, planSlot = 0x556c9d241358, tmfd = {ctid = {ip_blkid = {bi_hi 
= 24, bi_lo = 0}, ip_posid = 0}, xmax = 2636379920, cmax = 21868, traversed = 
226}, cpDeletedSlot = 0x0, cpUpdateReturningSlot = 0x7fff66dea5b0}
        estate = 0x556c9d23f500
        operation = CMD_UPDATE
        resultRelInfo = 0x556c9d23fa48
        subplanstate = 0x556c9d23fd28
        slot = 0x556c9d220948
        oldSlot = 0x556c9d220838
        tuple_ctid = {ip_blkid = {bi_hi = 0, bi_lo = 0}, ip_posid = 6}
        oldtupdata = {t_len = 8, t_self = {ip_blkid = {bi_hi = 0, bi_lo = 0}, 
ip_posid = 7328}, t_tableOid = 0, t_data = 0x0}
        oldtuple = 0x0
        tupleid = 0x7fff66dea51a
        tuplock = false
#6  0x0000556c72078de2 in ExecProcNodeFirst (node=0x556c9d23f838) at 
execProcnode.c:470
No locals.
#7  0x0000556c72070082 in ExecProcNode (node=0x556c9d23f838) at 
../../../src/include/executor/executor.h:320
No locals.
#8  0x0000556c7206ad93 in ExecutePlan (queryDesc=0x556c9d2388d0, 
operation=CMD_UPDATE, sendTuples=false, numberTuples=0, 
direction=ForwardScanDirection, dest=0x556c9d242ab0) at execMain.c:1726
        estate = 0x556c9d23f500
        planstate = 0x556c9d23f838
        use_parallel_mode = false
        slot = 0x556c9d15e428
        current_tuple_count = 0
#9  0x0000556c7206ac22 in standard_ExecutorRun (queryDesc=0x556c9d2388d0, 
direction=ForwardScanDirection, count=0) at execMain.c:368
        estate = 0x556c9d23f500
        operation = CMD_UPDATE
        dest = 0x556c9d242ab0
        sendTuples = false
        oldcontext = 0x556c9d2387d0
#10 0x0000556c7206aa7a in ExecutorRun (queryDesc=0x556c9d2388d0, 
direction=ForwardScanDirection, count=0) at execMain.c:305
No locals.
#11 0x0000556c723bee23 in ProcessQuery (plan=0x556c9d241a88, 
sourceText=0x556c9d11ad50 "UPDATE t SET id = 2;", params=0x0, queryEnv=0x0, 
dest=0x556c9d242ab0, qc=0x7fff66dea9a8) at pquery.c:161
        queryDesc = 0x556c9d2388d0
#12 0x0000556c723bde66 in PortalRunMulti (portal=0x556c9d1bd8b0, 
isTopLevel=true, setHoldSnapshot=false, dest=0x556c9d242ab0, 
altdest=0x556c9d242ab0, qc=0x7fff66dea9a8) at pquery.c:1268
        pstmt = 0x556c9d241a88
        stmtlist_item__state = {l = 0x556c9d242a60, i = 0}
        active_snapshot_set = true
        stmtlist_item = 0x556c9d242a78
#13 0x0000556c723bd4f3 in PortalRun (portal=0x556c9d1bd8b0, 
count=9223372036854775807, isTopLevel=true, dest=0x556c9d242ab0, 
altdest=0x556c9d242ab0, qc=0x7fff66dea9a8) at pquery.c:783
        _save_exception_stack = 0x7fff66deab50
        _save_context_stack = 0x0
        _local_sigjmp_buf = {{__jmpbuf = {93924264249040, 5807493733627851093, 
93924254207184, 140734919259808, 0, 0, 5807493733992755541, 
413896871259795797}, __mask_was_saved = 0, __saved_mask = {__val = 
{93924263862225, 5807493733990658389, 413896870674035029, 2416, 128, 
19815728600, 0, 93924981216272, 93924980010064, 2635856208, 112, 
93924980010064, 93924981222064, 140734919256288, 93924263927232, 
140734919256288}}}}
        _do_rethrow = false
        result = false
        nprocessed = 93924981222064
        saveTopTransactionResourceOwner = 0x556c9d15aa38
        saveTopTransactionContext = 0x556c9d1cfcd0
        saveActivePortal = 0x0
        saveResourceOwner = 0x556c9d15aa38
        savePortalContext = 0x0
        saveMemoryContext = 0x556c9d1cfcd0
#14 0x0000556c723b8216 in exec_simple_query (query_string=0x556c9d11ad50 
"UPDATE t SET id = 2;") at postgres.c:1278
        cmdtaglen = 6
        snapshot_set = true
        per_parsetree_context = 0x0
        plantree_list = 0x556c9d242a60
        parsetree = 0x556c9d11b8b8
        commandTag = CMDTAG_UPDATE
        qc = {commandTag = CMDTAG_UNKNOWN, nprocessed = 0}
        querytree_list = 0x556c9d11c378
        portal = 0x556c9d1bd8b0
        receiver = 0x556c9d242ab0
        format = 0
        cmdtagname = 0x556c727b651a "UPDATE"
        parsetree_item__state = {l = 0x556c9d11b8e8, i = 0}
        dest = DestRemote
        oldcontext = 0x556c9d1cfcd0
        parsetree_list = 0x556c9d11b8e8
        parsetree_item = 0x556c9d11b900
        save_log_statement_stats = false
        was_logged = false
        use_implicit_block = false
        msec_str = "\000\252\336f\377\177\000\000\065W`rlU\000\000 
\252\336f\024\000\000\000P\255\021\235lU\000"
#15 0x0000556c723b7449 in PostgresMain (dbname=0x556c9d159058 "postgres", 
username=0x556c9d159038 "postgres") at postgres.c:4810
        query_string = 0x556c9d11ad50 "UPDATE t SET id = 2;"
        firstchar = 81
        input_message = {data = 0x556c9d11ad50 "UPDATE t SET id = 2;", len = 
21, maxlen = 1024, cursor = 21}
        local_sigjmp_buf = {{__jmpbuf = {93924264249040, 5807493733795623253, 
93924254207184, 140734919259808, 0, 0, 5807493733604782421, 
413896873920295253}, __mask_was_saved = 1, __saved_mask = {__val = {4194304, 
93924979975808, 93924980289560, 140734919257056, 93924263926944, 
140734919257136, 93924979975808, 93924979975808, 93924979975808, 
140734919257088, 93924261123494, 0, 93924979975808, 140734919257136, 
93924261236879, 71354625712}}}}
        send_ready_for_query = false
        idle_in_transaction_timeout_enabled = false
        idle_session_timeout_enabled = false
#16 0x0000556c723af2e3 in BackendMain (startup_data=0x7fff66deacc8, 
startup_data_len=24) at backend_startup.c:124
        bsdata = 0x7fff66deacc8
#17 0x0000556c7228a1d9 in postmaster_child_launch (child_type=B_BACKEND, 
child_slot=1, startup_data=0x7fff66deacc8, startup_data_len=24, 
client_sock=0x7fff66dead28) at launch_backend.c:263
        pid = 0
#18 0x0000556c72291482 in BackendStartup (client_sock=0x7fff66dead28) at 
postmaster.c:3606
        bn = 0x556c9d149e08
        pid = 64
        startup_data = {canAcceptConnections = CAC_OK, socket_created = 
828192736536497, fork_started = 828192736536500}
        cac = CAC_OK
#19 0x0000556c7228f399 in ServerLoop () at postmaster.c:1713
        s = {sock = 9, raddr = {addr = {ss_family = 1, __ss_padding = 
"\000\000\000\000\000\000Y\320z\326o\177\000\000`\256\336f", '\000' <repeats 12 
times>, 
"\377\377\377\377\000\000\000\000\356\310\347Y\000\000\000\000\250wF\327o\177\000\000\250\256\336f\377\177\000\000z\360\006\327o\177\000\000\000\256\336f\377\177\000\000\020\256\336f\377\177\000\000\361!I\327o\177\000\000\f\000\000\000\000\000\000\000\030\343\370\323o\177\000\000\001\000\000\000\000\000\000",
 __ss_align = 0}, salen = 2}}
        i = 0
        now = 1774877531
        last_lockfile_recheck_time = 1774877525
        last_touch_time = 1774876384
        events = {{pos = 2, events = 2, fd = 7, user_data = 0x0}, {pos = 0, 
events = 0, fd = 1, user_data = 0x0}, {pos = -1, events = 0, fd = 1725869624, 
user_data = 0x7f6fd671f4a8}, {pos = -687470336, events = 32623, fd = 32816, 
user_data = 0xffffffffffffffb0}, {pos = 1725871776, events = 32767, fd = 0, 
user_data = 0x0}, {pos = -696561324, events = 32623, fd = 32768, user_data = 
0x7f6fd70890f8 <[email protected]>}, {pos = 1725870896, events = 32767, fd = 
1909378256, user_data = 0x7fff66deb6a0}, {pos = 0, events = 0, fd = 0, 
user_data = 0x7f6fd7497197 <_dl_fixup+215>}, {pos = 1, events = 0, fd = 0, 
user_data = 0x1000}, {pos = -697174872, events = 32623, fd = 1725870672, 
user_data = 0x7f6fd749ec3e <_dl_runtime_resolve_xsavec+126>}, {pos = 0, events 
= 0, fd = 0, user_data = 0x7fff66deb2a0}, {pos = 1923292504, events = 21868, fd 
= 2, user_data = 0x556c72a31d58 <BlockSig>}, {pos = 1725923456, events = 32767, 
fd = 0, user_data = 0x0}, {pos = -696282847, events = 32623, fd = 1725869888, 
user_data = 0x2ffff00001fa0}, {pos = 1919420112, events = 21868, fd = 2, 
user_data = 0x71ceccd0}, {pos = -696568333, events = 32623, fd = -1659558320, 
user_data = 0x7f6fd6907be0 <main_arena+96>}, {pos = 7, events = 0, fd = 
-1660034546, user_data = 0x1000}, {pos = 99, events = 0, fd = -1660033016, 
user_data = 0x1030}, {pos = 64, events = 0, fd = 257, user_data = 0x0}, {pos = 
0, events = 0, fd = 0, user_data = 0x0}, {pos = 0, events = 0, fd = 0, 
user_data = 0xffffffffffffffff}, {pos = -1, events = 4294967295, fd = 
1935764781, user_data = 0x3737310a61746164}, {pos = 0, events = 0, fd = 0, 
user_data = 0x7f6fd6907be0 <main_arena+96>}, {pos = 0, events = 0, fd = 2, 
user_data = 0x0}, {pos = 0, events = 0, fd = 0, user_data = 0x0}, {pos = 0, 
events = 0, fd = 0, user_data = 0x0}, {pos = 0, events = 0, fd = 0, user_data = 
0x2}, {pos = 2, events = 0, fd = 16449785, user_data = 0x0}, {pos = 16646394, 
events = 0, fd = 16711931, user_data = 0x0}, {pos = 0, events = 0, fd = 0, 
user_data = 0x0}, {pos = 16, events = 0, fd = -1659594464, user_data = 
0x556c9d153670}, {pos = -1659595456, events = 1, fd = 4, user_data = 0x2d0}, 
{pos = 784, events = 0, fd = -1701897728, user_data = 0x1400}, {pos = 
1919420112, events = 21868, fd = 1725870448, user_data = 0x556c72621ad7 
<wipe_mem+39>}, {pos = 2, events = 0, fd = 6, user_data = 0x0}, {pos = 0, 
events = 0, fd = 0, user_data = 0x0}, {pos = 0, events = 0, fd = 0, user_data = 
0x4}, {pos = -1659595280, events = 21868, fd = 1024, user_data = 0x0}, {pos = 
-1659595456, events = 21868, fd = -1659595456, user_data = 0x7fff66deb190}, 
{pos = 1919090397, events = 21868, fd = -1659595456, user_data = 
0x556c9d149540}, {pos = 1725870560, events = 32767, fd = 1919032236, user_data 
= 0x400}, {pos = -1659599512, events = 21868, fd = 0, user_data = 
0x556c72a4b2a0 <context_freelists+16>}, {pos = 1024, events = 0, fd = 
-1659554192, user_data = 0x556c9d149540}, {pos = -1659595456, events = 21868, 
fd = 1725870592, user_data = 0x7f6fd67adc5a <_IO_flush_all_lockp+554>}, {pos = 
1725871776, events = 1, fd = -1659595456, user_data = 0x7f6fd67abfb0 
<flush_cleanup>}, {pos = 0, events = 0, fd = 0, user_data = 0x7f6fd67adc5a 
<_IO_flush_all_lockp+554>}, {pos = -1659595456, events = 1, fd = -1701897728, 
user_data = 0x7f6fd67abfb0 <flush_cleanup>}, {pos = 0, events = 0, fd = 0, 
user_data = 0x0}, {pos = 1725870896, events = 32767, fd = -1701897728, 
user_data = 0x556c726806d0 <__libc_csu_init>}, {pos = 0, events = 0, fd = 0, 
user_data = 0x7f6fd67fe00d <__libc_fork+285>}, {pos = 1725870944, events = 
32767, fd = -1701897728, user_data = 0x7fff66deb6a0}, {pos = 1915264797, events 
= 21868, fd = 4194304, user_data = 0x556c72621b51 <AllocSetDelete+97>}, {pos = 
0, events = 0, fd = -696660285, user_data = 0x13726806d0}, {pos = 13, events = 
21868, fd = 4194304, user_data = 0x556c725e4c18 <should_output_to_server+40>}, 
{pos = -1659604656, events = 21868, fd = 13, user_data = 0x1366deb330}, {pos = 
13, events = 21868, fd = 1725870880, user_data = 0x556c725e4c18 
<should_output_to_server+40>}, {pos = 1725870896, events = 32767, fd = 13, 
user_data = 0x7fff66deb360}, {pos = 1918782979, events = 21868, fd = 
1725870960, user_data = 0x556c9d14d2d8}, {pos = 1725870944, events = 32767, fd 
= 1915273988, user_data = 0x0}, {pos = 13, events = 372808, fd = 1725871008, 
user_data = 0x556c7228a0c0 <postmaster_child_launch+128>}, {pos = 1725871024, 
events = 32767, fd = 1915285345, user_data = 0x1}, {pos = 2, events = 0, fd = 
1725871072, user_data = 0x556c7228f1c0 <maybe_start_bgworkers+400>}, {pos = 
-1659783720, events = 21868, fd = 1915285345, user_data = 0x556c9d11b000}, {pos 
= 1923181696, events = 21868, fd = -1659783720, user_data = 0x556c72a16c80 
<BackgroundWorkerList>}}
        nevents = 1
#20 0x0000556c7228e0b5 in PostmasterMain (argc=3, argv=0x556c9d1125f0) at 
postmaster.c:1403
        opt = -1
        status = 0
        userDoption = 0x556c9d119ba0 "/usr/local/pgvan-master/data"
        listen_addr_saved = true
        output_config_variable = 0x0
#21 0x0000556c72114136 in main (argc=3, argv=0x556c9d1125f0) at main.c:231
        do_check_root = true
        dispatch_option = DISPATCH_POSTMASTER
From fc0e4763fd44c1e1571387c39b9cb5a0c8167730 Mon Sep 17 00:00:00 2001
From: "Anton A. Melnikov" <[email protected]>
Date: Fri, 27 Mar 2026 11:32:25 +0300
Subject: [PATCH] Avoid repeated parsing of generated column expressions.

After commit 83ea6c54, execution paths were introduced
where ExecInitGenerated() can be invoked per row
for the same ResultRelInfo during a single query.
Because the parsed expression trees are allocated
in es_query_cxt, each call results in additional
allocations that are not freed until query end.

Fix it by caching the parsed expression trees
(Node *) in ResultRelInfo,
ensuring that build_column_default() and stringToNode()
are executed only once per attribute per query
and subsequent calls reuse the cached parse trees.

This restores the old behavior where expression parsing
is done once per query and prevents unbounded memory growth.
---
 src/backend/executor/execMain.c        |  1 +
 src/backend/executor/nodeModifyTable.c | 14 +++++++++++++-
 src/include/nodes/execnodes.h          |  3 +++
 3 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 58b84955c2b..b3d2304624e 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1311,6 +1311,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
 	resultRelInfo->ri_GenVirtualNotNullConstraintExprs = NULL;
 	resultRelInfo->ri_GeneratedExprsI = NULL;
 	resultRelInfo->ri_GeneratedExprsU = NULL;
+	resultRelInfo->ri_GeneratedExprsParsed = NULL;
 	resultRelInfo->ri_projectReturning = NULL;
 	resultRelInfo->ri_onConflictArbiterIndexes = NIL;
 	resultRelInfo->ri_onConflict = NULL;
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 4cd5e262e0f..451a38bc7c9 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -485,7 +485,19 @@ ExecInitGenerated(ResultRelInfo *resultRelInfo,
 			Expr	   *expr;
 
 			/* Fetch the GENERATED AS expression tree */
-			expr = (Expr *) build_column_default(rel, i + 1);
+			if (resultRelInfo->ri_GeneratedExprsParsed == NULL)
+				resultRelInfo->ri_GeneratedExprsParsed = palloc0(natts * sizeof(Node *));
+
+			if (resultRelInfo->ri_GeneratedExprsParsed[i] == NULL)
+			{
+				expr = (Expr *) build_column_default(rel, i + 1);
+				resultRelInfo->ri_GeneratedExprsParsed[i] = (Node *) expr;
+			}
+			else
+			{
+				expr = (Expr *) resultRelInfo->ri_GeneratedExprsParsed[i];
+			}
+
 			if (expr == NULL)
 				elog(ERROR, "no generation expression found for column number %d of table \"%s\"",
 					 i + 1, RelationGetRelationName(rel));
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 684e398f824..ca30137a308 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -582,6 +582,9 @@ typedef struct ResultRelInfo
 	int			ri_NumGeneratedNeededI;
 	int			ri_NumGeneratedNeededU;
 
+	/* Cached parsed GENERATED expressions */
+	Node      **ri_GeneratedExprsParsed;
+
 	/* list of RETURNING expressions */
 	List	   *ri_returningList;
 
-- 
2.50.1

Reply via email to