From de04e151abff07dab378d5a3d006e76ea56359f6 Mon Sep 17 00:00:00 2001 From: Sami Imseih Date: Sun, 10 May 2026 07:06:04 -0500 Subject: [PATCH v1] pgstat: Introduce pg_stat_report_anytime() for mid-transaction stats flush Add an API to flush pending stats that are safe to report inside a transaction without waiting for transaction end. Relation write counters (tuples inserted, updated, deleted) for tables modified in the current transaction are excluded, since their final values depend on commit/abort outcome. The SQL function pg_stat_report_anytime(pid) flushes the target backend's pending stats: if the PID matches the caller's own backend it flushes immediately, otherwise it signals the target to flush at its next CHECK_FOR_INTERRUPTS (for regular backends) or main-loop iteration (for auxiliary processes). The C function pgstat_report_anytime_stat() flushes pending stats in the calling backend only. --- doc/src/sgml/monitoring.sgml | 26 ++++ src/backend/postmaster/autovacuum.c | 3 + src/backend/postmaster/checkpointer.c | 3 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 3 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/tcop/postgres.c | 3 + src/backend/utils/activity/pgstat.c | 61 +++++++++- src/backend/utils/activity/pgstat_relation.c | 97 +++++++++------ src/backend/utils/adt/pgstatfuncs.c | 40 ++++++ src/backend/utils/init/globals.c | 1 + src/include/catalog/pg_proc.dat | 6 + src/include/miscadmin.h | 1 + src/include/pgstat.h | 3 + src/include/storage/procsignal.h | 2 + src/test/regress/expected/stats.out | 122 +++++++++++++++++++ src/test/regress/sql/stats.sql | 81 ++++++++++++ 19 files changed, 431 insertions(+), 35 deletions(-) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 08d5b824552..bb6c928e3e7 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -5607,6 +5607,32 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + + + pg_stat_report_anytime + + pg_stat_report_anytime ( integer ) + boolean + + + Force flush of pending statistics to shared memory for the backend + with the specified process ID. Unlike normal statistics reporting, + this can be called from within a transaction. For relations modified + by INSERT, UPDATE, or + DELETE in the current transaction, only read + counters (scans, tuples fetched, blocks hit) are flushed + immediately; write counters (tuples inserted, updated, deleted) + are deferred until the transaction ends. + Returns true if the flush was successfully + triggered, false otherwise. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index a5a8db2ff88..c5fddf75dab 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -825,6 +825,9 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (ReportAnytimeStatsPending) + ProcessReportAnytimeStatsInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 087120db090..874cceb3970 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -694,6 +694,9 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + if (ReportAnytimeStatsPending) + ProcessReportAnytimeStatsInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index a2c0ff012c5..4e09e93f8da 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -17,6 +17,7 @@ #include #include "miscadmin.h" +#include "pgstat.h" #include "postmaster/interrupt.h" #include "storage/ipc.h" #include "storage/latch.h" @@ -48,6 +49,9 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + if (ReportAnytimeStatsPending) + ProcessReportAnytimeStatsInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 0f207ac0356..d83a5fda862 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -870,6 +870,9 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (ReportAnytimeStatsPending) + ProcessReportAnytimeStatsInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index b46bac681fe..4a5534a8f9b 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -24,6 +24,7 @@ #include "access/xlogutils.h" #include "libpq/pqsignal.h" #include "miscadmin.h" +#include "pgstat.h" #include "postmaster/auxprocess.h" #include "postmaster/startup.h" #include "storage/ipc.h" @@ -192,6 +193,9 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + if (ReportAnytimeStatsPending) + ProcessReportAnytimeStatsInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 4f12eaf2c85..b1239cbb07f 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -876,6 +876,9 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + if (ReportAnytimeStatsPending) + ProcessReportAnytimeStatsInterrupt(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 264e4c22ca6..40023ac9888 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -711,6 +711,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_REPACK_MESSAGE)) HandleRepackMessageInterrupt(); + if (CheckProcSignal(PROCSIG_REPORT_ANYTIME_STATS)) + HandleReportAnytimeStatsInterrupt(); + if (CheckProcSignal(PROCSIG_SLOTSYNC_MESSAGE)) HandleSlotSyncMessageInterrupt(); diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index dbef734a93f..dbca372a3f1 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3609,6 +3609,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (ReportAnytimeStatsPending) + ProcessReportAnytimeStatsInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index b67da88c7dc..9b5d9bf09cb 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -106,6 +106,7 @@ #include "access/xact.h" #include "lib/dshash.h" +#include "miscadmin.h" #include "pgstat.h" #include "storage/fd.h" #include "storage/ipc.h" @@ -845,6 +846,57 @@ pgstat_force_next_flush(void) pgStatForceNextFlush = true; } +/* + * Immediately flush all pending statistics entries to shared memory. + * + * Unlike pgstat_report_stat(), this can be called anytime, including + * within a transaction. + */ +void +pgstat_report_anytime_stat(void) +{ + pgstat_flush_pending_entries(false); + + for (PgStat_Kind kind = PGSTAT_KIND_MIN; kind <= PGSTAT_KIND_MAX; kind++) + { + const PgStat_KindInfo *kind_info = pgstat_get_kind_info(kind); + + if (!kind_info || !kind_info->flush_static_cb) + continue; + + kind_info->flush_static_cb(false); + } +} + +/* + * HandleReportAnytimeStatsInterrupt + * Handle receipt of an interrupt requesting an anytime stats report. + * + * All the actual work is deferred to ProcessReportAnytimeStatsInterrupt(), + * because we cannot safely acquire locks inside the signal handler. + */ +void +HandleReportAnytimeStatsInterrupt(void) +{ + InterruptPending = true; + ReportAnytimeStatsPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessReportAnytimeStatsInterrupt + * Report all pending statistics to shared memory. + * + * Called from ProcessInterrupts() when ReportAnytimeStatsPending is set. + */ +void +ProcessReportAnytimeStatsInterrupt(void) +{ + ReportAnytimeStatsPending = false; + + pgstat_report_anytime_stat(); +} + /* * Only for use by pgstat_reset_counters() */ @@ -1414,7 +1466,14 @@ pgstat_flush_pending_entries(bool nowait) /* flush the stats, if possible */ did_flush = kind_info->flush_pending_cb(entry_ref, nowait); - Assert(did_flush || nowait); + /* + * When nowait is false we block for the lock, so the only reason a + * flush_pending_cb can legitimately return false is that the entry + * has active transaction state that must not be freed yet (e.g. + * relation stats with trans != NULL). That situation only arises + * mid-transaction, hence the IsTransactionOrTransactionBlock() check. + */ + Assert(did_flush || nowait || IsTransactionOrTransactionBlock()); /* determine next entry, before deleting the pending entry */ if (dlist_has_next(&pgStatPending, cur)) diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c index b2ca28f83ba..848687a9f7e 100644 --- a/src/backend/utils/activity/pgstat_relation.c +++ b/src/backend/utils/activity/pgstat_relation.c @@ -828,64 +828,76 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) /* * Ignore entries that didn't accumulate any actual counts, such as - * indexes that were opened by the planner but not used. + * indexes that were opened by the planner but not used. The entry cannot + * be freed if there is active transaction state, since + * AtEOXact_PgStat_Relations will still merge counters into it. */ if (pg_memory_is_all_zeros(&lstats->counts, sizeof(struct PgStat_TableCounts))) - return true; + return (lstats->trans == NULL); if (!pgstat_lock_entry(entry_ref, nowait)) return false; - /* add the values to the shared entry. */ + /* Update counters that are always safe to flush. */ tabentry = &shtabstats->stats; tabentry->numscans += lstats->counts.numscans; if (lstats->counts.numscans) { - TimestampTz t = GetCurrentTransactionStopTimestamp(); + TimestampTz t = IsTransactionOrTransactionBlock() ? + GetCurrentStatementStartTimestamp() : + GetCurrentTransactionStopTimestamp(); if (t > tabentry->lastscan) tabentry->lastscan = t; } tabentry->tuples_returned += lstats->counts.tuples_returned; tabentry->tuples_fetched += lstats->counts.tuples_fetched; - tabentry->tuples_inserted += lstats->counts.tuples_inserted; - tabentry->tuples_updated += lstats->counts.tuples_updated; - tabentry->tuples_deleted += lstats->counts.tuples_deleted; tabentry->tuples_hot_updated += lstats->counts.tuples_hot_updated; tabentry->tuples_newpage_updated += lstats->counts.tuples_newpage_updated; + tabentry->blocks_fetched += lstats->counts.blocks_fetched; + tabentry->blocks_hit += lstats->counts.blocks_hit; /* - * If table was truncated/dropped, first reset the live/dead counters. + * Update counters that are only safe to flush outside of a transaction + * that has modified this relation. */ - if (lstats->counts.truncdropped) + if (lstats->trans == NULL) { - tabentry->live_tuples = 0; - tabentry->dead_tuples = 0; - tabentry->ins_since_vacuum = 0; - } + tabentry->tuples_inserted += lstats->counts.tuples_inserted; + tabentry->tuples_updated += lstats->counts.tuples_updated; + tabentry->tuples_deleted += lstats->counts.tuples_deleted; - tabentry->live_tuples += lstats->counts.delta_live_tuples; - tabentry->dead_tuples += lstats->counts.delta_dead_tuples; - tabentry->mod_since_analyze += lstats->counts.changed_tuples; + /* + * If table was truncated/dropped, first reset the live/dead counters. + */ + if (lstats->counts.truncdropped) + { + tabentry->live_tuples = 0; + tabentry->dead_tuples = 0; + tabentry->ins_since_vacuum = 0; + } - /* - * Using tuples_inserted to update ins_since_vacuum does mean that we'll - * track aborted inserts too. This isn't ideal, but otherwise probably - * not worth adding an extra field for. It may just amount to autovacuums - * triggering for inserts more often than they maybe should, which is - * probably not going to be common enough to be too concerned about here. - */ - tabentry->ins_since_vacuum += lstats->counts.tuples_inserted; + tabentry->live_tuples += lstats->counts.delta_live_tuples; + tabentry->dead_tuples += lstats->counts.delta_dead_tuples; + tabentry->mod_since_analyze += lstats->counts.changed_tuples; - tabentry->blocks_fetched += lstats->counts.blocks_fetched; - tabentry->blocks_hit += lstats->counts.blocks_hit; + /* + * Using tuples_inserted to update ins_since_vacuum does mean that + * we'll track aborted inserts too. This isn't ideal, but otherwise + * probably not worth adding an extra field for. It may just amount + * to autovacuums triggering for inserts more often than they maybe + * should, which is probably not going to be common enough to be too + * concerned about here. + */ + tabentry->ins_since_vacuum += lstats->counts.tuples_inserted; - /* Clamp live_tuples in case of negative delta_live_tuples */ - tabentry->live_tuples = Max(tabentry->live_tuples, 0); - /* Likewise for dead_tuples */ - tabentry->dead_tuples = Max(tabentry->dead_tuples, 0); + /* Clamp live_tuples in case of negative delta_live_tuples */ + tabentry->live_tuples = Max(tabentry->live_tuples, 0); + /* Likewise for dead_tuples */ + tabentry->dead_tuples = Max(tabentry->dead_tuples, 0); + } pgstat_unlock_entry(entry_ref); @@ -893,13 +905,30 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) dbentry = pgstat_prep_database_pending(dboid); dbentry->tuples_returned += lstats->counts.tuples_returned; dbentry->tuples_fetched += lstats->counts.tuples_fetched; - dbentry->tuples_inserted += lstats->counts.tuples_inserted; - dbentry->tuples_updated += lstats->counts.tuples_updated; - dbentry->tuples_deleted += lstats->counts.tuples_deleted; dbentry->blocks_fetched += lstats->counts.blocks_fetched; dbentry->blocks_hit += lstats->counts.blocks_hit; - return true; + if (lstats->trans == NULL) + { + dbentry->tuples_inserted += lstats->counts.tuples_inserted; + dbentry->tuples_updated += lstats->counts.tuples_updated; + dbentry->tuples_deleted += lstats->counts.tuples_deleted; + return true; + } + + /* + * This is a partial, in-transaction flush. Zero out the counters we + * already flushed so they aren't double-counted on the next flush. + */ + lstats->counts.numscans = 0; + lstats->counts.tuples_returned = 0; + lstats->counts.tuples_fetched = 0; + lstats->counts.tuples_hot_updated = 0; + lstats->counts.tuples_newpage_updated = 0; + lstats->counts.blocks_fetched = 0; + lstats->counts.blocks_hit = 0; + + return false; } void diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 6f9c9c72de5..eb22490dc2c 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -28,6 +28,7 @@ #include "replication/logicallauncher.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "storage/procsignal.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/timestamp.h" @@ -1929,6 +1930,45 @@ pg_stat_force_next_flush(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +/* + * Signal a backend to report all its pending statistics to shared memory. + * If the target is the current backend, the report happens immediately. + */ +Datum +pg_stat_report_anytime(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + + if (pid == MyProcPid) + { + pgstat_report_anytime_stat(); + PG_RETURN_BOOL(true); + } + + proc = BackendPidGetProc(pid); + if (proc == NULL) + proc = AuxiliaryPidGetProc(pid); + + if (proc == NULL) + { + ereport(WARNING, + (errmsg("PID %d is not a PostgreSQL server process", pid))); + PG_RETURN_BOOL(false); + } + + procNumber = GetNumberFromPGProc(proc); + if (SendProcSignal(pid, PROCSIG_REPORT_ANYTIME_STATS, procNumber) < 0) + { + ereport(WARNING, + (errmsg("could not send signal to process %d: %m", pid))); + PG_RETURN_BOOL(false); + } + + PG_RETURN_BOOL(true); +} + /* Reset all counters for the current database */ Datum diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index bbd28d14d99..1b5b3d59c3c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t ReportAnytimeStatsPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index be157a5fbe9..406628025b1 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6209,6 +6209,12 @@ proname => 'pg_stat_force_next_flush', proisstrict => 'f', provolatile => 'v', proparallel => 'r', prorettype => 'void', proargtypes => '', prosrc => 'pg_stat_force_next_flush' }, +{ oid => '9953', + descr => 'statistics: force flush of pending stats to shared memory, including from within a transaction', + proname => 'pg_stat_report_anytime', provolatile => 'v', + prorettype => 'bool', proargtypes => 'int4', + prosrc => 'pg_stat_report_anytime', + proacl => '{POSTGRES=X}' }, { oid => '2274', descr => 'statistics: reset collected statistics for current database', proname => 'pg_stat_reset', proisstrict => 'f', provolatile => 'v', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 8ccdf61246b..7f8b38cb9d7 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -97,6 +97,7 @@ extern PGDLLIMPORT volatile sig_atomic_t TransactionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; +extern PGDLLIMPORT volatile sig_atomic_t ReportAnytimeStatsPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; diff --git a/src/include/pgstat.h b/src/include/pgstat.h index dfa2e837638..87def3b08e2 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -552,6 +552,9 @@ extern void pgstat_initialize(void); /* Functions called from backends */ extern long pgstat_report_stat(bool force); extern void pgstat_force_next_flush(void); +extern void pgstat_report_anytime_stat(void); +extern void HandleReportAnytimeStatsInterrupt(void); +extern void ProcessReportAnytimeStatsInterrupt(void); extern void pgstat_reset_counters(void); extern void pgstat_reset(PgStat_Kind kind, Oid dboid, uint64 objid); diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index aaa158bfd66..a184d449eba 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -38,6 +38,8 @@ typedef enum PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ PROCSIG_SLOTSYNC_MESSAGE, /* ask slot synchronization to stop */ PROCSIG_REPACK_MESSAGE, /* Message from repack worker */ + PROCSIG_REPORT_ANYTIME_STATS, /* ask backend to report anytime + * statistics */ PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check * PGPROC->pendingRecoveryConflicts for the * reason */ diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out index c551abb1178..66b683965a6 100644 --- a/src/test/regress/expected/stats.out +++ b/src/test/regress/expected/stats.out @@ -2040,4 +2040,126 @@ SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE loc (1 row) DROP TABLE part_test; +-- +-- Test pg_stat_report_anytime +-- +CREATE TABLE partial_flush(id int); +INSERT INTO partial_flush VALUES (1), (2), (3); +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +-- Record counters before the explicit transaction +SELECT seq_scan AS seq_scan_before, + seq_tup_read AS seq_tup_read_before, + n_tup_ins AS n_tup_ins_before, + n_tup_upd AS n_tup_upd_before + FROM pg_stat_user_tables WHERE relname = 'partial_flush' \gset +BEGIN; +SET LOCAL stats_fetch_consistency = none; +-- Generate both transaction-safe and transaction-unsafe counters. +SELECT count(*) FROM partial_flush; + count +------- + 3 +(1 row) + +INSERT INTO partial_flush VALUES (4), (5); +UPDATE partial_flush SET id = id WHERE id = 1; +-- Flush mid-transaction +SELECT pg_stat_report_anytime(pg_backend_pid()); + pg_stat_report_anytime +------------------------ + t +(1 row) + +-- Transaction-safe counters should be visible mid-transaction. +-- Transaction-unsafe counters (ins, upd) should NOT be flushed yet, +-- since their final values depend on whether the transaction commits. +SELECT seq_scan - :seq_scan_before AS seq_scan_delta, + seq_tup_read - :seq_tup_read_before AS seq_tup_read_delta, + n_tup_ins - :n_tup_ins_before AS n_tup_ins_delta, + n_tup_upd - :n_tup_upd_before AS n_tup_upd_delta + FROM pg_stat_user_tables WHERE relname = 'partial_flush'; + seq_scan_delta | seq_tup_read_delta | n_tup_ins_delta | n_tup_upd_delta +----------------+--------------------+-----------------+----------------- + 2 | 8 | 0 | 0 +(1 row) + +-- Generate more transaction-safe activity to verify no double counting. +SELECT count(*) FROM partial_flush; + count +------- + 5 +(1 row) + +-- Flush again mid-transaction +SELECT pg_stat_report_anytime(pg_backend_pid()); + pg_stat_report_anytime +------------------------ + t +(1 row) + +-- Should show cumulative totals, not double-counted. +SELECT seq_scan - :seq_scan_before AS seq_scan_delta, + seq_tup_read - :seq_tup_read_before AS seq_tup_read_delta, + n_tup_ins - :n_tup_ins_before AS n_tup_ins_delta, + n_tup_upd - :n_tup_upd_before AS n_tup_upd_delta + FROM pg_stat_user_tables WHERE relname = 'partial_flush'; + seq_scan_delta | seq_tup_read_delta | n_tup_ins_delta | n_tup_upd_delta +----------------+--------------------+-----------------+----------------- + 3 | 13 | 0 | 0 +(1 row) + +COMMIT; +-- After commit, all counters should be flushed. +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT seq_scan - :seq_scan_before AS seq_scan_delta, + seq_tup_read - :seq_tup_read_before AS seq_tup_read_delta, + n_tup_ins - :n_tup_ins_before AS n_tup_ins_delta, + n_tup_upd - :n_tup_upd_before AS n_tup_upd_delta + FROM pg_stat_user_tables WHERE relname = 'partial_flush'; + seq_scan_delta | seq_tup_read_delta | n_tup_ins_delta | n_tup_upd_delta +----------------+--------------------+-----------------+----------------- + 3 | 13 | 2 | 1 +(1 row) + +DROP TABLE partial_flush; +-- Test that pg_stat_report_anytime also flushes non-relation stats. +CREATE TABLE wal_flush_test(id int); +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT wal_records AS wal_records_before + FROM pg_stat_get_backend_wal(pg_backend_pid()) \gset +BEGIN; +SET LOCAL stats_fetch_consistency = none; +-- Generate WAL inside the transaction. +INSERT INTO wal_flush_test SELECT generate_series(1, 10); +-- Flush mid-transaction; WAL stats should become visible immediately. +SELECT pg_stat_report_anytime(pg_backend_pid()); + pg_stat_report_anytime +------------------------ + t +(1 row) + +SELECT wal_records > :wal_records_before AS wal_flushed + FROM pg_stat_get_backend_wal(pg_backend_pid()); + wal_flushed +------------- + t +(1 row) + +COMMIT; +DROP TABLE wal_flush_test; -- End of Stats Test diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql index 610fd21fae4..c8bc0f22f27 100644 --- a/src/test/regress/sql/stats.sql +++ b/src/test/regress/sql/stats.sql @@ -1008,4 +1008,85 @@ SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE loc DROP TABLE part_test; +-- +-- Test pg_stat_report_anytime +-- +CREATE TABLE partial_flush(id int); +INSERT INTO partial_flush VALUES (1), (2), (3); +SELECT pg_stat_force_next_flush(); + +-- Record counters before the explicit transaction +SELECT seq_scan AS seq_scan_before, + seq_tup_read AS seq_tup_read_before, + n_tup_ins AS n_tup_ins_before, + n_tup_upd AS n_tup_upd_before + FROM pg_stat_user_tables WHERE relname = 'partial_flush' \gset + +BEGIN; +SET LOCAL stats_fetch_consistency = none; + +-- Generate both transaction-safe and transaction-unsafe counters. +SELECT count(*) FROM partial_flush; +INSERT INTO partial_flush VALUES (4), (5); +UPDATE partial_flush SET id = id WHERE id = 1; + +-- Flush mid-transaction +SELECT pg_stat_report_anytime(pg_backend_pid()); + +-- Transaction-safe counters should be visible mid-transaction. +-- Transaction-unsafe counters (ins, upd) should NOT be flushed yet, +-- since their final values depend on whether the transaction commits. +SELECT seq_scan - :seq_scan_before AS seq_scan_delta, + seq_tup_read - :seq_tup_read_before AS seq_tup_read_delta, + n_tup_ins - :n_tup_ins_before AS n_tup_ins_delta, + n_tup_upd - :n_tup_upd_before AS n_tup_upd_delta + FROM pg_stat_user_tables WHERE relname = 'partial_flush'; + +-- Generate more transaction-safe activity to verify no double counting. +SELECT count(*) FROM partial_flush; + +-- Flush again mid-transaction +SELECT pg_stat_report_anytime(pg_backend_pid()); + +-- Should show cumulative totals, not double-counted. +SELECT seq_scan - :seq_scan_before AS seq_scan_delta, + seq_tup_read - :seq_tup_read_before AS seq_tup_read_delta, + n_tup_ins - :n_tup_ins_before AS n_tup_ins_delta, + n_tup_upd - :n_tup_upd_before AS n_tup_upd_delta + FROM pg_stat_user_tables WHERE relname = 'partial_flush'; + +COMMIT; + +-- After commit, all counters should be flushed. +SELECT pg_stat_force_next_flush(); + +SELECT seq_scan - :seq_scan_before AS seq_scan_delta, + seq_tup_read - :seq_tup_read_before AS seq_tup_read_delta, + n_tup_ins - :n_tup_ins_before AS n_tup_ins_delta, + n_tup_upd - :n_tup_upd_before AS n_tup_upd_delta + FROM pg_stat_user_tables WHERE relname = 'partial_flush'; + +DROP TABLE partial_flush; + +-- Test that pg_stat_report_anytime also flushes non-relation stats. +CREATE TABLE wal_flush_test(id int); +SELECT pg_stat_force_next_flush(); +SELECT wal_records AS wal_records_before + FROM pg_stat_get_backend_wal(pg_backend_pid()) \gset + +BEGIN; +SET LOCAL stats_fetch_consistency = none; + +-- Generate WAL inside the transaction. +INSERT INTO wal_flush_test SELECT generate_series(1, 10); + +-- Flush mid-transaction; WAL stats should become visible immediately. +SELECT pg_stat_report_anytime(pg_backend_pid()); + +SELECT wal_records > :wal_records_before AS wal_flushed + FROM pg_stat_get_backend_wal(pg_backend_pid()); + +COMMIT; +DROP TABLE wal_flush_test; + -- End of Stats Test -- 2.50.1 (Apple Git-155)