Thread

Commits

Same data as JSON: GET /api/v1/messages/:b64id/commits the thread's linked commits as JSON, with link sources. API reference →
  1. pgstat: store statistics in shared memory.

  1. BUG #19439: pg_stat_xact_user_tables stat not currect during the transaction

    PG Bug reporting form <noreply@postgresql.org> — 2026-03-25T14:06:37Z

    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 configuration is as
    follows:
    
      postgres:
        image: postgres:18.0
        command: [
          "postgres",
          "-N", "200",
          "-c", "max_prepared_transactions=100",
          "-c", "wal_level=logical",
          "-c", "max_wal_senders=10",
          "-c", "max_replication_slots=20",
          "-c", "wal_keep_size=10",
          "-c", "max_slot_wal_keep_size=1024"
        ]
        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 relname =
    '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 relname =
    '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
    
    This is not what I would expect from reading
    https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-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.
    
    Kind regards
    
    Klemen Kobau
    
    
    
    
    
    
  2. Re: BUG #19439: pg_stat_xact_user_tables stat not currect during the transaction

    Xuneng Zhou <xunengzhou@gmail.com> — 2026-03-27T02:15:27Z

    Hi klemen,
    
    Thanks for the report.
    
    On Fri, Mar 27, 2026 at 5:36 AM PG Bug reporting form
    <noreply@postgresql.org> 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 configuration is as
    > follows:
    >
    >   postgres:
    >     image: postgres:18.0
    >     command: [
    >       "postgres",
    >       "-N", "200",
    >       "-c", "max_prepared_transactions=100",
    >       "-c", "wal_level=logical",
    >       "-c", "max_wal_senders=10",
    >       "-c", "max_replication_slots=20",
    >       "-c", "wal_keep_size=10",
    >       "-c", "max_slot_wal_keep_size=1024"
    >     ]
    >     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 relname =
    > '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 relname =
    > '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#MONITORING-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 behavior
    does not seem fully consistent with wording such as “throughout the
    transaction.” It also seems more appropriate for these table views to
    reflect stats within their own scopes, rather than cumulative values
    spanning txn boundaries.
    
    
    --
    Best,
    Xuneng
    
    
    
    
  3. Re: BUG #19439: pg_stat_xact_user_tables stat not currect during the transaction

    Xuneng Zhou <xunengzhou@gmail.com> — 2026-03-28T05:47:51Z

    On Fri, Mar 27, 2026 at 10:15 AM Xuneng Zhou <xunengzhou@gmail.com> wrote:
    >
    > Hi klemen,
    >
    > Thanks for the report.
    >
    > On Fri, Mar 27, 2026 at 5:36 AM PG Bug reporting form
    > <noreply@postgresql.org> 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 configuration is as
    > > follows:
    > >
    > >   postgres:
    > >     image: postgres:18.0
    > >     command: [
    > >       "postgres",
    > >       "-N", "200",
    > >       "-c", "max_prepared_transactions=100",
    > >       "-c", "wal_level=logical",
    > >       "-c", "max_wal_senders=10",
    > >       "-c", "max_replication_slots=20",
    > >       "-c", "wal_keep_size=10",
    > >       "-c", "max_slot_wal_keep_size=1024"
    > >     ]
    > >     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 relname =
    > > '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 relname =
    > > '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#MONITORING-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 behavior
    > does not seem fully consistent with wording such as “throughout the
    > transaction.” It also seems more appropriate for these table views to
    > reflect stats within their own scopes, rather than cumulative values
    > spanning txn boundaries.
    >
    
    I’ve looked into this issue and 'd like to propose a patch to 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 subject
    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" — 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 the
    4 non-inline counter functions in pgstat_relation.c). After the first
    call per entry per transaction, the check reduces to a single integer
    comparison with a predictably not-taken branch.
    
    For function stats, a new PgStat_FunctionPending wrapper struct embeds
    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 psql
    -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 contrast,
    ordinary pg_regress SQL files are executed by psql in file/stdin mode,
    which runs through MainLoop() and dispatches top-level statements one
    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 — each verifying isolation across
    consecutive top-level transactions within a single message.
    
    
    --
    Best,
    Xuneng
    
  4. Re: BUG #19439: pg_stat_xact_user_tables stat not currect during the transaction

    Xuneng Zhou <xunengzhou@gmail.com> — 2026-03-29T03:17:23Z

    On Sat, Mar 28, 2026 at 1:47 PM Xuneng Zhou <xunengzhou@gmail.com> wrote:
    >
    > On Fri, Mar 27, 2026 at 10:15 AM Xuneng Zhou <xunengzhou@gmail.com> wrote:
    > >
    > > Hi klemen,
    > >
    > > Thanks for the report.
    > >
    > > On Fri, Mar 27, 2026 at 5:36 AM PG Bug reporting form
    > > <noreply@postgresql.org> 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 configuration is as
    > > > follows:
    > > >
    > > >   postgres:
    > > >     image: postgres:18.0
    > > >     command: [
    > > >       "postgres",
    > > >       "-N", "200",
    > > >       "-c", "max_prepared_transactions=100",
    > > >       "-c", "wal_level=logical",
    > > >       "-c", "max_wal_senders=10",
    > > >       "-c", "max_replication_slots=20",
    > > >       "-c", "wal_keep_size=10",
    > > >       "-c", "max_slot_wal_keep_size=1024"
    > > >     ]
    > > >     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 relname =
    > > > '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 relname =
    > > > '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#MONITORING-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 behavior
    > > does not seem fully consistent with wording such as “throughout the
    > > transaction.” It also seems more appropriate for these table views to
    > > reflect stats within their own scopes, rather than cumulative values
    > > spanning txn boundaries.
    > >
    >
    > I’ve looked into this issue and 'd like to propose a patch to 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 subject
    > 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" — 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 the
    > 4 non-inline counter functions in pgstat_relation.c). After the first
    > call per entry per transaction, the check reduces to a single integer
    > comparison with a predictably not-taken branch.
    >
    > For function stats, a new PgStat_FunctionPending wrapper struct embeds
    > 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 psql
    > -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 contrast,
    > ordinary pg_regress SQL files are executed by psql in file/stdin mode,
    > which runs through MainLoop() and dispatches top-level statements one
    > 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 — each verifying isolation across
    > 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.
    
    --
    Best,
    Xuneng
    
    
    
    
  5. Re: BUG #19439: pg_stat_xact_user_tables stat not currect during the transaction

    Xuneng Zhou <xunengzhou@gmail.com> — 2026-03-30T03:14:19Z

    On Sun, Mar 29, 2026 at 11:17 AM Xuneng Zhou <xunengzhou@gmail.com> wrote:
    >
    > On Sat, Mar 28, 2026 at 1:47 PM Xuneng Zhou <xunengzhou@gmail.com> wrote:
    > >
    > > On Fri, Mar 27, 2026 at 10:15 AM Xuneng Zhou <xunengzhou@gmail.com> wrote:
    > > >
    > > > Hi klemen,
    > > >
    > > > Thanks for the report.
    > > >
    > > > On Fri, Mar 27, 2026 at 5:36 AM PG Bug reporting form
    > > > <noreply@postgresql.org> 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 configuration is as
    > > > > follows:
    > > > >
    > > > >   postgres:
    > > > >     image: postgres:18.0
    > > > >     command: [
    > > > >       "postgres",
    > > > >       "-N", "200",
    > > > >       "-c", "max_prepared_transactions=100",
    > > > >       "-c", "wal_level=logical",
    > > > >       "-c", "max_wal_senders=10",
    > > > >       "-c", "max_replication_slots=20",
    > > > >       "-c", "wal_keep_size=10",
    > > > >       "-c", "max_slot_wal_keep_size=1024"
    > > > >     ]
    > > > >     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 relname =
    > > > > '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 relname =
    > > > > '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#MONITORING-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 behavior
    > > > does not seem fully consistent with wording such as “throughout the
    > > > transaction.” It also seems more appropriate for these table views to
    > > > reflect stats within their own scopes, rather than cumulative values
    > > > spanning txn boundaries.
    > > >
    > >
    > > I’ve looked into this issue and 'd like to propose a patch to 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 subject
    > > 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" — 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 the
    > > 4 non-inline counter functions in pgstat_relation.c). After the first
    > > call per entry per transaction, the check reduces to a single integer
    > > comparison with a predictably not-taken branch.
    > >
    > > For function stats, a new PgStat_FunctionPending wrapper struct embeds
    > > 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 psql
    > > -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 contrast,
    > > ordinary pg_regress SQL files are executed by psql in file/stdin mode,
    > > which runs through MainLoop() and dispatches top-level statements one
    > > 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 — each verifying isolation across
    > > 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’t have a clear preference between the two approaches; both are
    presented for review.
    
    --
    Best,
    Xuneng
    
  6. Re: BUG #19439: pg_stat_xact_user_tables stat not currect during the transaction

    klemen kobau <klemen.kobau@gmail.com> — 2026-05-06T08:03:08Z

    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 <xunengzhou@gmail.com> wrote:
    
    > On Sun, Mar 29, 2026 at 11:17 AM Xuneng Zhou <xunengzhou@gmail.com> wrote:
    > >
    > > On Sat, Mar 28, 2026 at 1:47 PM Xuneng Zhou <xunengzhou@gmail.com>
    > wrote:
    > > >
    > > > On Fri, Mar 27, 2026 at 10:15 AM Xuneng Zhou <xunengzhou@gmail.com>
    > wrote:
    > > > >
    > > > > Hi klemen,
    > > > >
    > > > > Thanks for the report.
    > > > >
    > > > > On Fri, Mar 27, 2026 at 5:36 AM PG Bug reporting form
    > > > > <noreply@postgresql.org> 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
    > configuration is as
    > > > > > follows:
    > > > > >
    > > > > >   postgres:
    > > > > >     image: postgres:18.0
    > > > > >     command: [
    > > > > >       "postgres",
    > > > > >       "-N", "200",
    > > > > >       "-c", "max_prepared_transactions=100",
    > > > > >       "-c", "wal_level=logical",
    > > > > >       "-c", "max_wal_senders=10",
    > > > > >       "-c", "max_replication_slots=20",
    > > > > >       "-c", "wal_keep_size=10",
    > > > > >       "-c", "max_slot_wal_keep_size=1024"
    > > > > >     ]
    > > > > >     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
    > relname =
    > > > > > '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
    > relname =
    > > > > > '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#MONITORING-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
    > behavior
    > > > > does not seem fully consistent with wording such as “throughout the
    > > > > transaction.” It also seems more appropriate for these table views to
    > > > > reflect stats within their own scopes, rather than cumulative values
    > > > > spanning txn boundaries.
    > > > >
    > > >
    > > > I’ve looked into this issue and 'd like to propose a patch to 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 subject
    > > > 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" — 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 the
    > > > 4 non-inline counter functions in pgstat_relation.c). After the first
    > > > call per entry per transaction, the check reduces to a single integer
    > > > comparison with a predictably not-taken branch.
    > > >
    > > > For function stats, a new PgStat_FunctionPending wrapper struct embeds
    > > > 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 psql
    > > > -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 contrast,
    > > > ordinary pg_regress SQL files are executed by psql in file/stdin mode,
    > > > which runs through MainLoop() and dispatches top-level statements one
    > > > 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 — each verifying isolation across
    > > > 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’t have a clear preference between the two approaches; both are
    > presented for review.
    >
    > --
    > Best,
    > Xuneng
    >
    
  7. Re: BUG #19439: pg_stat_xact_user_tables stat not currect during the transaction

    Xuneng Zhou <xunengzhou@gmail.com> — 2026-05-07T06:35:49Z

    Hi klemen,
    
    On Wed, May 6, 2026 at 4:03 PM klemen kobau <klemen.kobau@gmail.com> 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 <xunengzhou@gmail.com> wrote:
    >>
    >> On Sun, Mar 29, 2026 at 11:17 AM Xuneng Zhou <xunengzhou@gmail.com> wrote:
    >> >
    >> > On Sat, Mar 28, 2026 at 1:47 PM Xuneng Zhou <xunengzhou@gmail.com> wrote:
    >> > >
    >> > > On Fri, Mar 27, 2026 at 10:15 AM Xuneng Zhou <xunengzhou@gmail.com> wrote:
    >> > > >
    >> > > > Hi klemen,
    >> > > >
    >> > > > Thanks for the report.
    >> > > >
    >> > > > On Fri, Mar 27, 2026 at 5:36 AM PG Bug reporting form
    >> > > > <noreply@postgresql.org> 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 configuration is as
    >> > > > > follows:
    >> > > > >
    >> > > > >   postgres:
    >> > > > >     image: postgres:18.0
    >> > > > >     command: [
    >> > > > >       "postgres",
    >> > > > >       "-N", "200",
    >> > > > >       "-c", "max_prepared_transactions=100",
    >> > > > >       "-c", "wal_level=logical",
    >> > > > >       "-c", "max_wal_senders=10",
    >> > > > >       "-c", "max_replication_slots=20",
    >> > > > >       "-c", "wal_keep_size=10",
    >> > > > >       "-c", "max_slot_wal_keep_size=1024"
    >> > > > >     ]
    >> > > > >     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 relname =
    >> > > > > '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 relname =
    >> > > > > '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#MONITORING-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 behavior
    >> > > > does not seem fully consistent with wording such as “throughout the
    >> > > > transaction.” It also seems more appropriate for these table views to
    >> > > > reflect stats within their own scopes, rather than cumulative values
    >> > > > spanning txn boundaries.
    >> > > >
    >> > >
    >> > > I’ve looked into this issue and 'd like to propose a patch to 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 subject
    >> > > 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" — 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 the
    >> > > 4 non-inline counter functions in pgstat_relation.c). After the first
    >> > > call per entry per transaction, the check reduces to a single integer
    >> > > comparison with a predictably not-taken branch.
    >> > >
    >> > > For function stats, a new PgStat_FunctionPending wrapper struct embeds
    >> > > 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 psql
    >> > > -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 contrast,
    >> > > ordinary pg_regress SQL files are executed by psql in file/stdin mode,
    >> > > which runs through MainLoop() and dispatches top-level statements one
    >> > > 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 — each verifying isolation across
    >> > > 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’t 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.
    
    -- 
    Best,
    Xuneng
    
    
    
    
  8. Re: BUG #19439: pg_stat_xact_user_tables stat not currect during the transaction

    Xuneng Zhou <xunengzhou@gmail.com> — 2026-05-13T03:30:41Z

    > >> --- 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’t 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.
    >
    
    Here is the updated version using eager baseline refresh, i.e. sweeping all
    backend-local pending pgstat entries at each top-level transaction boundary.
    
    I tested the eager-baseline approach at both micro and macro levels.  The
    results show the same cost shape in both cases: the patch cost scales with the
    number of backend-local pending pgstat entries that must be swept at each
    top-level transaction boundary.
    
    The microbenchmark isolates the transaction-boundary cost.  It first creates a
    controlled number of pending pgstat entries in one backend, then times 10000
    tiny BEGIN/COMMIT boundaries in one simple-query message:
    
      BEGIN; COMMIT; BEGIN; COMMIT; ...
    
    The results below use matching -O0 --enable-debug --enable-cassert builds for
    both installs.
    
      pending entries   unpatched us/xact       patched us/xact       patch delta
      ---------------------------------------------------------------------------
      0                 713.526                 703.357               -10.169
      100               740.954                 754.298               +13.344
      1000              1183.302 / 1177.393     1211.533 / 1213.089   about +32 us
      5000              2978.008 / 2967.674     3236.365 / 3081.945
    about +186 us median
    
    Machine: Mac mini,  M4 Pro, 48GB mem
    
    This is intentionally hostile to eager sweeping: one backend accumulates many
    pending entries, then repeatedly crosses top-level transaction boundaries while
    doing almost no useful work inside the transactions.  The added cost is small at
    100 pending entries, where noise matters, but becomes clear at 1000 and 5000
    entries.  In this debug/cassert build, the patch adds roughly 0.03-0.04 us per
    pending entry per transaction in the 1000-5000 entry range.  Absolute numbers
    are inflated by the build profile, but the linear shape is the relevant signal.
    
    The macro benchmark shows when that same boundary cost is visible in a more
    query-shaped workload:
    
      workload                       base tps     patched tps   change
      ------------------------------------------------------------------
      5000 tables, 1 row/table        11216          6231       -44%
      1000 tables, 1000 rows/table     7683          7113        -7%
      100 tables, 10000 rows/table     2152          2162         ~0%
    
    The latency breakdown explains the TPS pattern.  For the 5000-table/1-row case:
    
      base:    avg_lat=0.089 ms, select_avg=0.051 ms
      patched: avg_lat=0.160 ms, select_avg=0.062 ms
    
    Machine: intel xeon server, 40 cores, 128GB mem
    
    The SELECT itself barely changes.  Most of the regression appears outside the
    SELECT, where the patch does the baseline sweep at transaction end.  This is
    the worst case for the eager design: the transaction does very little real table
    work, but the backend has thousands of pending relation stats entries.
    
    As the number of pending entries drops, or as the query does more real scan
    work, the same fixed boundary cost is diluted.  With 1000 tables and 1000 rows
    per table, the regression falls to about 7%.  With 100 tables and 10000 rows per
    table, the scan dominates and the sweep over about 100 pending entries is lost
    in noise.
    
    Taken together, the benchmarks confirm the expected implementation cost model:
    
      eager baseline refresh cost ~= O(number of pending pgstat entries per backend)
    
    Row count does not directly drive the cost; it only hides or exposes the fixed
    transaction-boundary work.
    
    These results suggest that the eager-sweeping approach has an unfavorable
    cost model for long-lived sessions that accumulate many pending stats entries
    and then execute small transactions.  A lazy baseline appraoch, where
    each pending
    entry records the current transaction generation only when that entry is first
    touched, should avoid the transaction-boundary sweep and make the cost scale
    with the transaction's actual working set instead. However, it still
    suffers from the
    potential overhead of additional comparisons on hot paths, as well as increased
    maintenance pain.
    
    -- 
    Regards,
    Xuneng Zhou
    HighGo Software Co., Ltd.