agora inbox for pgsql-hackers@postgresql.org
help / color / mirror / Atom feedAdd per-backend lock statistics
12+ messages / 5 participants
[nested] [flat]
* Add per-backend lock statistics
@ 2026-06-03 13:58 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 2 replies; 12+ messages in thread
From: Bertrand Drouvot @ 2026-06-03 13:58 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org
Hi hackers,
Now that we have global lock statistics since 4019f725f5d, it could be useful
to have the same kind of information on a per-backend basis.
Indeed, pg_stat_lock gives us cluster-wide aggregates: total waits, total wait
time, total fast-path exceeded across all backends since last reset.
When we see high numbers, we can't answer:
- Which backend is affected the most?
- Is it one backend affected or many?
- Is a specific application or connection pool suffering?
- After a specific workload/application is improved, did its lock behavior
improve?
With per-backend lock stats, we could:
1/ Isolate problematic sessions. We can correlate locks behavior with specific
PIDs visible in pg_stat_activity: identify the exact application_name or user
experiencing lock waits.
2/ Debug live contention. During an incident, we could pinpoint which backends
are experiencing fast-path exhaustion or lock waits without having to reset
global stats and lose history.
3/ Define workload characterization. Different backend types may have very
different lock profiles. Per-backend stats would let us see this directly.
4/ Compare before/after per session. We could measure a single backend's lock
behavior across a specific operation, which is impossible with global counters
that include metrics from all other backends.
IO and WAL stats already have per-backend counterparts (pg_stat_get_backend_io(),
pg_stat_get_backend_wal()). Lock stats are the same class of operational data:
having them only at the global level is an inconsistency that limits observability.
As far the technical implementation:
This data can be retrieved with a new system function called
pg_stat_get_backend_lock(), that returns one tuple per lock type based on the PID
provided in input.
pgstat_flush_backend() gains a new flag value, able to control the flush of the
lock stats.
This patch relies mostly on the infrastructure provided by 9aea73fc61d4, that
has introduced backend statistics.
The overhead (2 functions calls and counters increments) on the hot path (normal
lock acquisition) is zero: counters are only incremented on paths that are already
"slow" (post deadlock timeout waits, fast-path slot exhaustion) and does not add
that much memory per-backend: PgStat_PendingLock is 288 bytes.
The patch is made of 2 sub-patches:
0001: Refactor pg_stat_get_lock() to use a helper function
Extract the tuple-building logic from pg_stat_get_lock() into a new
static helper pg_stat_lock_build_tuples(). This is in preparation for
pg_stat_get_backend_lock() which will reuse the same helper, following
the pattern established by pg_stat_io_build_tuples() for IO stats and
pg_stat_wal_build_tuple() for WAL stats.
0002: Add per-backend lock statistics
As discussed above.
Looking forward to your feedback,
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v1-0001-Refactor-pg_stat_get_lock-to-use-a-helper-functio.patch (2.9K, ../../aiAzEY+cMQb%2FW8yu@bdtpg/2-v1-0001-Refactor-pg_stat_get_lock-to-use-a-helper-functio.patch)
download | inline diff:
From c00ae2fb9022b80bf2262afe4bc23e3255d02809 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Wed, 3 Jun 2026 13:04:26 +0000
Subject: [PATCH v1 1/2] Refactor pg_stat_get_lock() to use a helper function
Extract the tuple-building logic from pg_stat_get_lock() into a new
static helper pg_stat_lock_build_tuples(). This is in preparation for
pg_stat_get_backend_lock() which will reuse the same helper, following
the pattern established by pg_stat_io_build_tuples() for IO stats and
pg_stat_wal_build_tuple() for WAL stats.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by:
Discussion:
---
src/backend/utils/adt/pgstatfuncs.c | 47 +++++++++++++++++++----------
1 file changed, 31 insertions(+), 16 deletions(-)
100.0% src/backend/utils/adt/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6f9c9c72de5..353607954ad 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1737,38 +1737,53 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
wal_stats->stat_reset_timestamp));
}
-Datum
-pg_stat_get_lock(PG_FUNCTION_ARGS)
+/*
+ * pg_stat_lock_build_tuples
+ *
+ * Helper routine for pg_stat_get_lock(), filling a result tuplestore with one
+ * tuple for each lock type.
+ */
+static void
+pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo,
+ PgStat_LockEntry *lock_stats,
+ TimestampTz stat_reset_timestamp)
{
#define PG_STAT_LOCK_COLS 5
- ReturnSetInfo *rsinfo;
- PgStat_Lock *lock_stats;
-
- InitMaterializedSRF(fcinfo, 0);
- rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
-
- lock_stats = pgstat_fetch_stat_lock();
-
for (int lcktype = 0; lcktype <= LOCKTAG_LAST_TYPE; lcktype++)
{
- const char *locktypename;
Datum values[PG_STAT_LOCK_COLS] = {0};
bool nulls[PG_STAT_LOCK_COLS] = {0};
- PgStat_LockEntry *lck_stats = &lock_stats->stats[lcktype];
+ PgStat_LockEntry *lck_stats = &lock_stats[lcktype];
int i = 0;
- locktypename = LockTagTypeNames[lcktype];
-
- values[i++] = CStringGetTextDatum(locktypename);
+ values[i++] = CStringGetTextDatum(LockTagTypeNames[lcktype]);
values[i++] = Int64GetDatum(lck_stats->waits);
values[i++] = Int64GetDatum(lck_stats->wait_time);
values[i++] = Int64GetDatum(lck_stats->fastpath_exceeded);
- values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
+ if (stat_reset_timestamp != 0)
+ values[i] = TimestampTzGetDatum(stat_reset_timestamp);
+ else
+ nulls[i] = true;
Assert(i + 1 == PG_STAT_LOCK_COLS);
tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
}
+}
+
+Datum
+pg_stat_get_lock(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsinfo;
+ PgStat_Lock *lock_stats;
+
+ InitMaterializedSRF(fcinfo, 0);
+ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+ lock_stats = pgstat_fetch_stat_lock();
+
+ pg_stat_lock_build_tuples(rsinfo, lock_stats->stats,
+ lock_stats->stat_reset_timestamp);
return (Datum) 0;
}
--
2.34.1
[text/x-diff] v1-0002-Add-per-backend-lock-statistics.patch (13.5K, ../../aiAzEY+cMQb%2FW8yu@bdtpg/3-v1-0002-Add-per-backend-lock-statistics.patch)
download | inline diff:
From 51eb375f5cc254ada379c0722db1c4e07a91910d Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Wed, 3 Jun 2026 13:05:03 +0000
Subject: [PATCH v1 2/2] Add per-backend lock statistics
This commit adds per-backend lock statistics, providing the same information as
pg_stat_lock, except that it is now possible to retrieve those stats (lock wait
counts, wait times, and fast-path exceeded count) on a per-backend basis.
This data can be retrieved with a new system function called
pg_stat_get_backend_lock(), that returns one tuple per lock type based on the PID
provided in input. Like pg_stat_get_backend_io(), this is useful when joined
with pg_stat_activity to get a live picture of the locks behavior for each running
backend.
pgstat_flush_backend() gains a new flag value, able to control the flush of the
lock stats.
This commit relies mostly on the infrastructure provided by 9aea73fc61d4, that
has introduced backend statistics.
XXX: Bump catalog version. A bump of PGSTAT_FILE_FORMAT_ID is not required,
as backend stats do not persist on disk.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by:
Discussion:
---
doc/src/sgml/monitoring.sgml | 19 ++++++
src/backend/utils/activity/pgstat_backend.c | 70 +++++++++++++++++++++
src/backend/utils/activity/pgstat_lock.c | 4 ++
src/backend/utils/adt/pgstatfuncs.c | 29 ++++++++-
src/include/catalog/pg_proc.dat | 8 +++
src/include/pgstat.h | 11 ++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/regress/expected/stats.out | 12 ++++
src/test/regress/sql/stats.sql | 9 +++
9 files changed, 162 insertions(+), 3 deletions(-)
14.7% doc/src/sgml/
38.6% src/backend/utils/activity/
14.8% src/backend/utils/adt/
8.7% src/include/catalog/
9.9% src/include/
6.8% src/test/regress/expected/
6.1% src/test/regress/sql/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..3936fb62a5d 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5545,6 +5545,25 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-lock" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_lock</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_lock</function> ( <type>integer</type> )
+ <returnvalue>setof record</returnvalue>
+ </para>
+ <para>
+ Returns lock statistics about the backend with the specified
+ process ID. The output fields are exactly the same as the ones in the
+ <structname>pg_stat_lock</structname> view.
+ </para>
+ <para>
+ The function does not return lock statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 73461c9bca5..297eda0a489 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -39,6 +39,7 @@
*/
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
+static bool backend_has_lockstats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -86,6 +87,37 @@ pgstat_count_backend_io_op(IOObject io_object, IOContext io_context,
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report lock stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_lock_waits(uint8 locktag_type, long msecs)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+ PendingBackendStats.pending_lock.stats[locktag_type].waits++;
+ PendingBackendStats.pending_lock.stats[locktag_type].wait_time += (PgStat_Counter) msecs;
+
+ backend_has_lockstats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+ PendingBackendStats.pending_lock.stats[locktag_type].fastpath_exceeded++;
+
+ backend_has_lockstats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -262,6 +294,36 @@ pgstat_flush_backend_entry_wal(PgStat_EntryRef *entry_ref)
prevBackendWalUsage = pgWalUsage;
}
+/*
+ * Flush out locally pending backend lock statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_PendingLock *bktype_shstats;
+
+ if (!backend_has_lockstats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.lock_stats;
+
+ for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+ {
+#define LOCKSTAT_ACC(fld) \
+ (bktype_shstats->stats[i].fld += PendingBackendStats.pending_lock.stats[i].fld)
+ LOCKSTAT_ACC(waits);
+ LOCKSTAT_ACC(wait_time);
+ LOCKSTAT_ACC(fastpath_exceeded);
+#undef LOCKSTAT_ACC
+ }
+
+ MemSet(&PendingBackendStats.pending_lock, 0, sizeof(PgStat_PendingLock));
+ backend_has_lockstats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -286,6 +348,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
pgstat_backend_wal_have_pending())
has_pending_data = true;
+ /* Some lock data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -301,6 +367,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_WAL)
pgstat_flush_backend_entry_wal(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
+ pgstat_flush_backend_entry_lock(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -339,6 +408,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
+ backend_has_lockstats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index aec64f8fb4b..76116db3593 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -131,6 +131,8 @@ pgstat_count_lock_fastpath_exceeded(uint8 locktag_type)
PendingLockStats.stats[locktag_type].fastpath_exceeded++;
have_lockstats = true;
pgstat_report_fixed = true;
+
+ pgstat_count_backend_lock_fastpath_exceeded(locktag_type);
}
/*
@@ -147,4 +149,6 @@ pgstat_count_lock_waits(uint8 locktag_type, long msecs)
PendingLockStats.stats[locktag_type].wait_time += (PgStat_Counter) msecs;
have_lockstats = true;
pgstat_report_fixed = true;
+
+ pgstat_count_backend_lock_waits(locktag_type, msecs);
}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 353607954ad..3f7c238e557 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1740,8 +1740,8 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
/*
* pg_stat_lock_build_tuples
*
- * Helper routine for pg_stat_get_lock(), filling a result tuplestore with one
- * tuple for each lock type.
+ * Helper routine for pg_stat_get_lock() and pg_stat_get_backend_lock(),
+ * filling a result tuplestore with one tuple for each lock type.
*/
static void
pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo,
@@ -1788,6 +1788,31 @@ pg_stat_get_lock(PG_FUNCTION_ARGS)
return (Datum) 0;
}
+/*
+ * Returns lock statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_lock(PG_FUNCTION_ARGS)
+{
+ int pid;
+ ReturnSetInfo *rsinfo;
+ PgStat_Backend *backend_stats;
+
+ InitMaterializedSRF(fcinfo, 0);
+ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ return (Datum) 0;
+
+ pg_stat_lock_build_tuples(rsinfo, backend_stats->lock_stats.stats,
+ backend_stats->stat_reset_timestamp);
+
+ return (Datum) 0;
+}
+
/*
* Returns statistics of SLRU caches.
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..87f7161527e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6092,6 +6092,14 @@
proargmodes => '{i,o,o,o,o,o,o}',
proargnames => '{backend_pid,wal_records,wal_fpi,wal_bytes,wal_fpi_bytes,wal_buffers_full,stats_reset}',
prosrc => 'pg_stat_get_backend_wal' },
+{ oid => '9682', descr => 'statistics: backend lock statistics',
+ proname => 'pg_stat_get_backend_lock', prorows => '10', proretset => 't',
+ provolatile => 'v', proparallel => 'r', prorettype => 'record',
+ proargtypes => 'int4',
+ proallargtypes => '{int4,text,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o}',
+ proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
+ prosrc => 'pg_stat_get_backend_lock' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..b5e69e6250e 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -523,6 +523,7 @@ typedef struct PgStat_Backend
TimestampTz stat_reset_timestamp;
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
+ PgStat_PendingLock lock_stats;
} PgStat_Backend;
/* ---------
@@ -535,6 +536,12 @@ typedef struct PgStat_BackendPending
* Backend statistics store the same amount of IO data as PGSTAT_KIND_IO.
*/
PgStat_PendingIO pending_io;
+
+ /*
+ * Backend statistics store the same amount of lock data as
+ * PGSTAT_KIND_LOCK.
+ */
+ PgStat_PendingLock pending_lock;
} PgStat_BackendPending;
/*
@@ -586,6 +593,10 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
IOContext io_context,
IOOp io_op, uint32 cnt,
uint64 bytes);
+
+/* used by pgstat_lock.c for lock stats tracked in backends */
+extern void pgstat_count_backend_lock_waits(uint8 locktag_type, long msecs);
+extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index fe463faaf63..b0788336ae3 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -705,7 +705,8 @@ extern void pgstat_archiver_snapshot_cb(void);
/* flags for pgstat_flush_backend() */
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL)
+#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index bbb1db3c433..fa550676f83 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -2019,6 +2019,10 @@ BEGIN
END;
$$;
SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset
+-- Test pg_stat_get_backend_lock()
+SELECT fastpath_exceeded AS backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation' \gset
-- Needs a lock on each partition
SELECT count(*) FROM part_test;
count
@@ -2039,5 +2043,13 @@ SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE loc
t
(1 row)
+SELECT fastpath_exceeded > :backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation';
+ ?column?
+----------
+ t
+(1 row)
+
DROP TABLE part_test;
-- End of Stats Test
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 610fd21fae4..f5683302a75 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -998,6 +998,11 @@ $$;
SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset
+-- Test pg_stat_get_backend_lock()
+SELECT fastpath_exceeded AS backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation' \gset
+
-- Needs a lock on each partition
SELECT count(*) FROM part_test;
@@ -1006,6 +1011,10 @@ SELECT pg_stat_force_next_flush();
SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation';
+SELECT fastpath_exceeded > :backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation';
+
DROP TABLE part_test;
-- End of Stats Test
--
2.34.1
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-04 21:32 Tristan Partin <tristan@partin.io>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
1 sibling, 1 reply; 12+ messages in thread
From: Tristan Partin @ 2026-06-04 21:32 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: pgsql-hackers <pgsql-hackers@lists.postgresql.org>
On Wed Jun 3, 2026 at 1:59 PM UTC, Bertrand Drouvot wrote:
> Hi hackers,
>
> Now that we have global lock statistics since 4019f725f5d, it could be useful
> to have the same kind of information on a per-backend basis.
>
> Indeed, pg_stat_lock gives us cluster-wide aggregates: total waits, total wait
> time, total fast-path exceeded across all backends since last reset.
>
> When we see high numbers, we can't answer:
>
> - Which backend is affected the most?
> - Is it one backend affected or many?
> - Is a specific application or connection pool suffering?
> - After a specific workload/application is improved, did its lock behavior
> improve?
>
> With per-backend lock stats, we could:
>
> 1/ Isolate problematic sessions. We can correlate locks behavior with specific
> PIDs visible in pg_stat_activity: identify the exact application_name or user
> experiencing lock waits.
>
> 2/ Debug live contention. During an incident, we could pinpoint which backends
> are experiencing fast-path exhaustion or lock waits without having to reset
> global stats and lose history.
>
> 3/ Define workload characterization. Different backend types may have very
> different lock profiles. Per-backend stats would let us see this directly.
>
> 4/ Compare before/after per session. We could measure a single backend's lock
> behavior across a specific operation, which is impossible with global counters
> that include metrics from all other backends.
>
> IO and WAL stats already have per-backend counterparts (pg_stat_get_backend_io(),
> pg_stat_get_backend_wal()). Lock stats are the same class of operational data:
> having them only at the global level is an inconsistency that limits observability.
The motivation makes sense to me.
> As far the technical implementation:
>
> This data can be retrieved with a new system function called
> pg_stat_get_backend_lock(), that returns one tuple per lock type based on the PID
> provided in input.
>
> pgstat_flush_backend() gains a new flag value, able to control the flush of the
> lock stats.
>
> This patch relies mostly on the infrastructure provided by 9aea73fc61d4, that
> has introduced backend statistics.
>
> The overhead (2 functions calls and counters increments) on the hot path (normal
> lock acquisition) is zero: counters are only incremented on paths that are already
> "slow" (post deadlock timeout waits, fast-path slot exhaustion) and does not add
> that much memory per-backend: PgStat_PendingLock is 288 bytes.
>
> The patch is made of 2 sub-patches:
>
> 0001: Refactor pg_stat_get_lock() to use a helper function
> +static void
> +pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo,
> + PgStat_LockEntry *lock_stats,
> + TimestampTz stat_reset_timestamp)
I think that the alignment of the second and third arguments could be
off by one. They should line up with the capital R in ReturnSetInfo.
> - values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
> + if (stat_reset_timestamp != 0)
> + values[i] = TimestampTzGetDatum(stat_reset_timestamp);
> + else
> + nulls[i] = true;
It's not super clear to me why this changed in the first patch. Perhaps
it is meant to be in the second patch? I see in the second patch that we
use the stat_reset_timestamp from the backend stats instead of the lock
stats in pg_stat_get_backend_lock(). The motivation makes sense. It
might be cleaner to move the change into patch 2.
> 0002: Add per-backend lock statistics
> + Returns lock statistics about the backend with the specified
> + process ID. The output fields are exactly the same as the ones in the
> + <structname>pg_stat_lock</structname> view.
It probably makes sense to link to pg_stat_lock here.
Other than the few comments I had, this patchset looks good. It follows
patterns that were already established with the per-backend IO and WAL
stats.
--
Tristan Partin
PostgreSQL Contributors Team
AWS (https://aws.amazon.com)
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-05 08:29 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Tristan Partin <tristan@partin.io>
0 siblings, 1 reply; 12+ messages in thread
From: Bertrand Drouvot @ 2026-06-05 08:29 UTC (permalink / raw)
To: Tristan Partin <tristan@partin.io>; +Cc: pgsql-hackers <pgsql-hackers@lists.postgresql.org>
Hi,
On Thu, Jun 04, 2026 at 09:32:39PM +0000, Tristan Partin wrote:
> On Wed Jun 3, 2026 at 1:59 PM UTC, Bertrand Drouvot wrote:
>
> The motivation makes sense to me.
Thanks for looking at it and sharing your thoughts!
> > 0001: Refactor pg_stat_get_lock() to use a helper function
>
> > +static void
> > +pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo,
> > + PgStat_LockEntry *lock_stats,
> > + TimestampTz stat_reset_timestamp)
>
> I think that the alignment of the second and third arguments could be
> off by one. They should line up with the capital R in ReturnSetInfo.
They look ok to me in the C file, what about you?
> > - values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
> > + if (stat_reset_timestamp != 0)
> > + values[i] = TimestampTzGetDatum(stat_reset_timestamp);
> > + else
> > + nulls[i] = true;
>
> It's not super clear to me why this changed in the first patch.
It's to make less "noise" in the second patch and keep the second patch focusing
only on the "new feature". It's to ease to review but could be merged before
being pushed would the commiter decides to do so.
> > 0002: Add per-backend lock statistics
>
> > + Returns lock statistics about the backend with the specified
> > + process ID. The output fields are exactly the same as the ones in the
> > + <structname>pg_stat_lock</structname> view.
>
> It probably makes sense to link to pg_stat_lock here.
Not sure as that would not be consistent with pg_stat_get_backend_io and
pg_stat_get_backend_wal descriptions in monitoring.sgml.
> Other than the few comments I had, this patchset looks good. It follows
> patterns that were already established with the per-backend IO and WAL
> stats.
Thanks!
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-05 16:02 Tristan Partin <tristan@partin.io>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 0 replies; 12+ messages in thread
From: Tristan Partin @ 2026-06-05 16:02 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: pgsql-hackers <pgsql-hackers@lists.postgresql.org>
On Fri Jun 5, 2026 at 8:29 AM UTC, Bertrand Drouvot wrote:
> Hi,
>
> On Thu, Jun 04, 2026 at 09:32:39PM +0000, Tristan Partin wrote:
>> On Wed Jun 3, 2026 at 1:59 PM UTC, Bertrand Drouvot wrote:
>> > 0001: Refactor pg_stat_get_lock() to use a helper function
>>
>> > +static void
>> > +pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo,
>> > + PgStat_LockEntry *lock_stats,
>> > + TimestampTz stat_reset_timestamp)
>>
>> I think that the alignment of the second and third arguments could be
>> off by one. They should line up with the capital R in ReturnSetInfo.
Probably just my editor being weird if it looks good to you!
> They look ok to me in the C file, what about you?
>
>> > - values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
>> > + if (stat_reset_timestamp != 0)
>> > + values[i] = TimestampTzGetDatum(stat_reset_timestamp);
>> > + else
>> > + nulls[i] = true;
>>
>> It's not super clear to me why this changed in the first patch.
>
> It's to make less "noise" in the second patch and keep the second patch focusing
> only on the "new feature". It's to ease to review but could be merged before
> being pushed would the commiter decides to do so.
Sounds good. To me it made the review a little more difficult, but
I understand the motivation.
>> > 0002: Add per-backend lock statistics
>>
>> > + Returns lock statistics about the backend with the specified
>> > + process ID. The output fields are exactly the same as the ones in the
>> > + <structname>pg_stat_lock</structname> view.
>>
>> It probably makes sense to link to pg_stat_lock here.
>
> Not sure as that would not be consistent with pg_stat_get_backend_io and
> pg_stat_get_backend_wal descriptions in monitoring.sgml.
Makes sense. Maybe we can update that in a future documentation update.
I'll go ahead and submit something in a separate thread.
--
Tristan Partin
PostgreSQL Contributors Team
AWS (https://aws.amazon.com)
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-24 05:57 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
1 sibling, 1 reply; 12+ messages in thread
From: Michael Paquier @ 2026-06-24 05:57 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
On Wed, Jun 03, 2026 at 01:58:41PM +0000, Bertrand Drouvot wrote:
> 0001: Refactor pg_stat_get_lock() to use a helper function
>
> Extract the tuple-building logic from pg_stat_get_lock() into a new
> static helper pg_stat_lock_build_tuples(). This is in preparation for
> pg_stat_get_backend_lock() which will reuse the same helper, following
> the pattern established by pg_stat_io_build_tuples() for IO stats and
> pg_stat_wal_build_tuple() for WAL stats.
- values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
+ if (stat_reset_timestamp != 0)
+ values[i] = TimestampTzGetDatum(stat_reset_timestamp);
+ else
+ nulls[i] = true;
Wait a minute here. I was wondering for a couple of minutes if we
should do that on HEAD as well, but we have reset_after_failure that
would set it to a nice value for the persistent part of the data..
That looks OK.
> 0002: Add per-backend lock statistics
+/* used by pgstat_lock.c for lock stats tracked in backends */
+extern void pgstat_count_backend_lock_waits(uint8 locktag_type, long msecs);
+extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
Nit. pgstat_fetch_stat_backend() and routines listed below are not
related to pgstat_lock.c. Add a newline perhaps?
I don't see much popping out on a closer read of 0002 (well, we've
discussed this patch and being able to see the balancing of lock
acquisitions across live backends is something that can be handy). As
far as I can see, you rely on the same infra as what has been done for
IO and WAL. Nice to see backend_has_lockstats being kept local to
pgstat_backend.c.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../ajtx2_k6qTsf-noM@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-24 07:21 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 12+ messages in thread
From: Bertrand Drouvot @ 2026-06-24 07:21 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: pgsql-hackers@lists.postgresql.org
Hi,
On Wed, Jun 24, 2026 at 02:57:47PM +0900, Michael Paquier wrote:
> On Wed, Jun 03, 2026 at 01:58:41PM +0000, Bertrand Drouvot wrote:
> > 0001: Refactor pg_stat_get_lock() to use a helper function
> >
> > Extract the tuple-building logic from pg_stat_get_lock() into a new
> > static helper pg_stat_lock_build_tuples(). This is in preparation for
> > pg_stat_get_backend_lock() which will reuse the same helper, following
> > the pattern established by pg_stat_io_build_tuples() for IO stats and
> > pg_stat_wal_build_tuple() for WAL stats.
>
> - values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
> + if (stat_reset_timestamp != 0)
> + values[i] = TimestampTzGetDatum(stat_reset_timestamp);
> + else
> + nulls[i] = true;
>
> Wait a minute here. I was wondering for a couple of minutes if we
> should do that on HEAD as well, but we have reset_after_failure that
> would set it to a nice value for the persistent part of the data..
Right and per-backend stats are not written to disk, hence the need here.
>
> > 0002: Add per-backend lock statistics
>
> +/* used by pgstat_lock.c for lock stats tracked in backends */
> +extern void pgstat_count_backend_lock_waits(uint8 locktag_type, long msecs);
> +extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
> extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
>
> Nit. pgstat_fetch_stat_backend() and routines listed below are not
> related to pgstat_lock.c. Add a newline perhaps?
Yeah. This is not introduced by the patch, as it's currently not related to
pgstat_io.c on HEAD either, but let's clean it in passing. Done in v2 (that's
the only change compared to v1).
> far as I can see, you rely on the same infra as what has been done for
> IO and WAL.
Right.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v2-0001-Refactor-pg_stat_get_lock-to-use-a-helper-functio.patch (3.1K, ../../ajuFXuTckRaqdZbm@bdtpg/2-v2-0001-Refactor-pg_stat_get_lock-to-use-a-helper-functio.patch)
download | inline diff:
From c03251b425467553798c21e29d24ea87826f9c7f Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Wed, 3 Jun 2026 13:04:26 +0000
Subject: [PATCH v2 1/2] Refactor pg_stat_get_lock() to use a helper function
Extract the tuple-building logic from pg_stat_get_lock() into a new
static helper pg_stat_lock_build_tuples(). This is in preparation for
pg_stat_get_backend_lock() which will reuse the same helper, following
the pattern established by pg_stat_io_build_tuples() for IO stats and
pg_stat_wal_build_tuple() for WAL stats.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Tristan Partin <tristan@partin.io>
Discussion: https://postgr.es/m/aiAzEY%2BcMQb/W8yu%40bdtpg
---
src/backend/utils/adt/pgstatfuncs.c | 47 +++++++++++++++++++----------
1 file changed, 31 insertions(+), 16 deletions(-)
100.0% src/backend/utils/adt/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6f9c9c72de5..353607954ad 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1737,38 +1737,53 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
wal_stats->stat_reset_timestamp));
}
-Datum
-pg_stat_get_lock(PG_FUNCTION_ARGS)
+/*
+ * pg_stat_lock_build_tuples
+ *
+ * Helper routine for pg_stat_get_lock(), filling a result tuplestore with one
+ * tuple for each lock type.
+ */
+static void
+pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo,
+ PgStat_LockEntry *lock_stats,
+ TimestampTz stat_reset_timestamp)
{
#define PG_STAT_LOCK_COLS 5
- ReturnSetInfo *rsinfo;
- PgStat_Lock *lock_stats;
-
- InitMaterializedSRF(fcinfo, 0);
- rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
-
- lock_stats = pgstat_fetch_stat_lock();
-
for (int lcktype = 0; lcktype <= LOCKTAG_LAST_TYPE; lcktype++)
{
- const char *locktypename;
Datum values[PG_STAT_LOCK_COLS] = {0};
bool nulls[PG_STAT_LOCK_COLS] = {0};
- PgStat_LockEntry *lck_stats = &lock_stats->stats[lcktype];
+ PgStat_LockEntry *lck_stats = &lock_stats[lcktype];
int i = 0;
- locktypename = LockTagTypeNames[lcktype];
-
- values[i++] = CStringGetTextDatum(locktypename);
+ values[i++] = CStringGetTextDatum(LockTagTypeNames[lcktype]);
values[i++] = Int64GetDatum(lck_stats->waits);
values[i++] = Int64GetDatum(lck_stats->wait_time);
values[i++] = Int64GetDatum(lck_stats->fastpath_exceeded);
- values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
+ if (stat_reset_timestamp != 0)
+ values[i] = TimestampTzGetDatum(stat_reset_timestamp);
+ else
+ nulls[i] = true;
Assert(i + 1 == PG_STAT_LOCK_COLS);
tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
}
+}
+
+Datum
+pg_stat_get_lock(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsinfo;
+ PgStat_Lock *lock_stats;
+
+ InitMaterializedSRF(fcinfo, 0);
+ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+ lock_stats = pgstat_fetch_stat_lock();
+
+ pg_stat_lock_build_tuples(rsinfo, lock_stats->stats,
+ lock_stats->stat_reset_timestamp);
return (Datum) 0;
}
--
2.34.1
[text/x-diff] v2-0002-Add-per-backend-lock-statistics.patch (13.6K, ../../ajuFXuTckRaqdZbm@bdtpg/3-v2-0002-Add-per-backend-lock-statistics.patch)
download | inline diff:
From 421160967aa541dc514c31e2ba2fdc3edd52e7d0 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Wed, 3 Jun 2026 13:05:03 +0000
Subject: [PATCH v2 2/2] Add per-backend lock statistics
This commit adds per-backend lock statistics, providing the same information as
pg_stat_lock, except that it is now possible to retrieve those stats (lock wait
counts, wait times, and fast-path exceeded count) on a per-backend basis.
This data can be retrieved with a new system function called
pg_stat_get_backend_lock(), that returns one tuple per lock type based on the PID
provided in input. Like pg_stat_get_backend_io(), this is useful when joined
with pg_stat_activity to get a live picture of the locks behavior for each running
backend.
pgstat_flush_backend() gains a new flag value, able to control the flush of the
lock stats.
This commit relies mostly on the infrastructure provided by 9aea73fc61d4, that
has introduced backend statistics.
XXX: Bump catalog version. A bump of PGSTAT_FILE_FORMAT_ID is not required,
as backend stats do not persist on disk.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Tristan Partin <tristan@partin.io>
Discussion: https://postgr.es/m/aiAzEY%2BcMQb/W8yu%40bdtpg
---
doc/src/sgml/monitoring.sgml | 19 ++++++
src/backend/utils/activity/pgstat_backend.c | 70 +++++++++++++++++++++
src/backend/utils/activity/pgstat_lock.c | 4 ++
src/backend/utils/adt/pgstatfuncs.c | 29 ++++++++-
src/include/catalog/pg_proc.dat | 8 +++
src/include/pgstat.h | 12 ++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/regress/expected/stats.out | 12 ++++
src/test/regress/sql/stats.sql | 9 +++
9 files changed, 163 insertions(+), 3 deletions(-)
14.7% doc/src/sgml/
38.6% src/backend/utils/activity/
14.8% src/backend/utils/adt/
8.7% src/include/catalog/
9.9% src/include/
6.7% src/test/regress/expected/
6.1% src/test/regress/sql/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..3936fb62a5d 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5545,6 +5545,25 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-lock" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_lock</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_lock</function> ( <type>integer</type> )
+ <returnvalue>setof record</returnvalue>
+ </para>
+ <para>
+ Returns lock statistics about the backend with the specified
+ process ID. The output fields are exactly the same as the ones in the
+ <structname>pg_stat_lock</structname> view.
+ </para>
+ <para>
+ The function does not return lock statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 73461c9bca5..297eda0a489 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -39,6 +39,7 @@
*/
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
+static bool backend_has_lockstats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -86,6 +87,37 @@ pgstat_count_backend_io_op(IOObject io_object, IOContext io_context,
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report lock stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_lock_waits(uint8 locktag_type, long msecs)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+ PendingBackendStats.pending_lock.stats[locktag_type].waits++;
+ PendingBackendStats.pending_lock.stats[locktag_type].wait_time += (PgStat_Counter) msecs;
+
+ backend_has_lockstats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+ PendingBackendStats.pending_lock.stats[locktag_type].fastpath_exceeded++;
+
+ backend_has_lockstats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -262,6 +294,36 @@ pgstat_flush_backend_entry_wal(PgStat_EntryRef *entry_ref)
prevBackendWalUsage = pgWalUsage;
}
+/*
+ * Flush out locally pending backend lock statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_PendingLock *bktype_shstats;
+
+ if (!backend_has_lockstats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.lock_stats;
+
+ for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+ {
+#define LOCKSTAT_ACC(fld) \
+ (bktype_shstats->stats[i].fld += PendingBackendStats.pending_lock.stats[i].fld)
+ LOCKSTAT_ACC(waits);
+ LOCKSTAT_ACC(wait_time);
+ LOCKSTAT_ACC(fastpath_exceeded);
+#undef LOCKSTAT_ACC
+ }
+
+ MemSet(&PendingBackendStats.pending_lock, 0, sizeof(PgStat_PendingLock));
+ backend_has_lockstats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -286,6 +348,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
pgstat_backend_wal_have_pending())
has_pending_data = true;
+ /* Some lock data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -301,6 +367,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_WAL)
pgstat_flush_backend_entry_wal(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
+ pgstat_flush_backend_entry_lock(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -339,6 +408,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
+ backend_has_lockstats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index aec64f8fb4b..76116db3593 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -131,6 +131,8 @@ pgstat_count_lock_fastpath_exceeded(uint8 locktag_type)
PendingLockStats.stats[locktag_type].fastpath_exceeded++;
have_lockstats = true;
pgstat_report_fixed = true;
+
+ pgstat_count_backend_lock_fastpath_exceeded(locktag_type);
}
/*
@@ -147,4 +149,6 @@ pgstat_count_lock_waits(uint8 locktag_type, long msecs)
PendingLockStats.stats[locktag_type].wait_time += (PgStat_Counter) msecs;
have_lockstats = true;
pgstat_report_fixed = true;
+
+ pgstat_count_backend_lock_waits(locktag_type, msecs);
}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 353607954ad..3f7c238e557 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1740,8 +1740,8 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
/*
* pg_stat_lock_build_tuples
*
- * Helper routine for pg_stat_get_lock(), filling a result tuplestore with one
- * tuple for each lock type.
+ * Helper routine for pg_stat_get_lock() and pg_stat_get_backend_lock(),
+ * filling a result tuplestore with one tuple for each lock type.
*/
static void
pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo,
@@ -1788,6 +1788,31 @@ pg_stat_get_lock(PG_FUNCTION_ARGS)
return (Datum) 0;
}
+/*
+ * Returns lock statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_lock(PG_FUNCTION_ARGS)
+{
+ int pid;
+ ReturnSetInfo *rsinfo;
+ PgStat_Backend *backend_stats;
+
+ InitMaterializedSRF(fcinfo, 0);
+ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ return (Datum) 0;
+
+ pg_stat_lock_build_tuples(rsinfo, backend_stats->lock_stats.stats,
+ backend_stats->stat_reset_timestamp);
+
+ return (Datum) 0;
+}
+
/*
* Returns statistics of SLRU caches.
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa76c7923f0..775afed8b35 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6092,6 +6092,14 @@
proargmodes => '{i,o,o,o,o,o,o}',
proargnames => '{backend_pid,wal_records,wal_fpi,wal_bytes,wal_fpi_bytes,wal_buffers_full,stats_reset}',
prosrc => 'pg_stat_get_backend_wal' },
+{ oid => '9682', descr => 'statistics: backend lock statistics',
+ proname => 'pg_stat_get_backend_lock', prorows => '10', proretset => 't',
+ provolatile => 'v', proparallel => 'r', prorettype => 'record',
+ proargtypes => 'int4',
+ proallargtypes => '{int4,text,int8,int8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o}',
+ proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
+ prosrc => 'pg_stat_get_backend_lock' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..ec5ec1461a9 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -523,6 +523,7 @@ typedef struct PgStat_Backend
TimestampTz stat_reset_timestamp;
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
+ PgStat_PendingLock lock_stats;
} PgStat_Backend;
/* ---------
@@ -535,6 +536,12 @@ typedef struct PgStat_BackendPending
* Backend statistics store the same amount of IO data as PGSTAT_KIND_IO.
*/
PgStat_PendingIO pending_io;
+
+ /*
+ * Backend statistics store the same amount of lock data as
+ * PGSTAT_KIND_LOCK.
+ */
+ PgStat_PendingLock pending_lock;
} PgStat_BackendPending;
/*
@@ -586,6 +593,11 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
IOContext io_context,
IOOp io_op, uint32 cnt,
uint64 bytes);
+
+/* used by pgstat_lock.c for lock stats tracked in backends */
+extern void pgstat_count_backend_lock_waits(uint8 locktag_type, long msecs);
+extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 3ca4f454895..b3dc3ff7d8b 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -705,7 +705,8 @@ extern void pgstat_archiver_snapshot_cb(void);
/* flags for pgstat_flush_backend() */
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL)
+#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index bbb1db3c433..fa550676f83 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -2019,6 +2019,10 @@ BEGIN
END;
$$;
SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset
+-- Test pg_stat_get_backend_lock()
+SELECT fastpath_exceeded AS backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation' \gset
-- Needs a lock on each partition
SELECT count(*) FROM part_test;
count
@@ -2039,5 +2043,13 @@ SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE loc
t
(1 row)
+SELECT fastpath_exceeded > :backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation';
+ ?column?
+----------
+ t
+(1 row)
+
DROP TABLE part_test;
-- End of Stats Test
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 610fd21fae4..f5683302a75 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -998,6 +998,11 @@ $$;
SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset
+-- Test pg_stat_get_backend_lock()
+SELECT fastpath_exceeded AS backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation' \gset
+
-- Needs a lock on each partition
SELECT count(*) FROM part_test;
@@ -1006,6 +1011,10 @@ SELECT pg_stat_force_next_flush();
SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation';
+SELECT fastpath_exceeded > :backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation';
+
DROP TABLE part_test;
-- End of Stats Test
--
2.34.1
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-24 13:49 Tatsuya Kawata <kawatatatsuya0913@gmail.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 12+ messages in thread
From: Tatsuya Kawata @ 2026-06-24 13:49 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers@lists.postgresql.org
Hi Bertrand-san,
I tested the patch locally and did not find any functional issue.
I have two suggestions below.
== Doc suggestion ==
When the workload uses parallel scans, pg_stat_lock.fastpath_exceeded
grows more than what pg_stat_get_backend_lock shows for any
individual pid. The gap is the parallel workers' contribution:
each worker locks the valid subplans independently, accumulates into
its own per-backend entry, and the entry is dropped at worker exit
-- so the contribution is not folded into the leader's per-backend
view.
This is a property of the per-backend stats infrastructure rather
than something this patch introduces, but since one of the stated
motivations is "Isolate problematic sessions", users may
intuitively expect parallel-worker contributions to be visible
under the leader's pid. A short note in the docs of the per-backend
functions clarifying that
parallel-worker contributions are not aggregated into the leader's
entry would help avoid that misunderstanding.
== Column suggestion for pg_stat_lock ==
pg_stat_io has a backend_type column, which lets users still see
parallel-worker contributions in aggregate (via WHERE
backend_type='background worker') after workers exit. pg_stat_lock
has only locktype, so worker contributions blend into the relation
row and cannot be separated even in aggregate.
This may be out of scope for the present patch, but I wonder if
adding a backend_type axis to pg_stat_lock could be considered in a
follow-up patch. It would give an alternative attribution path
(similar to pg_stat_io's backend_type column) when per-backend
statistics cannot help.
Regards,
Tatsuya Kawata
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-24 15:50 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Tatsuya Kawata <kawatatatsuya0913@gmail.com>
0 siblings, 1 reply; 12+ messages in thread
From: Bertrand Drouvot @ 2026-06-24 15:50 UTC (permalink / raw)
To: Tatsuya Kawata <kawatatatsuya0913@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers@lists.postgresql.org
Hi Kawata-san,
On Wed, Jun 24, 2026 at 10:49:44PM +0900, Tatsuya Kawata wrote:
> Hi Bertrand-san,
>
> I tested the patch locally and did not find any functional issue.
Thanks!
> I have two suggestions below.
>
> == Doc suggestion ==
>
> When the workload uses parallel scans, pg_stat_lock.fastpath_exceeded
> grows more than what pg_stat_get_backend_lock shows for any
> individual pid. The gap is the parallel workers' contribution:
> each worker locks the valid subplans independently, accumulates into
> its own per-backend entry, and the entry is dropped at worker exit
> -- so the contribution is not folded into the leader's per-backend
> view.
>
> This is a property of the per-backend stats infrastructure rather
> than something this patch introduces, but since one of the stated
> motivations is "Isolate problematic sessions", users may
> intuitively expect parallel-worker contributions to be visible
> under the leader's pid. A short note in the docs of the per-backend
> functions clarifying that
> parallel-worker contributions are not aggregated into the leader's
> entry would help avoid that misunderstanding.
That's right, and the same could be said for per-backend I/O and WAL stats.
The stats are flushed when the transaction finish and then are visible from that
moment. The stats are gone once the backend exit. For parallel workers this window
is very short (between the flush and the exit) so that we can say that their stats
are not visible in practice.
I think that flushing statistics within running transactions [1] could help to
see what's going on for parallel workers too.
That said, I'm not sure the doc needs any clarifications given that those functions
take a PID as parameter and that they state something like "Returns I/O
/WAL statistics about the backend with the specified process ID".
> == Column suggestion for pg_stat_lock ==
>
> pg_stat_io has a backend_type column, which lets users still see
> parallel-worker contributions in aggregate (via WHERE
> backend_type='background worker') after workers exit. pg_stat_lock
> has only locktype, so worker contributions blend into the relation
> row and cannot be separated even in aggregate.
>
> This may be out of scope for the present patch, but I wonder if
> adding a backend_type axis to pg_stat_lock could be considered in a
> follow-up patch. It would give an alternative attribution path
> (similar to pg_stat_io's backend_type column) when per-backend
> statistics cannot help.
It's not related to this thread so that might be worth a dedicated one but I'm
not sure that would be more actionable while consuming more resources.
[1]: https://postgr.es/m/CAA5RZ0uA-4qcD3%2B2hjcE_-zQUBhvWf5foPM2vzYneFKrJLsBDQ%40mail.gmail.com
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-25 15:40 Rui Zhao <zhaorui126@gmail.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 2 replies; 12+ messages in thread
From: Rui Zhao @ 2026-06-25 15:40 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Tatsuya Kawata <kawatatatsuya0913@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers@lists.postgresql.org
Hi Bertrand,
I reviewed and tested v2; it builds cleanly. The implementation closely
mirrors the existing per-backend IO/WAL stats (same flush path, struct
layout, and backend-type guard), and the hot path is untouched: the
per-backend counters piggyback on the existing global counting points
(pgstat_count_lock_waits / pgstat_count_lock_fastpath_exceeded), so they
only fire where pg_stat_lock already counts -- fastpath_exceeded when the
fast-path slot limit is exceeded, and waits/wait_time only after a wait
longer than deadlock_timeout.
One tiny nit: in pgstat_count_backend_lock_waits() and
pgstat_count_backend_lock_fastpath_exceeded(), the Assert() is directly
followed by the counter update. A blank line after the Assert() would read
a bit better and is the more usual style in this code.
Otherwise LGTM.
Regards,
Rui
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-27 10:55 Tatsuya Kawata <kawatatatsuya0913@gmail.com>
parent: Rui Zhao <zhaorui126@gmail.com>
1 sibling, 0 replies; 12+ messages in thread
From: Tatsuya Kawata @ 2026-06-27 10:55 UTC (permalink / raw)
To: Rui Zhao <zhaorui126@gmail.com>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers@lists.postgresql.org
Hi Bertrand-san,
Thanks for the explanation!
> I think that flushing statistics within running transactions [1] could
help to
> see what's going on for parallel workers too.
Thanks, I'll follow that thread.
> For parallel workers this window
> is very short (between the flush and the exit) so that we can say that
their stats
> are not visible in practice.
> That said, I'm not sure the doc needs any clarifications given that those
functions
> take a PID as parameter and that they state something like "Returns I/O
> /WAL statistics about the backend with the specified process ID".
Right, it's per-PID, and that part is clear. My concern is that it isn't
obvious from the function description that a single query can span more
than one PID, as it does with parallel workers.
If it's worth documenting, I agree splitting it across lock / I/O / WAL
separately isn't great -- a single place such as "Statistics Functions"
seems better. Either way it's separate from this patch, so I'll start a
dedicated thread with a draft.
> It's not related to this thread so that might be worth a dedicated one
but I'm
> not sure that would be more actionable while consuming more resources.
Thanks -- agreed there's a tradeoff here. I can see a use case for it, so
I'd like to weigh that against the cost and, if it still looks worth it,
post it as a separate patch to get more opinions.
No need to reflect either of these in the current patch.
Regards,
Tatsuya Kawata
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-30 05:22 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Rui Zhao <zhaorui126@gmail.com>
1 sibling, 1 reply; 12+ messages in thread
From: Bertrand Drouvot @ 2026-06-30 05:22 UTC (permalink / raw)
To: Rui Zhao <zhaorui126@gmail.com>; +Cc: Tatsuya Kawata <kawatatatsuya0913@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers@lists.postgresql.org
Hi,
On Thu, Jun 25, 2026 at 11:40:19PM +0800, Rui Zhao wrote:
> Hi Bertrand,
>
> I reviewed and tested v2; it builds cleanly. The implementation closely
> mirrors the existing per-backend IO/WAL stats (same flush path, struct
> layout, and backend-type guard), and the hot path is untouched: the
> per-backend counters piggyback on the existing global counting points
> (pgstat_count_lock_waits / pgstat_count_lock_fastpath_exceeded), so they
> only fire where pg_stat_lock already counts -- fastpath_exceeded when the
> fast-path slot limit is exceeded, and waits/wait_time only after a wait
> longer than deadlock_timeout.
Thanks for the review. Attached is a rebase due to c776550e466 and making
use of the same changes.
> One tiny nit: in pgstat_count_backend_lock_waits() and
> pgstat_count_backend_lock_fastpath_exceeded(), the Assert() is directly
> followed by the counter update. A blank line after the Assert() would read
> a bit better and is the more usual style in this code.
Yeah, done in the attached.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v3-0001-Refactor-pg_stat_get_lock-to-use-a-helper-functio.patch (3.2K, ../../akNShhd8wVUVYbaP@bdtpg/2-v3-0001-Refactor-pg_stat_get_lock-to-use-a-helper-functio.patch)
download | inline diff:
From 1ad276bc977d930fbbade278af60e9d85b7a542a Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 30 Jun 2026 04:48:56 +0000
Subject: [PATCH v3 1/2] Refactor pg_stat_get_lock() to use a helper function
Extract the tuple-building logic from pg_stat_get_lock() into a new
static helper pg_stat_lock_build_tuples(). This is in preparation for
pg_stat_get_backend_lock() which will reuse the same helper, following
the pattern established by pg_stat_io_build_tuples() for IO stats and
pg_stat_wal_build_tuple() for WAL stats.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Tristan Partin <tristan@partin.io>
Reviewed-by: Tatsuya Kawata <kawatatatsuya0913@gmail.com>
Reviewed-by: Rui Zhao <zhaorui126@gmail.com>
Discussion: https://postgr.es/m/aiAzEY%2BcMQb/W8yu%40bdtpg
---
src/backend/utils/adt/pgstatfuncs.c | 47 +++++++++++++++++++----------
1 file changed, 31 insertions(+), 16 deletions(-)
100.0% src/backend/utils/adt/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 0c59df17901..1f9165f1616 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1737,38 +1737,53 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
wal_stats->stat_reset_timestamp));
}
-Datum
-pg_stat_get_lock(PG_FUNCTION_ARGS)
+/*
+ * pg_stat_lock_build_tuples
+ *
+ * Helper routine for pg_stat_get_lock(), filling a result tuplestore with one
+ * tuple for each lock type.
+ */
+static void
+pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo,
+ PgStat_LockEntry *lock_stats,
+ TimestampTz stat_reset_timestamp)
{
#define PG_STAT_LOCK_COLS 5
- ReturnSetInfo *rsinfo;
- PgStat_Lock *lock_stats;
-
- InitMaterializedSRF(fcinfo, 0);
- rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
-
- lock_stats = pgstat_fetch_stat_lock();
-
for (int lcktype = 0; lcktype <= LOCKTAG_LAST_TYPE; lcktype++)
{
- const char *locktypename;
Datum values[PG_STAT_LOCK_COLS] = {0};
bool nulls[PG_STAT_LOCK_COLS] = {0};
- PgStat_LockEntry *lck_stats = &lock_stats->stats[lcktype];
+ PgStat_LockEntry *lck_stats = &lock_stats[lcktype];
int i = 0;
- locktypename = LockTagTypeNames[lcktype];
-
- values[i++] = CStringGetTextDatum(locktypename);
+ values[i++] = CStringGetTextDatum(LockTagTypeNames[lcktype]);
values[i++] = Int64GetDatum(lck_stats->waits);
values[i++] = Float8GetDatum(pg_stat_us_to_ms(lck_stats->wait_time));
values[i++] = Int64GetDatum(lck_stats->fastpath_exceeded);
- values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
+ if (stat_reset_timestamp != 0)
+ values[i] = TimestampTzGetDatum(stat_reset_timestamp);
+ else
+ nulls[i] = true;
Assert(i + 1 == PG_STAT_LOCK_COLS);
tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
}
+}
+
+Datum
+pg_stat_get_lock(PG_FUNCTION_ARGS)
+{
+ ReturnSetInfo *rsinfo;
+ PgStat_Lock *lock_stats;
+
+ InitMaterializedSRF(fcinfo, 0);
+ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+ lock_stats = pgstat_fetch_stat_lock();
+
+ pg_stat_lock_build_tuples(rsinfo, lock_stats->stats,
+ lock_stats->stat_reset_timestamp);
return (Datum) 0;
}
--
2.34.1
[text/x-diff] v3-0002-Add-per-backend-lock-statistics.patch (13.7K, ../../akNShhd8wVUVYbaP@bdtpg/3-v3-0002-Add-per-backend-lock-statistics.patch)
download | inline diff:
From 1ebe6581c7c741e6be858cb9ee3a4c46a9f2af75 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 30 Jun 2026 04:53:52 +0000
Subject: [PATCH v3 2/2] Add per-backend lock statistics
This commit adds per-backend lock statistics, providing the same information as
pg_stat_lock, except that it is now possible to retrieve those stats (lock wait
counts, wait times, and fast-path exceeded count) on a per-backend basis.
This data can be retrieved with a new system function called
pg_stat_get_backend_lock(), that returns one tuple per lock type based on the PID
provided in input. Like pg_stat_get_backend_io(), this is useful when joined
with pg_stat_activity to get a live picture of the locks behavior for each running
backend.
pgstat_flush_backend() gains a new flag value, able to control the flush of the
lock stats.
This commit relies mostly on the infrastructure provided by 9aea73fc61d4, that
has introduced backend statistics.
XXX: Bump catalog version. A bump of PGSTAT_FILE_FORMAT_ID is not required,
as backend stats do not persist on disk.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Tristan Partin <tristan@partin.io>
Reviewed-by: Tatsuya Kawata <kawatatatsuya0913@gmail.com>
Reviewed-by: Rui Zhao <zhaorui126@gmail.com>
Discussion: https://postgr.es/m/aiAzEY%2BcMQb/W8yu%40bdtpg
---
doc/src/sgml/monitoring.sgml | 19 ++++++
src/backend/utils/activity/pgstat_backend.c | 72 +++++++++++++++++++++
src/backend/utils/activity/pgstat_lock.c | 4 ++
src/backend/utils/adt/pgstatfuncs.c | 29 ++++++++-
src/include/catalog/pg_proc.dat | 8 +++
src/include/pgstat.h | 12 ++++
src/include/utils/pgstat_internal.h | 3 +-
src/test/regress/expected/stats.out | 12 ++++
src/test/regress/sql/stats.sql | 9 +++
9 files changed, 165 insertions(+), 3 deletions(-)
14.7% doc/src/sgml/
38.4% src/backend/utils/activity/
14.8% src/backend/utils/adt/
8.7% src/include/catalog/
10.1% src/include/
6.7% src/test/regress/expected/
6.1% src/test/regress/sql/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 6dcf05eb702..316d3c5767a 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5545,6 +5545,25 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para></entry>
</row>
+ <row>
+ <entry id="pg-stat-get-backend-lock" role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_stat_get_backend_lock</primary>
+ </indexterm>
+ <function>pg_stat_get_backend_lock</function> ( <type>integer</type> )
+ <returnvalue>setof record</returnvalue>
+ </para>
+ <para>
+ Returns lock statistics about the backend with the specified
+ process ID. The output fields are exactly the same as the ones in the
+ <structname>pg_stat_lock</structname> view.
+ </para>
+ <para>
+ The function does not return lock statistics for the checkpointer,
+ the background writer, the startup process and the autovacuum launcher.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 73461c9bca5..b736b2ccc6f 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -39,6 +39,7 @@
*/
static PgStat_BackendPending PendingBackendStats;
static bool backend_has_iostats = false;
+static bool backend_has_lockstats = false;
/*
* WAL usage counters saved from pgWalUsage at the previous call to
@@ -86,6 +87,39 @@ pgstat_count_backend_io_op(IOObject io_object, IOContext io_context,
pgstat_report_fixed = true;
}
+/*
+ * Utility routines to report lock stats for backends, kept here to avoid
+ * exposing PendingBackendStats to the outside world.
+ */
+void
+pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+
+ PendingBackendStats.pending_lock.stats[locktag_type].waits++;
+ PendingBackendStats.pending_lock.stats[locktag_type].wait_time += usecs;
+
+ backend_has_lockstats = true;
+ pgstat_report_fixed = true;
+}
+
+void
+pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type)
+{
+ if (!pgstat_tracks_backend_bktype(MyBackendType))
+ return;
+
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+
+ PendingBackendStats.pending_lock.stats[locktag_type].fastpath_exceeded++;
+
+ backend_has_lockstats = true;
+ pgstat_report_fixed = true;
+}
+
/*
* Returns statistics of a backend by proc number.
*/
@@ -262,6 +296,36 @@ pgstat_flush_backend_entry_wal(PgStat_EntryRef *entry_ref)
prevBackendWalUsage = pgWalUsage;
}
+/*
+ * Flush out locally pending backend lock statistics. Locking is managed
+ * by the caller.
+ */
+static void
+pgstat_flush_backend_entry_lock(PgStat_EntryRef *entry_ref)
+{
+ PgStatShared_Backend *shbackendent;
+ PgStat_PendingLock *bktype_shstats;
+
+ if (!backend_has_lockstats)
+ return;
+
+ shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+ bktype_shstats = &shbackendent->stats.lock_stats;
+
+ for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+ {
+#define LOCKSTAT_ACC(fld) \
+ (bktype_shstats->stats[i].fld += PendingBackendStats.pending_lock.stats[i].fld)
+ LOCKSTAT_ACC(waits);
+ LOCKSTAT_ACC(wait_time);
+ LOCKSTAT_ACC(fastpath_exceeded);
+#undef LOCKSTAT_ACC
+ }
+
+ MemSet(&PendingBackendStats.pending_lock, 0, sizeof(PgStat_PendingLock));
+ backend_has_lockstats = false;
+}
+
/*
* Flush out locally pending backend statistics
*
@@ -286,6 +350,10 @@ pgstat_flush_backend(bool nowait, uint32 flags)
pgstat_backend_wal_have_pending())
has_pending_data = true;
+ /* Some lock data pending? */
+ if ((flags & PGSTAT_BACKEND_FLUSH_LOCK) && backend_has_lockstats)
+ has_pending_data = true;
+
if (!has_pending_data)
return false;
@@ -301,6 +369,9 @@ pgstat_flush_backend(bool nowait, uint32 flags)
if (flags & PGSTAT_BACKEND_FLUSH_WAL)
pgstat_flush_backend_entry_wal(entry_ref);
+ if (flags & PGSTAT_BACKEND_FLUSH_LOCK)
+ pgstat_flush_backend_entry_lock(entry_ref);
+
pgstat_unlock_entry(entry_ref);
return false;
@@ -339,6 +410,7 @@ pgstat_create_backend(ProcNumber procnum)
MemSet(&PendingBackendStats, 0, sizeof(PgStat_BackendPending));
backend_has_iostats = false;
+ backend_has_lockstats = false;
/*
* Initialize prevBackendWalUsage with pgWalUsage so that
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index 8910a15634d..c20c7599683 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -131,6 +131,8 @@ pgstat_count_lock_fastpath_exceeded(uint8 locktag_type)
PendingLockStats.stats[locktag_type].fastpath_exceeded++;
have_lockstats = true;
pgstat_report_fixed = true;
+
+ pgstat_count_backend_lock_fastpath_exceeded(locktag_type);
}
/*
@@ -147,4 +149,6 @@ pgstat_count_lock_waits(uint8 locktag_type, PgStat_Counter usecs)
PendingLockStats.stats[locktag_type].wait_time += usecs;
have_lockstats = true;
pgstat_report_fixed = true;
+
+ pgstat_count_backend_lock_waits(locktag_type, usecs);
}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 1f9165f1616..0c6a20843a5 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1740,8 +1740,8 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
/*
* pg_stat_lock_build_tuples
*
- * Helper routine for pg_stat_get_lock(), filling a result tuplestore with one
- * tuple for each lock type.
+ * Helper routine for pg_stat_get_lock() and pg_stat_get_backend_lock(),
+ * filling a result tuplestore with one tuple for each lock type.
*/
static void
pg_stat_lock_build_tuples(ReturnSetInfo *rsinfo,
@@ -1788,6 +1788,31 @@ pg_stat_get_lock(PG_FUNCTION_ARGS)
return (Datum) 0;
}
+/*
+ * Returns lock statistics for a backend with given PID.
+ */
+Datum
+pg_stat_get_backend_lock(PG_FUNCTION_ARGS)
+{
+ int pid;
+ ReturnSetInfo *rsinfo;
+ PgStat_Backend *backend_stats;
+
+ InitMaterializedSRF(fcinfo, 0);
+ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+ pid = PG_GETARG_INT32(0);
+ backend_stats = pgstat_fetch_stat_backend_by_pid(pid, NULL);
+
+ if (!backend_stats)
+ return (Datum) 0;
+
+ pg_stat_lock_build_tuples(rsinfo, backend_stats->lock_stats.stats,
+ backend_stats->stat_reset_timestamp);
+
+ return (Datum) 0;
+}
+
/*
* Returns statistics of SLRU caches.
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index efe13b7866a..73bb7fbb430 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6092,6 +6092,14 @@
proargmodes => '{i,o,o,o,o,o,o}',
proargnames => '{backend_pid,wal_records,wal_fpi,wal_bytes,wal_fpi_bytes,wal_buffers_full,stats_reset}',
prosrc => 'pg_stat_get_backend_wal' },
+{ oid => '9682', descr => 'statistics: backend lock statistics',
+ proname => 'pg_stat_get_backend_lock', prorows => '10', proretset => 't',
+ provolatile => 'v', proparallel => 'r', prorettype => 'record',
+ proargtypes => 'int4',
+ proallargtypes => '{int4,text,int8,float8,int8,timestamptz}',
+ proargmodes => '{i,o,o,o,o,o}',
+ proargnames => '{backend_pid,locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
+ prosrc => 'pg_stat_get_backend_lock' },
{ oid => '6248', descr => 'statistics: information about WAL prefetching',
proname => 'pg_stat_get_recovery_prefetch', prorows => '1', proretset => 't',
provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 72496695999..58a44857f13 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -523,6 +523,7 @@ typedef struct PgStat_Backend
TimestampTz stat_reset_timestamp;
PgStat_BktypeIO io_stats;
PgStat_WalCounters wal_counters;
+ PgStat_PendingLock lock_stats;
} PgStat_Backend;
/* ---------
@@ -535,6 +536,12 @@ typedef struct PgStat_BackendPending
* Backend statistics store the same amount of IO data as PGSTAT_KIND_IO.
*/
PgStat_PendingIO pending_io;
+
+ /*
+ * Backend statistics store the same amount of lock data as
+ * PGSTAT_KIND_LOCK.
+ */
+ PgStat_PendingLock pending_lock;
} PgStat_BackendPending;
/*
@@ -586,6 +593,11 @@ extern void pgstat_count_backend_io_op(IOObject io_object,
IOContext io_context,
IOOp io_op, uint32 cnt,
uint64 bytes);
+
+/* used by pgstat_lock.c for lock stats tracked in backends */
+extern void pgstat_count_backend_lock_waits(uint8 locktag_type, PgStat_Counter usecs);
+extern void pgstat_count_backend_lock_fastpath_exceeded(uint8 locktag_type);
+
extern PgStat_Backend *pgstat_fetch_stat_backend(ProcNumber procNumber);
extern PgStat_Backend *pgstat_fetch_stat_backend_by_pid(int pid,
BackendType *bktype);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 3ca4f454895..b3dc3ff7d8b 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -705,7 +705,8 @@ extern void pgstat_archiver_snapshot_cb(void);
/* flags for pgstat_flush_backend() */
#define PGSTAT_BACKEND_FLUSH_IO (1 << 0) /* Flush I/O statistics */
#define PGSTAT_BACKEND_FLUSH_WAL (1 << 1) /* Flush WAL statistics */
-#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL)
+#define PGSTAT_BACKEND_FLUSH_LOCK (1 << 2) /* Flush lock statistics */
+#define PGSTAT_BACKEND_FLUSH_ALL (PGSTAT_BACKEND_FLUSH_IO | PGSTAT_BACKEND_FLUSH_WAL | PGSTAT_BACKEND_FLUSH_LOCK)
extern bool pgstat_flush_backend(bool nowait, uint32 flags);
extern bool pgstat_backend_flush_cb(bool nowait);
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index bbb1db3c433..fa550676f83 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -2019,6 +2019,10 @@ BEGIN
END;
$$;
SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset
+-- Test pg_stat_get_backend_lock()
+SELECT fastpath_exceeded AS backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation' \gset
-- Needs a lock on each partition
SELECT count(*) FROM part_test;
count
@@ -2039,5 +2043,13 @@ SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE loc
t
(1 row)
+SELECT fastpath_exceeded > :backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation';
+ ?column?
+----------
+ t
+(1 row)
+
DROP TABLE part_test;
-- End of Stats Test
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 610fd21fae4..f5683302a75 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -998,6 +998,11 @@ $$;
SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset
+-- Test pg_stat_get_backend_lock()
+SELECT fastpath_exceeded AS backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation' \gset
+
-- Needs a lock on each partition
SELECT count(*) FROM part_test;
@@ -1006,6 +1011,10 @@ SELECT pg_stat_force_next_flush();
SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation';
+SELECT fastpath_exceeded > :backend_fastpath_exceeded_before
+ FROM pg_stat_get_backend_lock(pg_backend_pid())
+ WHERE locktype = 'relation';
+
DROP TABLE part_test;
-- End of Stats Test
--
2.34.1
^ permalink raw reply [nested|flat] 12+ messages in thread
* Re: Add per-backend lock statistics
@ 2026-06-30 08:07 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 0 replies; 12+ messages in thread
From: Michael Paquier @ 2026-06-30 08:07 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Rui Zhao <zhaorui126@gmail.com>; Tatsuya Kawata <kawatatatsuya0913@gmail.com>; pgsql-hackers@lists.postgresql.org
On Tue, Jun 30, 2026 at 05:22:14AM +0000, Bertrand Drouvot wrote:
> Yeah, done in the attached.
v20 is open, so done this one.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../akN5WzUmPhk9lgVY@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 12+ messages in thread
end of thread, other threads:[~2026-06-30 08:07 UTC | newest]
Thread overview: 12+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-06-03 13:58 Add per-backend lock statistics Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-06-04 21:32 ` Tristan Partin <tristan@partin.io>
2026-06-05 08:29 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-06-05 16:02 ` Tristan Partin <tristan@partin.io>
2026-06-24 05:57 ` Michael Paquier <michael@paquier.xyz>
2026-06-24 07:21 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-06-24 13:49 ` Tatsuya Kawata <kawatatatsuya0913@gmail.com>
2026-06-24 15:50 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-06-25 15:40 ` Rui Zhao <zhaorui126@gmail.com>
2026-06-27 10:55 ` Tatsuya Kawata <kawatatatsuya0913@gmail.com>
2026-06-30 05:22 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-06-30 08:07 ` Michael Paquier <michael@paquier.xyz>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox