From 687d4282c59898bac4f86de2e37c3b4eb4fb1bab Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Jul 2026 20:21:52 +0000 Subject: [PATCH] Add pg_stat_role system view for per-role cumulative statistics The new view reports, for each role, the number of sessions established and the number of committed and rolled back transactions, along with the time at which the role's statistics were last reset. Statistics are attributed to the login role of a session, so SET ROLE and SECURITY DEFINER functions do not redirect where activity is counted. Only client backends are counted, matching the session accounting of pg_stat_database. This adds a new variable-numbered cumulative statistics kind PGSTAT_KIND_ROLE, keyed by role OID and shared across databases. Stats entries are created by CREATE ROLE and dropped by DROP ROLE (transactionally), survive clean restarts, and can be reset with the new pg_stat_reset_role_stats() function. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012dKR6zB32MW351m4dsoXPg --- doc/src/sgml/monitoring.sgml | 122 ++++++++++++++ src/backend/catalog/system_views.sql | 11 ++ src/backend/commands/user.c | 7 + src/backend/tcop/postgres.c | 1 + src/backend/utils/activity/Makefile | 1 + src/backend/utils/activity/meson.build | 1 + src/backend/utils/activity/pgstat.c | 18 ++ src/backend/utils/activity/pgstat_role.c | 200 +++++++++++++++++++++++ src/backend/utils/activity/pgstat_xact.c | 1 + src/backend/utils/adt/pgstatfuncs.c | 90 ++++++++++ src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_proc.dat | 12 ++ src/include/pgstat.h | 20 ++- src/include/utils/pgstat_internal.h | 16 ++ src/include/utils/pgstat_kind.h | 15 +- src/test/regress/expected/rules.out | 8 + src/test/regress/expected/stats.out | 112 ++++++++++++- src/test/regress/sql/stats.sql | 46 ++++++ src/tools/pgindent/typedefs.list | 2 + 19 files changed, 668 insertions(+), 17 deletions(-) create mode 100644 src/backend/utils/activity/pgstat_role.c diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 858788b227c4e..1800c51bb700e 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -547,6 +547,15 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser + + pg_stat_rolepg_stat_role + One row per role, showing statistics about sessions and + transactions of that role. See + + pg_stat_role for details. + + + pg_stat_slrupg_stat_slru One row per SLRU, showing statistics of operations. See @@ -2578,6 +2587,99 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + <structname>pg_stat_role</structname> + + + pg_stat_role + + + + The pg_stat_role view will contain one row per + role, showing cumulative statistics about the activity of sessions that + logged in as that role. Activity is attributed to the login role of a + session; SET ROLE and SECURITY + DEFINER functions do not change the role activity is counted + for. Only client sessions are counted, not background processes such as + autovacuum workers. + + + + <structname>pg_stat_role</structname> View + + + + + Column Type + + + Description + + + + + + + + roleid oid + + + OID of the role + + + + + + rolname name + + + Name of the role + + + + + + sessions bigint + + + Number of sessions established by this role + + + + + + xact_commit bigint + + + Number of transactions in sessions of this role that have been + committed + + + + + + xact_rollback bigint + + + Number of transactions in sessions of this role that have been + rolled back + + + + + + stats_reset timestamp with time zone + + + Time at which these statistics were last reset + + + + +
+ +
+ <structname>pg_stat_ssl</structname> @@ -5984,6 +6086,26 @@ description | Waiting for a newly initialized WAL file to reach durable storage can be granted EXECUTE to run the function.
+ + + + + pg_stat_reset_role_stats + + pg_stat_reset_role_stats ( oid ) + void + + + Resets statistics for a single role shown in the + pg_stat_role view to zero. If + the argument is NULL, reset statistics for all + roles. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 6c1c5545cb56a..bb1e3f5b0a0a3 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1561,6 +1561,17 @@ CREATE VIEW pg_stat_subscription_stats AS FROM pg_subscription as s, pg_stat_get_subscription_stats(s.oid) as ss; +CREATE VIEW pg_stat_role AS + SELECT + rs.roleid, + r.rolname, + rs.sessions, + rs.xact_commit, + rs.xact_rollback, + rs.stats_reset + FROM pg_roles as r, + pg_stat_get_role_stats(r.oid) as rs; + CREATE VIEW pg_wait_events AS SELECT * FROM pg_get_wait_events(); diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index be11c49f919d0..f27bd2d6a1310 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -32,6 +32,7 @@ #include "commands/user.h" #include "libpq/crypt.h" #include "miscadmin.h" +#include "pgstat.h" #include "port/pg_bitutils.h" #include "storage/lmgr.h" #include "utils/acl.h" @@ -606,6 +607,9 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) /* Post creation hook for new role */ InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0); + /* Create the statistics entry for the new role */ + pgstat_create_role(roleid); + /* * Close pg_authid, but keep lock till commit. */ @@ -1325,6 +1329,9 @@ DropRole(DropRoleStmt *stmt) * Remove settings for this role. */ DropSetting(InvalidOid, roleid); + + /* Drop the statistics entry for this role */ + pgstat_drop_role(roleid); } /* diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index b6bdfe213feec..e1265a91709b9 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -4497,6 +4497,7 @@ PostgresMain(const char *dbname, const char *username) on_proc_exit(log_disconnections, 0); pgstat_report_connect(MyDatabaseId); + pgstat_report_role_connect(GetSessionUserId()); /* Perform initialization specific to a WAL sender process. */ if (am_walsender) diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile index 5fed953c28a7f..9caa719892527 100644 --- a/src/backend/utils/activity/Makefile +++ b/src/backend/utils/activity/Makefile @@ -30,6 +30,7 @@ OBJS = \ pgstat_lock.o \ pgstat_relation.o \ pgstat_replslot.o \ + pgstat_role.o \ pgstat_shmem.o \ pgstat_slru.o \ pgstat_subscription.o \ diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build index 470b5dac402bd..74cfb9b266efe 100644 --- a/src/backend/utils/activity/meson.build +++ b/src/backend/utils/activity/meson.build @@ -15,6 +15,7 @@ backend_sources += files( 'pgstat_lock.c', 'pgstat_relation.c', 'pgstat_replslot.c', + 'pgstat_role.c', 'pgstat_shmem.c', 'pgstat_slru.c', 'pgstat_subscription.c', diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c index 8b2a5ec367503..74c02d2b9dc34 100644 --- a/src/backend/utils/activity/pgstat.c +++ b/src/backend/utils/activity/pgstat.c @@ -382,6 +382,23 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE] .reset_timestamp_cb = pgstat_backend_reset_timestamp_cb, }, + [PGSTAT_KIND_ROLE] = { + .name = "role", + + .fixed_amount = false, + .write_to_file = true, + /* so pg_stat_role entries can be seen in all databases */ + .accessed_across_databases = true, + + .shared_size = sizeof(PgStatShared_Role), + .shared_data_off = offsetof(PgStatShared_Role, stats), + .shared_data_len = sizeof(((PgStatShared_Role *) 0)->stats), + .pending_size = sizeof(PgStat_StatRoleEntry), + + .flush_pending_cb = pgstat_role_flush_cb, + .reset_timestamp_cb = pgstat_role_reset_timestamp_cb, + }, + /* stats for fixed-numbered (mostly 1) objects */ [PGSTAT_KIND_ARCHIVER] = { @@ -786,6 +803,7 @@ pgstat_report_stat(bool force) } pgstat_update_dbstats(now); + pgstat_update_role_stats(); /* don't wait for lock acquisition when !force */ nowait = !force; diff --git a/src/backend/utils/activity/pgstat_role.c b/src/backend/utils/activity/pgstat_role.c new file mode 100644 index 0000000000000..7022453629aa6 --- /dev/null +++ b/src/backend/utils/activity/pgstat_role.c @@ -0,0 +1,200 @@ +/* ------------------------------------------------------------------------- + * + * pgstat_role.c + * Implementation of role statistics. + * + * This file contains the implementation of role statistics. It is kept + * separate from pgstat.c to enforce the line between the statistics access / + * storage implementation and the details about individual types of + * statistics. + * + * Statistics are attributed to the login role of a session (the one + * reported by pgstat_report_role_connect()), not to the current user, so + * SET ROLE and SECURITY DEFINER functions do not redirect where activity + * is counted. + * + * Copyright (c) 2001-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/utils/activity/pgstat_role.c + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "miscadmin.h" +#include "utils/pgstat_internal.h" + + +/* + * The login role of this session, as reported by + * pgstat_report_role_connect(). Remains InvalidOid in processes that don't + * report role statistics (e.g. autovacuum workers, background workers). + */ +static Oid pgStatSessionRoleId = InvalidOid; + +/* + * Transaction counts not yet transferred to the pending entry. Like the + * database transaction counters, these are accumulated at transaction end + * and moved to the pending entry by pgstat_update_role_stats(). + */ +static PgStat_Counter pgStatRoleXactCommit = 0; +static PgStat_Counter pgStatRoleXactRollback = 0; + +static PgStat_StatRoleEntry *pgstat_prep_role_pending(Oid roleid); + + +/* + * Report creating the role. + */ +void +pgstat_create_role(Oid roleid) +{ + /* Ensures that stats are dropped if transaction rolls back */ + pgstat_create_transactional(PGSTAT_KIND_ROLE, + InvalidOid, roleid); + + /* Create and initialize the role stats entry */ + pgstat_get_entry_ref(PGSTAT_KIND_ROLE, InvalidOid, roleid, + true, NULL); + pgstat_reset_entry(PGSTAT_KIND_ROLE, InvalidOid, roleid, 0); +} + +/* + * Report dropping the role. + * + * Ensures that stats are dropped if transaction commits. + */ +void +pgstat_drop_role(Oid roleid) +{ + pgstat_drop_transactional(PGSTAT_KIND_ROLE, + InvalidOid, roleid); +} + +/* + * Notify stats system of a new connection, attributed to the given login + * role. + * + * Like pgstat_report_connect(), only count client sessions, not background + * processes. + */ +void +pgstat_report_role_connect(Oid roleid) +{ + PgStat_StatRoleEntry *pending; + + if (MyBackendType != B_BACKEND) + return; + + /* remember the login role for transaction accounting */ + pgStatSessionRoleId = roleid; + + pending = pgstat_prep_role_pending(roleid); + pending->sessions++; +} + +/* + * Called from access/xact.c at transaction commit/abort. + */ +void +AtEOXact_PgStat_Role(bool isCommit, bool parallel) +{ + /* Don't count parallel worker transaction stats */ + if (parallel) + return; + + /* Don't count transactions in sessions not tied to a login role */ + if (!OidIsValid(pgStatSessionRoleId)) + return; + + if (isCommit) + pgStatRoleXactCommit++; + else + pgStatRoleXactRollback++; +} + +/* + * Subroutine for pgstat_report_stat(): Handle xact commit/rollback counts + * for the session's login role. + */ +void +pgstat_update_role_stats(void) +{ + PgStat_StatRoleEntry *pending; + + if (!OidIsValid(pgStatSessionRoleId)) + return; + + if (pgStatRoleXactCommit == 0 && pgStatRoleXactRollback == 0) + return; + + pending = pgstat_prep_role_pending(pgStatSessionRoleId); + pending->xact_commit += pgStatRoleXactCommit; + pending->xact_rollback += pgStatRoleXactRollback; + + pgStatRoleXactCommit = 0; + pgStatRoleXactRollback = 0; +} + +/* + * Support function for the SQL-callable pgstat* functions. Returns + * the collected statistics for one role or NULL. NULL doesn't mean + * that the role doesn't exist, just that there are no statistics, so the + * caller is better off to report ZERO instead. + */ +PgStat_StatRoleEntry * +pgstat_fetch_stat_role_entry(Oid roleid) +{ + return (PgStat_StatRoleEntry *) + pgstat_fetch_entry(PGSTAT_KIND_ROLE, InvalidOid, roleid, NULL); +} + +/* + * Find or create a local PgStat_StatRoleEntry entry for roleid. + */ +static PgStat_StatRoleEntry * +pgstat_prep_role_pending(Oid roleid) +{ + PgStat_EntryRef *entry_ref; + + entry_ref = pgstat_prep_pending_entry(PGSTAT_KIND_ROLE, InvalidOid, + roleid, NULL); + + return entry_ref->pending; +} + +/* + * Flush out pending stats for the entry + * + * If nowait is true and the lock could not be immediately acquired, returns + * false without flushing the entry. Otherwise returns true. + */ +bool +pgstat_role_flush_cb(PgStat_EntryRef *entry_ref, bool nowait) +{ + PgStat_StatRoleEntry *localent; + PgStatShared_Role *sharedent; + + localent = (PgStat_StatRoleEntry *) entry_ref->pending; + sharedent = (PgStatShared_Role *) entry_ref->shared_stats; + + if (!pgstat_lock_entry(entry_ref, nowait)) + return false; + +#define PGSTAT_ACCUM_ROLECOUNT(item) \ + (sharedent)->stats.item += (localent)->item + PGSTAT_ACCUM_ROLECOUNT(sessions); + PGSTAT_ACCUM_ROLECOUNT(xact_commit); + PGSTAT_ACCUM_ROLECOUNT(xact_rollback); +#undef PGSTAT_ACCUM_ROLECOUNT + + pgstat_unlock_entry(entry_ref); + return true; +} + +void +pgstat_role_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts) +{ + ((PgStatShared_Role *) header)->stats.stat_reset_timestamp = ts; +} diff --git a/src/backend/utils/activity/pgstat_xact.c b/src/backend/utils/activity/pgstat_xact.c index 3e1978775e163..b45a47b51ca69 100644 --- a/src/backend/utils/activity/pgstat_xact.c +++ b/src/backend/utils/activity/pgstat_xact.c @@ -42,6 +42,7 @@ AtEOXact_PgStat(bool isCommit, bool parallel) PgStat_SubXactStatus *xact_state; AtEOXact_PgStat_Database(isCommit, parallel); + AtEOXact_PgStat_Role(isCommit, parallel); /* handle transactional stats information */ xact_state = pgStatXactStack; diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index 565d0e70768bb..808debb18e48e 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -2151,6 +2151,31 @@ pg_stat_reset_subscription_stats(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +/* Reset role stats (a specific one or all of them) */ +Datum +pg_stat_reset_role_stats(PG_FUNCTION_ARGS) +{ + Oid roleid; + + if (PG_ARGISNULL(0)) + { + /* Clear all role stats */ + pgstat_reset_of_kind(PGSTAT_KIND_ROLE); + } + else + { + roleid = PG_GETARG_OID(0); + + if (!OidIsValid(roleid)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid role OID %u", roleid))); + pgstat_reset(PGSTAT_KIND_ROLE, InvalidOid, roleid); + } + + PG_RETURN_VOID(); +} + Datum pg_stat_get_archiver(PG_FUNCTION_ARGS) { @@ -2384,6 +2409,71 @@ pg_stat_get_subscription_stats(PG_FUNCTION_ARGS) PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); } +/* + * Get the statistics for the given role. If the role statistics are not + * available, return all-zeros stats. + */ +Datum +pg_stat_get_role_stats(PG_FUNCTION_ARGS) +{ +#define PG_STAT_GET_ROLE_STATS_COLS 5 + Oid roleid = PG_GETARG_OID(0); + TupleDesc tupdesc; + Datum values[PG_STAT_GET_ROLE_STATS_COLS] = {0}; + bool nulls[PG_STAT_GET_ROLE_STATS_COLS] = {0}; + PgStat_StatRoleEntry *roleentry; + PgStat_StatRoleEntry allzero; + int i = 0; + + /* Get role stats */ + roleentry = pgstat_fetch_stat_role_entry(roleid); + + /* Initialise attributes information in the tuple descriptor */ + tupdesc = CreateTemplateTupleDesc(PG_STAT_GET_ROLE_STATS_COLS); + TupleDescInitEntry(tupdesc, (AttrNumber) 1, "roleid", + OIDOID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 2, "sessions", + INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 3, "xact_commit", + INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 4, "xact_rollback", + INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 5, "stats_reset", + TIMESTAMPTZOID, -1, 0); + TupleDescFinalize(tupdesc); + BlessTupleDesc(tupdesc); + + if (!roleentry) + { + /* If the role is not found, initialise its stats */ + memset(&allzero, 0, sizeof(PgStat_StatRoleEntry)); + roleentry = &allzero; + } + + /* roleid */ + values[i++] = ObjectIdGetDatum(roleid); + + /* sessions */ + values[i++] = Int64GetDatum(roleentry->sessions); + + /* xact_commit */ + values[i++] = Int64GetDatum(roleentry->xact_commit); + + /* xact_rollback */ + values[i++] = Int64GetDatum(roleentry->xact_rollback); + + /* stats_reset */ + if (roleentry->stat_reset_timestamp == 0) + nulls[i] = true; + else + values[i] = TimestampTzGetDatum(roleentry->stat_reset_timestamp); + + Assert(i + 1 == PG_STAT_GET_ROLE_STATS_COLS); + + /* Returns the record as Datum */ + PG_RETURN_DATUM(HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls))); +} + /* * Checks for presence of stats for object with provided kind, database oid, * object oid. diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index ba4e2b4d908f9..967b3948df7b4 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202607091 +#define CATALOG_VERSION_NO 202607121 #endif diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 3cb84359adf06..70acc49e89e25 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5752,6 +5752,13 @@ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o}', proargnames => '{subid,subid,apply_error_count,sync_seq_error_count,sync_table_error_count,confl_insert_exists,confl_update_origin_differs,confl_update_exists,confl_update_deleted,confl_update_missing,confl_delete_origin_differs,confl_delete_missing,confl_multiple_unique_conflicts,stats_reset}', prosrc => 'pg_stat_get_subscription_stats' }, +{ oid => '9515', descr => 'statistics: information about role stats', + proname => 'pg_stat_get_role_stats', provolatile => 's', + proparallel => 'r', prorettype => 'record', proargtypes => 'oid', + proallargtypes => '{oid,oid,int8,int8,int8,timestamptz}', + proargmodes => '{i,o,o,o,o,o}', + proargnames => '{roleid,roleid,sessions,xact_commit,xact_rollback,stats_reset}', + prosrc => 'pg_stat_get_role_stats' }, { oid => '6118', descr => 'statistics: information about subscription', proname => 'pg_stat_get_subscription', prorows => '10', proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r', @@ -6267,6 +6274,11 @@ proname => 'pg_stat_reset_subscription_stats', proisstrict => 'f', provolatile => 'v', prorettype => 'void', proargtypes => 'oid', prosrc => 'pg_stat_reset_subscription_stats', proacl => '{POSTGRES=X}' }, +{ oid => '9516', + descr => 'statistics: reset collected statistics for a single role', + proname => 'pg_stat_reset_role_stats', proisstrict => 'f', + provolatile => 'v', prorettype => 'void', proargtypes => 'oid', + prosrc => 'pg_stat_reset_role_stats', proacl => '{POSTGRES=X}' }, { oid => '3163', descr => 'current trigger depth', proname => 'pg_trigger_depth', provolatile => 's', proparallel => 'r', diff --git a/src/include/pgstat.h b/src/include/pgstat.h index 58a44857f1311..81ef032aefb3e 100644 --- a/src/include/pgstat.h +++ b/src/include/pgstat.h @@ -218,7 +218,7 @@ typedef struct PgStat_TableXactStatus * ------------------------------------------------------------ */ -#define PGSTAT_FILE_FORMAT_ID 0x01A5BCBC +#define PGSTAT_FILE_FORMAT_ID 0x01A5BCBD typedef struct PgStat_ArchiverStats { @@ -448,6 +448,14 @@ typedef struct PgStat_StatSubEntry TimestampTz stat_reset_timestamp; } PgStat_StatSubEntry; +typedef struct PgStat_StatRoleEntry +{ + PgStat_Counter sessions; + PgStat_Counter xact_commit; + PgStat_Counter xact_rollback; + TimestampTz stat_reset_timestamp; +} PgStat_StatRoleEntry; + typedef struct PgStat_StatTabEntry { PgStat_Counter numscans; @@ -812,6 +820,16 @@ extern int pgstat_get_slru_index(const char *name); extern PgStat_SLRUStats *pgstat_fetch_slru(void); +/* + * Functions in pgstat_role.c + */ + +extern void pgstat_create_role(Oid roleid); +extern void pgstat_drop_role(Oid roleid); +extern void pgstat_report_role_connect(Oid roleid); +extern PgStat_StatRoleEntry *pgstat_fetch_stat_role_entry(Oid roleid); + + /* * Functions in pgstat_subscription.c */ diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h index b3dc3ff7d8bb7..37d6120053be1 100644 --- a/src/include/utils/pgstat_internal.h +++ b/src/include/utils/pgstat_internal.h @@ -519,6 +519,12 @@ typedef struct PgStatShared_Subscription PgStat_StatSubEntry stats; } PgStatShared_Subscription; +typedef struct PgStatShared_Role +{ + PgStatShared_Common header; + PgStat_StatRoleEntry stats; +} PgStatShared_Role; + typedef struct PgStatShared_ReplSlot { PgStatShared_Common header; @@ -847,6 +853,16 @@ extern void pgstat_wal_reset_all_cb(TimestampTz ts); extern void pgstat_wal_snapshot_cb(void); +/* + * Functions in pgstat_role.c + */ + +extern void AtEOXact_PgStat_Role(bool isCommit, bool parallel); +extern void pgstat_update_role_stats(void); +extern bool pgstat_role_flush_cb(PgStat_EntryRef *entry_ref, bool nowait); +extern void pgstat_role_reset_timestamp_cb(PgStatShared_Common *header, TimestampTz ts); + + /* * Functions in pgstat_subscription.c */ diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h index 2d78a02968347..0c0ae5d347d0e 100644 --- a/src/include/utils/pgstat_kind.h +++ b/src/include/utils/pgstat_kind.h @@ -30,15 +30,16 @@ #define PGSTAT_KIND_REPLSLOT 4 /* per-slot statistics */ #define PGSTAT_KIND_SUBSCRIPTION 5 /* per-subscription statistics */ #define PGSTAT_KIND_BACKEND 6 /* per-backend statistics */ +#define PGSTAT_KIND_ROLE 7 /* per-role statistics */ /* stats for fixed-numbered objects */ -#define PGSTAT_KIND_ARCHIVER 7 -#define PGSTAT_KIND_BGWRITER 8 -#define PGSTAT_KIND_CHECKPOINTER 9 -#define PGSTAT_KIND_IO 10 -#define PGSTAT_KIND_LOCK 11 -#define PGSTAT_KIND_SLRU 12 -#define PGSTAT_KIND_WAL 13 +#define PGSTAT_KIND_ARCHIVER 8 +#define PGSTAT_KIND_BGWRITER 9 +#define PGSTAT_KIND_CHECKPOINTER 10 +#define PGSTAT_KIND_IO 11 +#define PGSTAT_KIND_LOCK 12 +#define PGSTAT_KIND_SLRU 13 +#define PGSTAT_KIND_WAL 14 #define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE #define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 39905c2de142c..4f53d62ef5433 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -2274,6 +2274,14 @@ pg_stat_replication_slots| SELECT s.slot_name, FROM pg_replication_slots r, LATERAL pg_stat_get_replication_slot((r.slot_name)::text) s(slot_name, spill_txns, spill_count, spill_bytes, stream_txns, stream_count, stream_bytes, mem_exceeded_count, total_txns, total_bytes, slotsync_skip_count, slotsync_last_skip, stats_reset) WHERE (r.datoid IS NOT NULL); +pg_stat_role| SELECT rs.roleid, + r.rolname, + rs.sessions, + rs.xact_commit, + rs.xact_rollback, + rs.stats_reset + FROM pg_roles r, + LATERAL pg_stat_get_role_stats(r.oid) rs(roleid, sessions, xact_commit, xact_rollback, stats_reset); pg_stat_slru| SELECT name, blks_zeroed, blks_hit, diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out index 03cbc1cdef59e..c45a34b054214 100644 --- a/src/test/regress/expected/stats.out +++ b/src/test/regress/expected/stats.out @@ -127,14 +127,15 @@ SELECT id, name, fixed_amount, 4 | replslot | f | t | t 5 | subscription | f | t | t 6 | backend | f | t | f - 7 | archiver | t | f | t - 8 | bgwriter | t | f | t - 9 | checkpointer | t | f | t - 10 | io | t | f | t - 11 | lock | t | f | t - 12 | slru | t | f | t - 13 | wal | t | f | t -(13 rows) + 7 | role | f | t | t + 8 | archiver | t | f | t + 9 | bgwriter | t | f | t + 10 | checkpointer | t | f | t + 11 | io | t | f | t + 12 | lock | t | f | t + 13 | slru | t | f | t + 14 | wal | t | f | t +(14 rows) -- ensure that both seqscan and indexscan plans are allowed SET enable_seqscan TO on; @@ -1497,6 +1498,101 @@ SELECT pg_stat_get_subscription_stats(NULL); (1 row) +SELECT pg_stat_get_role_stats(NULL); + pg_stat_get_role_stats +------------------------ + +(1 row) + +-- Test per-role statistics +CREATE ROLE regress_stats_role1; +SELECT oid AS stats_test_role1_oid FROM pg_roles + WHERE rolname = 'regress_stats_role1' \gset +-- creating a role also creates its stats entry, with all-zero counters; +-- stats_reset is NULL until the entry is explicitly reset +SELECT pg_stat_have_stats('role', 0, :stats_test_role1_oid); + pg_stat_have_stats +-------------------- + t +(1 row) + +SELECT sessions, xact_commit, xact_rollback, + stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_role WHERE rolname = 'regress_stats_role1'; + sessions | xact_commit | xact_rollback | has_stats_reset +----------+-------------+---------------+----------------- + 0 | 0 | 0 | f +(1 row) + +-- stats entry of a rolled back CREATE ROLE is dropped again +BEGIN; +CREATE ROLE regress_stats_role2; +SELECT oid AS stats_test_role2_oid FROM pg_roles + WHERE rolname = 'regress_stats_role2' \gset +ROLLBACK; +SELECT pg_stat_have_stats('role', 0, :stats_test_role2_oid); + pg_stat_have_stats +-------------------- + f +(1 row) + +-- this session's activity is attributed to its login role +BEGIN; +SELECT 1 AS dummy; + dummy +------- + 1 +(1 row) + +COMMIT; +BEGIN; +SELECT 1 AS dummy; + dummy +------- + 1 +(1 row) + +ROLLBACK; +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT sessions > 0 AS has_sessions, xact_commit > 0 AS has_xact_commit, + xact_rollback > 0 AS has_xact_rollback + FROM pg_stat_role WHERE rolname = current_user; + has_sessions | has_xact_commit | has_xact_rollback +--------------+-----------------+------------------- + t | t | t +(1 row) + +-- resetting stats of a single role sets a new reset timestamp +SELECT pg_stat_reset_role_stats(:stats_test_role1_oid); + pg_stat_reset_role_stats +-------------------------- + +(1 row) + +SELECT sessions, xact_commit, xact_rollback, + stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_role WHERE rolname = 'regress_stats_role1'; + sessions | xact_commit | xact_rollback | has_stats_reset +----------+-------------+---------------+----------------- + 0 | 0 | 0 | t +(1 row) + +-- invalid role OID is rejected +SELECT pg_stat_reset_role_stats(0); +ERROR: invalid role OID 0 +-- dropping a role also drops its stats entry +DROP ROLE regress_stats_role1; +SELECT pg_stat_have_stats('role', 0, :stats_test_role1_oid); + pg_stat_have_stats +-------------------- + f +(1 row) + -- Test that the following operations are tracked in pg_stat_io and in -- backend stats: -- - reads of target blocks into shared buffers diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql index 4c265d1245c72..3c7b8fbd63c7f 100644 --- a/src/test/regress/sql/stats.sql +++ b/src/test/regress/sql/stats.sql @@ -660,6 +660,52 @@ SET enable_seqscan TO on; -- ensure that stats accessors handle NULL input correctly SELECT pg_stat_get_replication_slot(NULL); SELECT pg_stat_get_subscription_stats(NULL); +SELECT pg_stat_get_role_stats(NULL); + +-- Test per-role statistics +CREATE ROLE regress_stats_role1; +SELECT oid AS stats_test_role1_oid FROM pg_roles + WHERE rolname = 'regress_stats_role1' \gset + +-- creating a role also creates its stats entry, with all-zero counters; +-- stats_reset is NULL until the entry is explicitly reset +SELECT pg_stat_have_stats('role', 0, :stats_test_role1_oid); +SELECT sessions, xact_commit, xact_rollback, + stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_role WHERE rolname = 'regress_stats_role1'; + +-- stats entry of a rolled back CREATE ROLE is dropped again +BEGIN; +CREATE ROLE regress_stats_role2; +SELECT oid AS stats_test_role2_oid FROM pg_roles + WHERE rolname = 'regress_stats_role2' \gset +ROLLBACK; +SELECT pg_stat_have_stats('role', 0, :stats_test_role2_oid); + +-- this session's activity is attributed to its login role +BEGIN; +SELECT 1 AS dummy; +COMMIT; +BEGIN; +SELECT 1 AS dummy; +ROLLBACK; +SELECT pg_stat_force_next_flush(); +SELECT sessions > 0 AS has_sessions, xact_commit > 0 AS has_xact_commit, + xact_rollback > 0 AS has_xact_rollback + FROM pg_stat_role WHERE rolname = current_user; + +-- resetting stats of a single role sets a new reset timestamp +SELECT pg_stat_reset_role_stats(:stats_test_role1_oid); +SELECT sessions, xact_commit, xact_rollback, + stats_reset IS NOT NULL AS has_stats_reset + FROM pg_stat_role WHERE rolname = 'regress_stats_role1'; + +-- invalid role OID is rejected +SELECT pg_stat_reset_role_stats(0); + +-- dropping a role also drops its stats entry +DROP ROLE regress_stats_role1; +SELECT pg_stat_have_stats('role', 0, :stats_test_role1_oid); -- Test that the following operations are tracked in pg_stat_io and in diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 56c1f997f88b1..bbb7e2687b1a2 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2329,6 +2329,7 @@ PgStatShared_IO PgStatShared_Lock PgStatShared_Relation PgStatShared_ReplSlot +PgStatShared_Role PgStatShared_SLRU PgStatShared_Subscription PgStatShared_Wal @@ -2363,6 +2364,7 @@ PgStat_StatCustomVarEntry PgStat_StatDBEntry PgStat_StatFuncEntry PgStat_StatReplSlotEntry +PgStat_StatRoleEntry PgStat_StatSubEntry PgStat_StatTabEntry PgStat_StatsFileOp