Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wKsLA-001Mub-1L for pgsql-bugs@arkaria.postgresql.org; Thu, 07 May 2026 06:36:08 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wKsL8-002HRx-1K for pgsql-bugs@arkaria.postgresql.org; Thu, 07 May 2026 06:36:06 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wKsL8-002HRp-0O for pgsql-bugs@lists.postgresql.org; Thu, 07 May 2026 06:36:06 +0000 Received: from mail-ed1-x534.google.com ([2a00:1450:4864:20::534]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wKsL5-00000000xML-3lVx for pgsql-bugs@lists.postgresql.org; Thu, 07 May 2026 06:36:05 +0000 Received: by mail-ed1-x534.google.com with SMTP id 4fb4d7f45d1cf-67c566cb519so971082a12.3 for ; Wed, 06 May 2026 23:36:03 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1778135761; cv=none; d=google.com; s=arc-20240605; b=eoG3fuR8rwwriZyTLxpK2oAOovuLt4IY5VbTdpZkOrfz4viIciX3xwGpaxirSYgkWn gX2kc8RugkWtxDkglHtNRHvAOHa1mYBm8zTfiaKstN1Tzq2cqcw7RLtjL37MgaH7CE5r KazhmYooHCc6R+Nk/HRvLmj8LLPxrbnOK+442kCtGnJ21gYExCUjb4X/501tT6gEjTO2 rQuoOQZr8pyEUCxrYKajPpemqcGLESOraR2nWqKOKWHaGTjHUWb6MEWL4dpSjt+iQP/J W5PaBTqkewGnibg38AEo8GNNDpWAY4b/AtaLMmktV7tvbqF20abCrDYznFQMNTOyNZk7 zd0A== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:dkim-signature; bh=O7BrwMA6CUhVN4rMI18thWW+ZKXLl9/IyfnJJ21n50Q=; fh=AScb96HsWHT50EJZIWQHngp/v1dycFvgfmGCQCZFATU=; b=f+E7OwBsmgzyVzLgyWXSiOBlOMn3SYmz+b1x4O6/Q4LR2hux+R1RDCzbVcIfqgmsRj MlvJDQgxXzuBUZ5j6rzZEnTSPy0cfe6NzQMeriSG7IKRG0Xz8lJGsLgDqTpjVWcBEO11 eMPtl3MJVgwaxxG9lwhf/hJvzHcrThyjTcwv5Ah0wBsVKnhuHhu83bfjdxRDir5qgu8P O2GxknguXqIZXVP2OwcRIgs/9VWrWVXXNBBSVpEmaZnOJs9ACsAjuHIGvSvCZ5D+JkSx tepbqFX+0DVG5t0SHEXdS+/oDNZ/boqAcDbZlTdOYUq93eQ+ph+ATfF+HM8OM31MWxdz cTwA==; darn=lists.postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778135761; x=1778740561; darn=lists.postgresql.org; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:from:to:cc:subject:date :message-id:reply-to; bh=O7BrwMA6CUhVN4rMI18thWW+ZKXLl9/IyfnJJ21n50Q=; b=Zb0Id4mEdeE825LozFiZ95YMfshQvBkC60Gnq/j7xAnirUyRPC2z8Kjen5oB2gnc0Q aZZER1RTNgaLJGkapK+Hne+PQvioaAJU1S+FHUMQVYiEC+tfGYQRvQ/4fFCJb7AALNN9 OPpCZGFnl5eto7Dbd2vvTKRAsqAziL1ZPr6olddavVq8ozMfU8CLA4spOnKVzxhyc6yo TqJwIV6Bg1dQTrbtZKTXDv+hV8dDbjknXPHXto60iyffmXNYFuTYtv1BOT4UDvG1hMtC 5zqstbKaOazXoh3QJE5nUeXX5vYoNyLskmUij4nRVyDQfJZivU6d/GsfpLxlKLIOIdzA 9+Pw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778135761; x=1778740561; h=content-transfer-encoding:cc:to:subject:message-id:date:from :in-reply-to:references:mime-version:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=O7BrwMA6CUhVN4rMI18thWW+ZKXLl9/IyfnJJ21n50Q=; b=o63u01GkVpDmi2V5Rf/o6EP9LbY5io93J1EIyiwg8Osz+avuYLbfUXx+X7c/PSudGv C9K9DvabOwS5ZtAoHx93wcmWKXvzu1fyfFh+T7B1c9YRCB42Rq8rURaqRSF195nfXwVf rieuAZEviGniN+i/vay0D06CFOjBUV1I1GDCFBB3LVC6pAHKGC9rNqLH0aeD4D/u3bpy 7f7Lf9cx0Eoqm60nRSLByDaXdWGigvu2TewzXxIbDRBBMvHqUA2xaVRkvHJtGBJs62Ll 27rEPKDb8pM3+B4RDjTYZ+Wvua2uShy69K23gDcwZ4vshV+g5YsujZbmuxMl8J55ISZk R/2Q== X-Gm-Message-State: AOJu0YxqpFubwIrrfW+txVujGVna3VmbsHQJRfiUCd/6xkpP8I1J/W1p FCVvxZec1YiuZkFnjBPHvtpbc8H2A1RKOZxQeUJ93/QaC40nP7nHs3g0icr61lup0vlMbBdZjCN ePXOmgbSv4NMlG3+0G+eNczc3oV6pD4zWW8d9K/CWcInp X-Gm-Gg: AeBDieuDBj3K79l+H16lDC4TUX1ugVu3b9OOfQrpkr6TTkmn0+IaGCurNFM9jANFdQn 014ueHAe3zOUsom+WYg7OVB9zbXEAiill1BxrdzxcPonxH1TL1s0z8T6HwCPSQNRtDmEvLnXH0Y oEDb+GyPTQojQxFrGwnxJYESrH0ctaKTyu9urvC8RhMtUNTuyBJf4KlZl8F8X9uB0uqLUZSkgOK 91OwR9Cpez+76AvE2rc4y2oAOLKvzUOIi3IJ8mVlDl3OLxrt+dJYlBIwcr5FHF/di5YLyEBS+OU PyJbe0+P/85PachjKAaczyVeVAuHwHE56vAkh3eScueQdGmwTQ/RZKOy4ZA11pDFDNlukAi26cv WYkdfon+FIjzsph72LPAZ X-Received: by 2002:a05:6402:1ec9:b0:676:d85a:66ea with SMTP id 4fb4d7f45d1cf-67d63d7ef8dmr3326234a12.5.1778135761139; Wed, 06 May 2026 23:36:01 -0700 (PDT) MIME-Version: 1.0 References: <19439-8f02018f9fc3a240@postgresql.org> In-Reply-To: From: Xuneng Zhou Date: Thu, 7 May 2026 14:35:49 +0800 X-Gm-Features: AVHnY4IsCEWflZ9NCsGxyfcKA3WFpHBzjDrLtv4SqUcWU3sEuuUHfxdgG9VxtCI Message-ID: Subject: Re: BUG #19439: pg_stat_xact_user_tables stat not currect during the transaction To: klemen kobau Cc: pgsql-bugs@lists.postgresql.org Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk Hi klemen, On Wed, May 6, 2026 at 4:03=E2=80=AFPM klemen kobau wrote: > > Hi Xuneng, > > Thanks for the analysis and the patch proposals. As the original reporter= , I wanted to follow up since the thread has been quiet for over a month. > > I am new to this process, do I need to do anything to help the process? > > Kind regards > > Klemen Kobau > > > On Wed, 6 May 2026 at 09:56, Xuneng Zhou wrote: >> >> On Sun, Mar 29, 2026 at 11:17=E2=80=AFAM Xuneng Zhou wrote: >> > >> > On Sat, Mar 28, 2026 at 1:47=E2=80=AFPM Xuneng Zhou wrote: >> > > >> > > On Fri, Mar 27, 2026 at 10:15=E2=80=AFAM Xuneng Zhou wrote: >> > > > >> > > > Hi klemen, >> > > > >> > > > Thanks for the report. >> > > > >> > > > On Fri, Mar 27, 2026 at 5:36=E2=80=AFAM PG Bug reporting form >> > > > wrote: >> > > > > >> > > > > The following bug has been logged on the website: >> > > > > >> > > > > Bug reference: 19439 >> > > > > Logged by: klemen kobau >> > > > > Email address: klemen.kobau@gmail.com >> > > > > PostgreSQL version: 18.0 >> > > > > Operating system: Linux (EndeavorOS) >> > > > > Description: >> > > > > >> > > > > I am running postgres:18.0 in a docker container, the configurat= ion is as >> > > > > follows: >> > > > > >> > > > > postgres: >> > > > > image: postgres:18.0 >> > > > > command: [ >> > > > > "postgres", >> > > > > "-N", "200", >> > > > > "-c", "max_prepared_transactions=3D100", >> > > > > "-c", "wal_level=3Dlogical", >> > > > > "-c", "max_wal_senders=3D10", >> > > > > "-c", "max_replication_slots=3D20", >> > > > > "-c", "wal_keep_size=3D10", >> > > > > "-c", "max_slot_wal_keep_size=3D1024" >> > > > > ] >> > > > > environment: >> > > > > POSTGRES_USER: postgres >> > > > > POSTGRES_PASSWORD: postgres >> > > > > POSTGRES_DB: postgres >> > > > > TZ: UTC >> > > > > PGTZ: UTC >> > > > > ports: >> > > > > - 5432:5432 >> > > > > volumes: >> > > > > - postgres_data:/var/lib/postgresql >> > > > > >> > > > > I use psql version 18.3. >> > > > > >> > > > > I run the following: >> > > > > >> > > > > psql -h localhost -p 5432 -U postgres -d postgres -c " >> > > > > -- Transaction 1: insert 1 row, check stats, commit >> > > > > BEGIN; >> > > > > CREATE TABLE IF NOT EXISTS temp.xact_test (id serial PRIMARY KEY= , val text); >> > > > > INSERT INTO temp.xact_test (val) VALUES ('a'); >> > > > > SELECT relname, n_tup_ins FROM pg_stat_xact_user_tables WHERE re= lname =3D >> > > > > 'xact_test'; >> > > > > COMMIT; >> > > > > >> > > > > -- Transaction 2: insert 1 row, check stats >> > > > > BEGIN; >> > > > > INSERT INTO temp.xact_test (val) VALUES ('b'); >> > > > > SELECT relname, n_tup_ins FROM pg_stat_xact_user_tables WHERE re= lname =3D >> > > > > 'xact_test'; >> > > > > ROLLBACK; >> > > > > >> > > > > -- Cleanup >> > > > > DROP TABLE temp.xact_test; >> > > > > " >> > > > > >> > > > > And the output is >> > > > > >> > > > > BEGIN >> > > > > CREATE TABLE >> > > > > INSERT 0 1 >> > > > > relname | n_tup_ins >> > > > > -----------+----------- >> > > > > xact_test | 1 >> > > > > (1 row) >> > > > > >> > > > > COMMIT >> > > > > BEGIN >> > > > > INSERT 0 1 >> > > > > relname | n_tup_ins >> > > > > -----------+----------- >> > > > > xact_test | 2 >> > > > > (1 row) >> > > > > >> > > > > ROLLBACK >> > > > > DROP TABLE >> > > > >> > > > I can also reproduce this behavior on HEAD. >> > > > >> > > > > This is not what I would expect from reading >> > > > > https://www.postgresql.org/docs/current/monitoring-stats.html#MO= NITORING-STATS-VIEWS, >> > > > > where it states >> > > > > >> > > > > > A transaction can also see its own statistics (not yet flushed= out to the >> > > > > shared memory statistics) in the views pg_stat_xact_all_tables, >> > > > > pg_stat_xact_sys_tables, pg_stat_xact_user_tables, and >> > > > > pg_stat_xact_user_functions. These numbers do not act as stated = above; >> > > > > instead they update continuously throughout the transaction. >> > > > > >> > > > > based on this, I would expect that the numbers are updated each = time an >> > > > > insert happens and that they are isolated per transaction. >> > > > > >> > > > >> > > > This snippet of the doc feels somewhat ambiguous. The current beha= vior >> > > > does not seem fully consistent with wording such as =E2=80=9Cthrou= ghout the >> > > > transaction.=E2=80=9D It also seems more appropriate for these tab= le views to >> > > > reflect stats within their own scopes, rather than cumulative valu= es >> > > > spanning txn boundaries. >> > > > >> > > >> > > I=E2=80=99ve looked into this issue and 'd like to propose a patch t= o address >> > > it. Feedback is very welcome. >> > > >> > > --- Root cause >> > > >> > > Since the stats subsystem was rewritten (commit 5891c7a8), each >> > > backend keeps a hash of pending stats entries that persist across >> > > transaction boundaries. Entries that flush successfully are deleted >> > > from the backend-local pending list; entries not flushed remain >> > > pending with their accumulated counters. Flushing is driven by >> > > pgstat_report_stat(), called from the ReadyForQuery path, and subjec= t >> > > to a rate limiter. >> > > >> > > The pg_stat_xact_* views read these pending entries directly via >> > > find_tabstat_entry() / find_funcstat_entry(). Both functions return >> > > the raw accumulated counters without per-transaction scoping. >> > > >> > > The bug is deterministic when multiple top-level transactions are >> > > processed within a single simple-query message, because there is no >> > > ReadyForQuery boundary between the transactions and therefore no >> > > opportunity to flush and remove the pending entry. >> > > >> > > --- Fix >> > > >> > > The patch introduces a per-entry "transaction baseline" =E2=80=94 a = lazy >> > > snapshot of the counters taken the first time an entry is touched in >> > > each new top-level transaction. The accessor functions >> > > (find_tabstat_entry, find_funcstat_entry) then subtract the baseline >> > > from the current counters, yielding only the current transaction's >> > > delta. >> > > >> > > The baseline is keyed by MyProc->vxid.lxid. A static inline helper, >> > > pgstat_ensure_xact_baseline(), is called at every nontransactional >> > > counter-increment site (the 7 event-counter macros in pgstat.h and t= he >> > > 4 non-inline counter functions in pgstat_relation.c). After the firs= t >> > > call per entry per transaction, the check reduces to a single intege= r >> > > comparison with a predictably not-taken branch. >> > > >> > > For function stats, a new PgStat_FunctionPending wrapper struct embe= ds >> > > PgStat_FunctionCounts at offset 0 with the baseline fields appended, >> > > so the flush callback requires only a trivial cast change. >> > > >> > > --- Testing >> > > >> > > The regression test is a TAP test under src/bin/psql/t/ that uses ps= ql >> > > -c to send multi-statement strings as single simple-query messages. >> > > src/bin/psql/t look like the right existing harness for the primary >> > > regression because psql -c sends the whole multi-statement string as >> > > one simple-query message via simple_action_list dispatch. By contras= t, >> > > ordinary pg_regress SQL files are executed by psql in file/stdin mod= e, >> > > which runs through MainLoop() and dispatches top-level statements on= e >> > > at a time via SendQuery(), creating ReadyForQuery boundaries that >> > > could mask the bug. >> > > >> > > The test covers three scenarios: table n_tup_ins and seq_scan >> > > counters, and function calls =E2=80=94 each verifying isolation acro= ss >> > > consecutive top-level transactions within a single message. >> > > >> > >> > postgres % cat output.txt >> > BEGIN >> > CREATE TABLE >> > INSERT 0 1 >> > relname | n_tup_ins >> > -----------+----------- >> > xact_test | 1 >> > (1 row) >> > >> > COMMIT >> > BEGIN >> > INSERT 0 1 >> > relname | n_tup_ins >> > -----------+----------- >> > xact_test | 2 >> > (1 row) >> > >> > ROLLBACK >> > DROP TABLE >> > >> > I can also reproduce this behavior in v13. So this issue pre-existed >> > the major refactor 5891c7a8. >> > >> >> After further thought of v1, I'd like to also propose an alternative >> approach. The root cause and testing rationale are the same as >> described upthread; what changes is how the baseline is established. >> >> --- Problems with the lazy (v1) approach >> The v1 patch snapshots the baseline lazily -- on every >> nontransactional counter increment, it checks whether baseline_lxid >> matches the current >> transaction and snapshots if not. This has two drawbacks: >> >> 1) Hot-path overhead >> Every pgstat_count_heap_scan(), pgstat_count_buffer_read_block(), etc. >> acquires a branch. After the first call per entry per transaction, the >> branch is predictably not-taken, but it could still present on some of >> the most frequently executed paths in the backend. >> >> 2) Maintenance burden >> Every new counter added to PgStat_TableCounts requires a corresponding >> baseline snapshot at its increment site. Miss one, and that counter >> silently leaks across transactions >> >> --- eager baseline sweep >> The attached patch records the baseline eagerly at transaction >> boundaries instead of lazily at counter-increment sites. >> pgstat_set_pending_baselines() iterates the pgStatPending list and >> snapshots each entry's current counts into an xact_baseline field via >> struct assignment. It is called from AtEOXact_PgStat() (after folding >> transactional counts and removing dropped entries) and from >> PostPrepare_PgStat() (after relation cleanup), covering commit, abort, >> and PREPARE TRANSACTION. The view accessors unconditionally subtract >> the baseline. For entries created in the current transaction, >> xact_baseline is zero-initialized, so the subtraction is a no-op. >> >> I don=E2=80=99t have a clear preference between the two approaches; both= are >> presented for review. >> It would be useful to verify the fix by manually applying the patch and building the instance. Additionally, a few issues surfaced after looking at it again, which I will update later. --=20 Best, Xuneng