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_slruOne row per SLRU, showing statistics of operations. See
@@ -2578,6 +2587,99 @@ description | Waiting for a newly initialized WAL file to reach durable storage
+
+ pg_stat_role
+
+
+ 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.
+
+
+
+ pg_stat_role View
+
+
+
+
+ Column Type
+
+
+ Description
+
+
+
+
+
+
+
+ roleidoid
+
+
+ OID of the role
+
+
+
+
+
+ rolnamename
+
+
+ Name of the role
+
+
+
+
+
+ sessionsbigint
+
+
+ Number of sessions established by this role
+
+
+
+
+
+ xact_commitbigint
+
+
+ Number of transactions in sessions of this role that have been
+ committed
+
+
+
+
+
+ xact_rollbackbigint
+
+
+ Number of transactions in sessions of this role that have been
+ rolled back
+
+
+
+
+
+ stats_resettimestamp with time zone
+
+
+ Time at which these statistics were last reset
+
+
+
+
+
+
+
+
pg_stat_ssl
@@ -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