agora inbox for pgsql-hackers@postgresql.org  
help / color / mirror / Atom feed
Track skipped tables during autovacuum and autoanalyze
45+ messages / 6 participants
[nested] [flat]

* Track skipped tables during autovacuum and autoanalyze
@ 2026-03-24 06:11  Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-03-24 06:11 UTC (permalink / raw)
  To: pgsql-hackers

Hi,

I would like to propose adding new fields to pg_stat_all_tables to track
tables skipped during autovacuum and autoanalyze.

Currently, when autovacuum or autoanalyze is skipped because a lock cannot
be acquired, this information is only emitted in log messages. However, it
would be more useful if users could access this information via a system view,
alongside related fields such as last_autovacuum, on a per-table basis.

The attached patch add the following fields to pg_stat_all_tables:
 - last_skipped_autovacuum
 - last_skipped_autoanalyze
 - skipped_autovacuum_count
 - skipped_autoanalyze_count

Are there any concerns about exposing this in pg_stat_all_tables, or suggestions
for a better approach?

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>

Attachments:

  [text/x-diff] 0001-Track-skipped-tables-during-autovacuum-and-autoanaly.patch (11.4K, ../../20260324151133.7940a5c1f2ebd594d54da481@sraoss.co.jp/2-0001-Track-skipped-tables-during-autovacuum-and-autoanaly.patch)
  download | inline diff:
From e09abee64334a061716d5e8cb698e18a7db16ade Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH] Track skipped tables during autovacuum and autoanalyze

---
 src/backend/catalog/system_views.sql         |  4 ++
 src/backend/commands/vacuum.c                | 10 +++
 src/backend/utils/activity/pgstat_relation.c | 66 ++++++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 12 ++++
 src/include/catalog/pg_proc.dat              | 16 +++++
 src/include/pgstat.h                         |  8 +++
 src/test/regress/expected/rules.out          | 12 ++++
 7 files changed, 128 insertions(+)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f1ed7b58f13..914ba762b68 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -737,12 +737,16 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..5a0b5db39ad 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -829,10 +829,15 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	if ((options & VACOPT_VACUUM) != 0)
 	{
 		if (!rel_lock)
+		{
 			ereport(elevel,
 					(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 					 errmsg("skipping vacuum of \"%s\" --- lock not available",
 							relation->relname)));
+
+			if (AmAutoVacuumWorkerProcess())
+				pgstat_report_skipped_vacuum(relid);
+		}
 		else
 			ereport(elevel,
 					(errcode(ERRCODE_UNDEFINED_TABLE),
@@ -850,10 +855,15 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	if ((options & VACOPT_ANALYZE) != 0)
 	{
 		if (!rel_lock)
+		{
 			ereport(elevel,
 					(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 					 errmsg("skipping analyze of \"%s\" --- lock not available",
 							relation->relname)));
+
+			if (AmAutoVacuumWorkerProcess())
+				pgstat_report_skipped_analyze(relid);
+		}
 		else
 			ereport(elevel,
 					(errcode(ERRCODE_UNDEFINED_TABLE),
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..1329f7d9bf1 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -271,6 +271,72 @@ pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during autovacuum.
+ */
+void
+pgstat_report_skipped_vacuum(Oid relid)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+
+	Assert(AmAutoVacuumWorkerProcess());
+
+	if (!pgstat_track_counts)
+		return;
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+
+	tabentry->last_skipped_autovacuum_time = ts;
+	tabentry->skipped_autovacuum_count++;
+
+	pgstat_unlock_entry(entry_ref);
+}
+
+/*
+ * Report that the table was skipped during autoanalyze.
+ */
+void
+pgstat_report_skipped_analyze(Oid relid)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+
+	Assert(AmAutoVacuumWorkerProcess());
+
+	if (!pgstat_track_counts)
+		return;
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION, MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+
+	tabentry->last_skipped_autoanalyze_time = ts;
+	tabentry->skipped_autoanalyze_count++;
+
+	pgstat_unlock_entry(entry_ref);
+}
+
 /*
  * Report that the table was just analyzed and flush IO statistics.
  *
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 5f907335990..ae8e8fd90f6 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,12 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +176,12 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 84e7adde0e5..3ca1d6762f1 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,22 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
+{ oid => '8145', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 216b93492ba..bd6be54dfb3 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -460,6 +460,11 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -679,6 +684,9 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+extern void pgstat_report_skipped_vacuum(Oid relid);
+extern void pgstat_report_skipped_analyze(Oid relid);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 32bea58db2c..1ca129ad2a4 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1836,12 +1836,16 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2288,12 +2292,16 @@ pg_stat_sys_tables| SELECT relid,
     n_ins_since_vacuum,
     last_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2343,12 +2351,16 @@ pg_stat_user_tables| SELECT relid,
     n_ins_since_vacuum,
     last_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-24 14:58  Sami Imseih <samimseih@gmail.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Sami Imseih @ 2026-03-24 14:58 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: pgsql-hackers

Hi,

Thanks for the patch!

> The attached patch add the following fields to pg_stat_all_tables:
>  - last_skipped_autovacuum
>  - last_skipped_autoanalyze
>  - skipped_autovacuum_count
>  - skipped_autoanalyze_count
>
> Are there any concerns about exposing this in pg_stat_all_tables, or suggestions
> for a better approach?

I am not sure about the timestamp columns. I am not saying they will
not be useful,
but I think it will be better to just start with counters for this.
The way the views get
used, a dashboard built for tracking the deltas of the counters can easily spot
when there is a spike of skipped autovacuum/autoanalyze count.
Also, for tables that are being autovacuumed and skipped quickly,
the timestamps will just be overwritten.

So, I am +1 on the counters, -1 on the timestamps.

Out of scope for this patch, but I also wonder if we should add another counter,
autovacuum_started_count. If there are other types of failure scenarios such as
corrupt indexes, checksum failures, etc. which terminate the
autovacuum in flight,
we would be able to catch this by looking at the number of autovacuums
started vs completed. The skipped counters in this patch and a started
counter would capture different stages of the autovacuum lifecycle;
skipped means
"never started" (lock contention), while a started-minus-completed delta means
"started but failed." Both are useful signals, but for different reasons.

In terms of the patch:

1/

+                       if (AmAutoVacuumWorkerProcess())
+                               pgstat_report_skipped_vacuum(relid);

Any reason why this should not also include manual vacuum/analyze?
If someone has a vacuum/analyze script that uses SKIP_LOCKED, and
the operation gets skipped, this should be included in the counter.
this can be done with separate counter fields for autovacuum/autoanalyze and
vacuum/analyze

2/

+            pg_stat_get_skipped_autovacuum_count(C.oid) AS
skipped_autovacuum_count,

How about a name like "autovacuum_lock_skip_count"?


--
Sami Imseih
Amazon Web Services (AWS)





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-24 16:28  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Sami Imseih <samimseih@gmail.com>
  0 siblings, 2 replies; 45+ messages in thread

From: Yugo Nagata @ 2026-03-24 16:28 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: pgsql-hackers

On Tue, 24 Mar 2026 09:58:48 -0500
Sami Imseih <samimseih@gmail.com> wrote:

Thank you for your comments!

> > The attached patch add the following fields to pg_stat_all_tables:
> >  - last_skipped_autovacuum
> >  - last_skipped_autoanalyze
> >  - skipped_autovacuum_count
> >  - skipped_autoanalyze_count
> >
> > Are there any concerns about exposing this in pg_stat_all_tables, or suggestions
> > for a better approach?
> 
> I am not sure about the timestamp columns. I am not saying they will
> not be useful,
> but I think it will be better to just start with counters for this.
> The way the views get
> used, a dashboard built for tracking the deltas of the counters can easily spot
> when there is a spike of skipped autovacuum/autoanalyze count.
> Also, for tables that are being autovacuumed and skipped quickly,
> the timestamps will just be overwritten.
> 
> So, I am +1 on the counters, -1 on the timestamps.

Although the timestamps are overwritten on each skipped autovacuum or
autoanalyze, they still indicate when the last attempt was made. This
can help users confirm that autovacuum is actively attempting to run,
and that the issue is due to repeated skips rather than inactivity.

While counters can indicate overall activity, they do not reveal when
the last skip occurred. With timestamps, users can immediately see the
most recent attempt, even without a separate dashboard or historical
tracking.

Therefore, counters are useful for monitoring overall activity, but
timestamps give additional, complementary information, so it seems
worthwhile to include them too.
 
> Out of scope for this patch, but I also wonder if we should add another counter,
> autovacuum_started_count. If there are other types of failure scenarios such as
> corrupt indexes, checksum failures, etc. which terminate the
> autovacuum in flight,
> we would be able to catch this by looking at the number of autovacuums
> started vs completed. The skipped counters in this patch and a started
> counter would capture different stages of the autovacuum lifecycle;
> skipped means
> "never started" (lock contention), while a started-minus-completed delta means
> "started but failed." Both are useful signals, but for different reasons.

That makes sense. I was considering adding a counter to track
"canceled" autovacuum or autoanalyze, but tracking the number of started
or attempted autovacuums might provide a more general information than counting
only the failed ones.

> In terms of the patch:
> 
> 1/
> 
> +                       if (AmAutoVacuumWorkerProcess())
> +                               pgstat_report_skipped_vacuum(relid);
> 
> Any reason why this should not also include manual vacuum/analyze?
> If someone has a vacuum/analyze script that uses SKIP_LOCKED, and
> the operation gets skipped, this should be included in the counter.
> this can be done with separate counter fields for autovacuum/autoanalyze and
> vacuum/analyze

For manual vacuum/analyze, an explicit WARNING is output when the
operation is skipped, so I initially thought that reporting it in the
stats view was not necessary. However, I now agree that it should be
included.

> 2/
> 
> +            pg_stat_get_skipped_autovacuum_count(C.oid) AS
> skipped_autovacuum_count,
> 
> How about a name like "autovacuum_lock_skip_count"?

I'm not sure this is a good fit, since it may be better to keep the
naming consistent with other fields whose names end with
"...vacuum_count" or "...analyze_count". Happy to hear other opinions.

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-25 02:07  Michael Paquier <michael@paquier.xyz>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  1 sibling, 1 reply; 45+ messages in thread

From: Michael Paquier @ 2026-03-25 02:07 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Sami Imseih <samimseih@gmail.com>; pgsql-hackers

On Wed, Mar 25, 2026 at 01:28:47AM +0900, Yugo Nagata wrote:
> Although the timestamps are overwritten on each skipped autovacuum or
> autoanalyze, they still indicate when the last attempt was made. This
> can help users confirm that autovacuum is actively attempting to run,
> and that the issue is due to repeated skips rather than inactivity.
> 
> While counters can indicate overall activity, they do not reveal when
> the last skip occurred. With timestamps, users can immediately see the
> most recent attempt, even without a separate dashboard or historical
> tracking.
> 
> Therefore, counters are useful for monitoring overall activity, but
> timestamps give additional, complementary information, so it seems
> worthwhile to include them too.

Hmm..  I can buy this argument for the timestamps, especially for
database with many relations of various sizes that could take a
various amount of time to process.  The timestamps could offer hints
about the time it takes between the skips, even if snapshots of the
stats data are not taken at a very aggressive frequency.

This is v20 material at this stage, of course..
--
Michael

Attachments:

  [application/pgp-signature] signature.asc (832B, ../../acNDTM20nJjDYZxq@paquier.xyz/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-25 04:24  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Michael Paquier <michael@paquier.xyz>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-03-25 04:24 UTC (permalink / raw)
  To: Michael Paquier <michael@paquier.xyz>; +Cc: Sami Imseih <samimseih@gmail.com>; pgsql-hackers

On Wed, 25 Mar 2026 11:07:08 +0900
Michael Paquier <michael@paquier.xyz> wrote:

> On Wed, Mar 25, 2026 at 01:28:47AM +0900, Yugo Nagata wrote:
> > Although the timestamps are overwritten on each skipped autovacuum or
> > autoanalyze, they still indicate when the last attempt was made. This
> > can help users confirm that autovacuum is actively attempting to run,
> > and that the issue is due to repeated skips rather than inactivity.
> > 
> > While counters can indicate overall activity, they do not reveal when
> > the last skip occurred. With timestamps, users can immediately see the
> > most recent attempt, even without a separate dashboard or historical
> > tracking.
> > 
> > Therefore, counters are useful for monitoring overall activity, but
> > timestamps give additional, complementary information, so it seems
> > worthwhile to include them too.
> 
> Hmm..  I can buy this argument for the timestamps, especially for
> database with many relations of various sizes that could take a
> various amount of time to process.  The timestamps could offer hints
> about the time it takes between the skips, even if snapshots of the
> stats data are not taken at a very aggressive frequency.
> 
> This is v20 material at this stage, of course..

Thank you for your comments.
Yes, I understand that it's too late for this to be included in v19.

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-25 17:12  Sami Imseih <samimseih@gmail.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Sami Imseih @ 2026-03-25 17:12 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers

> > On Wed, Mar 25, 2026 at 01:28:47AM +0900, Yugo Nagata wrote:
> > > Although the timestamps are overwritten on each skipped autovacuum or
> > > autoanalyze, they still indicate when the last attempt was made. This
> > > can help users confirm that autovacuum is actively attempting to run,
> > > and that the issue is due to repeated skips rather than inactivity.
> > >
> > > While counters can indicate overall activity, they do not reveal when
> > > the last skip occurred. With timestamps, users can immediately see the
> > > most recent attempt, even without a separate dashboard or historical
> > > tracking.
> > >
> > > Therefore, counters are useful for monitoring overall activity, but
> > > timestamps give additional, complementary information, so it seems
> > > worthwhile to include them too.
> >
> > Hmm..  I can buy this argument for the timestamps, especially for
> > database with many relations of various sizes that could take a
> > various amount of time to process.  The timestamps could offer hints
> > about the time it takes between the skips, even if snapshots of the
> > stats data are not taken at a very aggressive frequency.

I'm fine with adding timestamps, as there seem to be convincing
reasons to add them.
My other concern is bloat of the pg_stat_all_tables view. This patch
adds 4 columns, or
8 if we also include manual vacuum and analyze (which I think we should).

Given that, should we also start thinking about splitting the vacuum
activity related
columns into a dedicated view and out of pg_stat_all_tables for v20?

--
Sami Imseih
Amazon Web Services (AWS)





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-25 23:26  Michael Paquier <michael@paquier.xyz>
  parent: Sami Imseih <samimseih@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Michael Paquier @ 2026-03-25 23:26 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: Yugo Nagata <nagata@sraoss.co.jp>; pgsql-hackers

On Wed, Mar 25, 2026 at 12:12:35PM -0500, Sami Imseih wrote:
> I'm fine with adding timestamps, as there seem to be convincing
> reasons to add them.
> My other concern is bloat of the pg_stat_all_tables view. This patch
> adds 4 columns, or
> 8 if we also include manual vacuum and analyze (which I think we should).
> 
> Given that, should we also start thinking about splitting the vacuum
> activity related
> columns into a dedicated view and out of pg_stat_all_tables for v20?

PgStat_StatTabEntry is shared between indexes and tables.  A bunch of
its fields apply only to tables, not indexes (aka all the vacuum and
analyze ones).  Few fields apply only to indexes, not tables.  Not
that many are shared between both.  I would advocate for a clean split
between indexes and tables, as a start, with a new variable-sized
stats kind dedicated to indexes.
--
Michael

Attachments:

  [application/pgp-signature] signature.asc (832B, ../../acRvIt2idOKeD7wv@paquier.xyz/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-26 01:18  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Michael Paquier <michael@paquier.xyz>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-03-26 01:18 UTC (permalink / raw)
  To: Michael Paquier <michael@paquier.xyz>; +Cc: Sami Imseih <samimseih@gmail.com>; pgsql-hackers

On Thu, 26 Mar 2026 08:26:26 +0900
Michael Paquier <michael@paquier.xyz> wrote:

> On Wed, Mar 25, 2026 at 12:12:35PM -0500, Sami Imseih wrote:
> > I'm fine with adding timestamps, as there seem to be convincing
> > reasons to add them.
> > My other concern is bloat of the pg_stat_all_tables view. This patch
> > adds 4 columns, or
> > 8 if we also include manual vacuum and analyze (which I think we should).
> > 
> > Given that, should we also start thinking about splitting the vacuum
> > activity related
> > columns into a dedicated view and out of pg_stat_all_tables for v20?
> 
> PgStat_StatTabEntry is shared between indexes and tables.  A bunch of
> its fields apply only to tables, not indexes (aka all the vacuum and
> analyze ones).  Few fields apply only to indexes, not tables.  Not
> that many are shared between both.  I would advocate for a clean split
> between indexes and tables, as a start, with a new variable-sized
> stats kind dedicated to indexes.

I see that the following fields are used in pg_stat_all_indexes:

- numscans
- lastscan
- tuples_returned
- tuples_fetched
- stat_reset_time

but they are also shared with pg_stat_all_tables. Are you suggesting
splitting these shared fields from those that are specific to tables?

I'm not sure this would significantly reduce the size of
PgStat_StatTabEntry. Could you elaborate on the expected benefits?

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-26 01:31  Michael Paquier <michael@paquier.xyz>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Michael Paquier @ 2026-03-26 01:31 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Sami Imseih <samimseih@gmail.com>; pgsql-hackers

On Thu, Mar 26, 2026 at 10:18:39AM +0900, Yugo Nagata wrote:
> I'm not sure this would significantly reduce the size of
> PgStat_StatTabEntry. Could you elaborate on the expected benefits?

The point is that this reduces the shmem footprint for indexes (well,
it's also benefitial for tables, just less), on top of being cleaner
because the stats views would only need to store and query the fields
they care about for each relkind.
--
Michael

Attachments:

  [application/pgp-signature] signature.asc (832B, ../../acSMZ-0K_YUrmzQG@paquier.xyz/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-26 02:25  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Michael Paquier <michael@paquier.xyz>
  0 siblings, 0 replies; 45+ messages in thread

From: Yugo Nagata @ 2026-03-26 02:25 UTC (permalink / raw)
  To: Michael Paquier <michael@paquier.xyz>; +Cc: Sami Imseih <samimseih@gmail.com>; pgsql-hackers

On Thu, 26 Mar 2026 10:31:19 +0900
Michael Paquier <michael@paquier.xyz> wrote:

> On Thu, Mar 26, 2026 at 10:18:39AM +0900, Yugo Nagata wrote:
> > I'm not sure this would significantly reduce the size of
> > PgStat_StatTabEntry. Could you elaborate on the expected benefits?
> 
> The point is that this reduces the shmem footprint for indexes (well,
> it's also benefitial for tables, just less), on top of being cleaner
> because the stats views would only need to store and query the fields
> they care about for each relkind.

Thank you for the clarification. I think I understand your point now.

So, you mean introducing a separate stats kind for indexes, such as
PGSTAT_KIND_INDEX?

Regards,
Yugo Nagata

> --
> Michael


-- 
Yugo Nagata <nagata@sraoss.co.jp>





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-26 10:22  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  1 sibling, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-03-26 10:22 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: pgsql-hackers; Yugo Nagata <nagata@sraoss.co.jp>; Michael Paquier <michael@paquier.xyz>


> On Tue, 24 Mar 2026 09:58:48 -0500
> Sami Imseih <samimseih@gmail.com> wrote:

> > 1/
> > 
> > +                       if (AmAutoVacuumWorkerProcess())
> > +                               pgstat_report_skipped_vacuum(relid);
> > 
> > Any reason why this should not also include manual vacuum/analyze?
> > If someone has a vacuum/analyze script that uses SKIP_LOCKED, and
> > the operation gets skipped, this should be included in the counter.
> > this can be done with separate counter fields for autovacuum/autoanalyze and
> > vacuum/analyze
> 
> For manual vacuum/analyze, an explicit WARNING is output when the
> operation is skipped, so I initially thought that reporting it in the
> stats view was not necessary. However, I now agree that it should be
> included.

I've attached an updated patch to also report skipped manual
vacuum/analyze.

The pgstat reporting functions are unified into a single function,
pgstat_report_skipped_vacuum_analyze(), which handles both
auto/manual and vacuum/analyze cases. Also it is fixed to use
InvalidOid for shared relations.

To support manual vacuum/analyze, some hack were needed to
obtain the relid before the lock attempt. When SKIP_LOCKED is specified,
the relid is obtained using RangeVarGetRelid() with NoLock prior to
locking. If ConditionalLockRelationOid() then fails, the skip is
reported.

To handle the possibility that the table is dropped between
RangeVarGetRelid() and the lock attempt, SearchSysCacheExists1() is
used after acquiring the lock.

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>

Attachments:

  [text/x-diff] v2-0001-Track-skipped-tables-during-vacuum-and-analyze.patch (16.7K, ../../20260326192203.e6dbb8d80f8d27dc15ceee59@sraoss.co.jp/2-v2-0001-Track-skipped-tables-during-vacuum-and-analyze.patch)
  download | inline diff:
From 7ca89977e0dd64fba33b2a0eda26099c65cb2154 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v2] Track skipped tables during vacuum and analyze

---
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 84 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 69 ++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 ++++++
 src/include/catalog/pg_proc.dat              | 32 ++++++++
 src/include/pgstat.h                         | 11 +++
 src/test/regress/expected/rules.out          | 24 ++++++
 7 files changed, 230 insertions(+), 22 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..8125dfac137 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -794,6 +794,11 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 (options & VACOPT_VACUUM) != 0,
+											 (options & VACOPT_ANALYZE) != 0,
+											 AmAutoVacuumWorkerProcess());
 	}
 
 	/* if relation is opened, leave */
@@ -904,7 +909,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +921,65 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				pgstat_report_skipped_vacuum_analyze(relid,
+													 (options & VACOPT_VACUUM) != 0,
+													 (options & VACOPT_ANALYZE) != 0,
+													 false);
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..51d0f280ebf 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,74 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+									 bool autovacuum)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(vacuum || analyze))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (autovacuum)
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (vacuum)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (analyze)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..d7f19d91783 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -706,6 +715,8 @@ extern void pgstat_report_vacuum(Relation rel, PgStat_Counter livetuples,
 extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+												 bool autovacuum);
 
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-26 23:07  Michael Paquier <michael@paquier.xyz>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Michael Paquier @ 2026-03-26 23:07 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Sami Imseih <samimseih@gmail.com>; pgsql-hackers

On Thu, Mar 26, 2026 at 07:22:03PM +0900, Yugo Nagata wrote:
> To handle the possibility that the table is dropped between
> RangeVarGetRelid() and the lock attempt, SearchSysCacheExists1() is
> used after acquiring the lock.

(Noticed while skimming through my emails this morning..)

+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
+                                     bool autovacuum)

I'd recommend to replace this interface with three booleans with a set
of three bitwise flags.  That would be less error prone for the
callers of this function, or we could finish by aggregating counters
we don't want to.
--
Michael

Attachments:

  [application/pgp-signature] signature.asc (832B, ../../acW8MW-Gb8Aj_TH2@paquier.xyz/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-27 07:35  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Michael Paquier <michael@paquier.xyz>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-03-27 07:35 UTC (permalink / raw)
  To: Michael Paquier <michael@paquier.xyz>; +Cc: Sami Imseih <samimseih@gmail.com>; pgsql-hackers

On Fri, 27 Mar 2026 08:07:29 +0900
Michael Paquier <michael@paquier.xyz> wrote:

> On Thu, Mar 26, 2026 at 07:22:03PM +0900, Yugo Nagata wrote:
> > To handle the possibility that the table is dropped between
> > RangeVarGetRelid() and the lock attempt, SearchSysCacheExists1() is
> > used after acquiring the lock.
> 
> (Noticed while skimming through my emails this morning..)
> 
> +void
> +pgstat_report_skipped_vacuum_analyze(Oid relid, bool vacuum, bool analyze,
> +                                     bool autovacuum)
> 
> I'd recommend to replace this interface with three booleans with a set
> of three bitwise flags.  That would be less error prone for the
> callers of this function, or we could finish by aggregating counters
> we don't want to.

Thank you for the suggestion.
I've attached a revised patch reflecting this change, and it also includes
the documentation.

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>

Attachments:

  [text/x-diff] v3-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch (23.9K, ../../20260327163549.b5df519c0099970ddbb3412d@sraoss.co.jp/2-v3-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch)
  download | inline diff:
From 075315d3111b5ebf398ef4b2c98167fd2870393b Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v3] Track skipped vacuum and analyze activity per relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_skipped_vacuum
-    last_skipped_autovacuum
-    last_skipped_analyze
-    last_skipped_autoanalyze
-    skipped_vacuum_count
-    skipped_autovacuum_count
-    skipped_analyze_count
-    skipped_autoanalyze_count

These field can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                 | 80 +++++++++++++++++
 src/backend/catalog/system_views.sql         |  8 ++
 src/backend/commands/vacuum.c                | 95 +++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c | 68 ++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 24 +++++
 src/include/catalog/pg_proc.dat              | 32 +++++++
 src/include/pgstat.h                         | 16 ++++
 src/test/regress/expected/rules.out          | 24 +++++
 8 files changed, 324 insertions(+), 23 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bb75ed1069b..9ec8070b2fc 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4372,6 +4372,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4382,6 +4392,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4391,6 +4411,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4401,6 +4431,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4411,6 +4451,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4421,6 +4471,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4430,6 +4490,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4440,6 +4510,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..2ca02a0354c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index bce3a2daa24..ce83b3171be 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -792,8 +792,18 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		bits8 flags = 0;
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0)
+			flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+		if ((options & VACOPT_ANALYZE) != 0)
+			flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+		if (AmAutoVacuumWorkerProcess())
+			flags |= PGSTAT_REPORT_SKIPPED_AUTOVAC;
+
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -801,7 +811,7 @@ vacuum_open_relation(Oid relid, RangeVar *relation, bits32 options,
 		return rel;
 
 	/*
-	 * Relation could not be opened, hence generate if possible a log
+	 * Relation could not be opened hence generate if possible a log
 	 * informing on the situation.
 	 *
 	 * If the RangeVar is not defined, we do not have enough information to
@@ -904,7 +914,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -917,29 +926,69 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				bits8	flags = 0;
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				if ((options & VACOPT_VACUUM) != 0)
+					flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+				if ((options & VACOPT_ANALYZE) != 0)
+					flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+
+				pgstat_report_skipped_vacuum_analyze(relid, flags);
+
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index bc8c43b96aa..cec4c88980e 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,6 +17,7 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
@@ -366,6 +367,73 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, bits8 flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !(flags & PGSTAT_REPORT_SKIPPED_ANY))
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_SKIPPED_AUTOVAC)
+	{
+		if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+		{
+			tabentry->last_skipped_autovacuum_time = ts;
+			tabentry->skipped_autovacuum_count++;
+		}
+		if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+		{
+			tabentry->last_skipped_autoanalyze_time = ts;
+			tabentry->skipped_autoanalyze_count++;
+		}
+	}
+	else
+	{
+		if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+		{
+			tabentry->last_skipped_vacuum_time = ts;
+			tabentry->skipped_vacuum_count++;
+		}
+		if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+		{
+			tabentry->last_skipped_analyze_time = ts;
+			tabentry->skipped_analyze_count++;
+		}
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..18aaa3996e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..ccfe3d9e02a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5667,6 +5667,38 @@
   proname => 'pg_stat_get_total_autoanalyze_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_total_autoanalyze_time' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..343081a7a25 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -707,6 +716,13 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_SKIPPED_VACUUM	(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_ANALYZE	(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOVAC	(1 << 2)	/* skipped during autovacuum/autoanalyze */
+#define PGSTAT_REPORT_SKIPPED_ANY   (PGSTAT_REPORT_SKIPPED_VACUUM | PGSTAT_REPORT_SKIPPED_ANALYZE)
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, bits8 flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..4ccfc43e903 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2293,13 +2301,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2348,13 +2364,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-27 16:48  Sami Imseih <samimseih@gmail.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Sami Imseih @ 2026-03-27 16:48 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers

> I've attached a revised patch reflecting this change, and it also includes
> the documentation.

Thanks fo the update!

I have some comments:

1/
+pgstat_report_skipped_vacuum_analyze(Oid relid, bits8 flags)

using bit8 is fine here, but I would have just used int. For this
case, it's just a matter of prefernace.

2/
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_SKIPPED_VACUUM   (1 << 0)        /* vacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_ANALYZE  (1 << 1)        /* analyze is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOVAC  (1 << 2)        /* skipped
during autovacuum/autoanalyze */
+#define PGSTAT_REPORT_SKIPPED_ANY   (PGSTAT_REPORT_SKIPPED_VACUUM |
PGSTAT_REPORT_SKIPPED_ANALYZE)

can we just have 4 flags, SKIPPED_VACUUM, SKIPPED_ANALYZE,
SKIPPED_AUTOVACUUM, SKIPPED_AUTOANALYZE,
which can then remove the nested if/else and makes the mapping more obvious

+       if (flags & PGSTAT_REPORT_SKIPPED_AUTOVAC)
+       {
+               if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+               {
+                       tabentry->last_skipped_autovacuum_time = ts;
+                       tabentry->skipped_autovacuum_count++;
+               }
+               if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+               {
+                       tabentry->last_skipped_autoanalyze_time = ts;
+                       tabentry->skipped_autoanalyze_count++;
+               }
+       }
+       else
+       {
+               if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+               {
+                       tabentry->last_skipped_vacuum_time = ts;
+                       tabentry->skipped_vacuum_count++;
+               }
+               if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+               {
+                       tabentry->last_skipped_analyze_time = ts;
+                       tabentry->skipped_analyze_count++;
+               }
+       }

3/
For the sake of consistency, can we rename the fields from

skipped_vacuum_count to vacuum_skipped_count, etc. ? to be similar
to fields like vacuum_count

4/
field documentation could be a bit better to match existing phrasing

For example, the timestamp fields:

-       Last time a manual vacuum on this table was attempted but skipped due to
-       lock unavailability (not counting <command>VACUUM FULL</command>)
+       The time of the last manual vacuum on this table that was skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)

and the counter fields

-       Number of times vacuums on this table have been attempted but skipped
+       Number of times a manual vacuum on this table has been skipped

5/
Partitioned table asymmetry between vacuum_count and vacuum_skipped_count.

vacuum_count never increments on a the parenttable, because the parent is never
pocessed. On the other hand, if the manual VACUUM/ANALYZE is on the
parent table,
then we will skip all the children. So, we should still report the skip on the
parent table, but we should add a Notes section in the docs perhaps to
document this caveat?

6/
It would be nice to add a test for this, but this requires concurrency and I'm
not sure it's woth it.

Also, can you create a CF entry in
https://commitfest.postgresql.org/59/, please.

Thanks!

--
Sami Imseih
Amazon Web Services (AWS)





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-03-28 07:18  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Sami Imseih <samimseih@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-03-28 07:18 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers

On Fri, 27 Mar 2026 11:48:27 -0500
Sami Imseih <samimseih@gmail.com> wrote:

> > I've attached a revised patch reflecting this change, and it also includes
> > the documentation.
> 
> Thanks fo the update!
> 
> I have some comments:
> 
> 1/
> +pgstat_report_skipped_vacuum_analyze(Oid relid, bits8 flags)
> 
> using bit8 is fine here, but I would have just used int. For this
> case, it's just a matter of prefernace.

That makes sense, since using int for flags seems common in other
places in the code. I'm not sure how much it affects performance,
though.

> 2/
> +/* flags for pgstat_flush_backend() */
> +#define PGSTAT_REPORT_SKIPPED_VACUUM   (1 << 0)        /* vacuum is skipped */
> +#define PGSTAT_REPORT_SKIPPED_ANALYZE  (1 << 1)        /* analyze is skipped */
> +#define PGSTAT_REPORT_SKIPPED_AUTOVAC  (1 << 2)        /* skipped
> during autovacuum/autoanalyze */
> +#define PGSTAT_REPORT_SKIPPED_ANY   (PGSTAT_REPORT_SKIPPED_VACUUM |
> PGSTAT_REPORT_SKIPPED_ANALYZE)
> 
> can we just have 4 flags, SKIPPED_VACUUM, SKIPPED_ANALYZE,
> SKIPPED_AUTOVACUUM, SKIPPED_AUTOANALYZE,
> which can then remove the nested if/else and makes the mapping more obvious

I am fine with that. In that case, the nested logic would move to the
caller side.

> 3/
> For the sake of consistency, can we rename the fields from
> 
> skipped_vacuum_count to vacuum_skipped_count, etc. ? to be similar
> to fields like vacuum_count

Hmm, I think skipped_vacuum_count is more consistent with
fields like last_vacuum and total_vacuum_time, where the modifier
comes before vacuum/analyze. What do you think about that?

> 4/
> field documentation could be a bit better to match existing phrasing
> 
> For example, the timestamp fields:
> 
> -       Last time a manual vacuum on this table was attempted but skipped due to
> -       lock unavailability (not counting <command>VACUUM FULL</command>)
> +       The time of the last manual vacuum on this table that was skipped
> +       due to lock unavailability (not counting <command>VACUUM FULL</command>)

I intended to keep consistency with the existing last_vacuum:

  Last time at which this table was manually vacuumed (not counting VACUUM FULL)

although "at which" was accidentally omitted. Your suggestion seems
simpler and more natural to me. Should we prioritize that over consistency?
 
> and the counter fields
> 
> -       Number of times vacuums on this table have been attempted but skipped
> +       Number of times a manual vacuum on this table has been skipped

The "a munual" was also accidentally omitted, so I'll fix it.

> 5/
> Partitioned table asymmetry between vacuum_count and vacuum_skipped_count.
> 
> vacuum_count never increments on a the parenttable, because the parent is never
> pocessed. On the other hand, if the manual VACUUM/ANALYZE is on the
> parent table,
> then we will skip all the children. So, we should still report the skip on the
> parent table, but we should add a Notes section in the docs perhaps to
> document this caveat?

Yeah, we cannot report skips on the children when a manual
vacuum/analyze on the parent table is skipped. (It might be possible
to obtain child information with NoLock, but that would not be safe.)

Therefore, I agree that the best we can do here is to add a note to the
documentation of last_skipped_vacuum/analyze and skipped_vacuum/analyze_count.

For example:

  When a manual vacuum or analyze on a parent table in an inheritance
  or partitioning hierarchy is skipped, the statistics are recorded
  only for the parent table, not for its children.

> 6/
> It would be nice to add a test for this, but this requires concurrency and I'm
> not sure it's woth it.

I'm not sure what meaningful tests we could add for these statistics.
I couldn't find any existing tests for fields like last_vacuum.

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-04-13 08:05  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-04-13 08:05 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Sami Imseih <samimseih@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers

Hello Sami Imseih,

On Sat, 28 Mar 2026 16:18:02 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:

> On Fri, 27 Mar 2026 11:48:27 -0500
> Sami Imseih <samimseih@gmail.com> wrote:
> 
> > > I've attached a revised patch reflecting this change, and it also includes
> > > the documentation.
> > 
> > Thanks fo the update!
> > 
> > I have some comments:
> > 
> > 1/
> > +pgstat_report_skipped_vacuum_analyze(Oid relid, bits8 flags)
> > 
> > using bit8 is fine here, but I would have just used int. For this
> > case, it's just a matter of prefernace.
> 
> That makes sense, since using int for flags seems common in other
> places in the code. I'm not sure how much it affects performance,
> though.
> 
> > 2/
> > +/* flags for pgstat_flush_backend() */
> > +#define PGSTAT_REPORT_SKIPPED_VACUUM   (1 << 0)        /* vacuum is skipped */
> > +#define PGSTAT_REPORT_SKIPPED_ANALYZE  (1 << 1)        /* analyze is skipped */
> > +#define PGSTAT_REPORT_SKIPPED_AUTOVAC  (1 << 2)        /* skipped
> > during autovacuum/autoanalyze */
> > +#define PGSTAT_REPORT_SKIPPED_ANY   (PGSTAT_REPORT_SKIPPED_VACUUM |
> > PGSTAT_REPORT_SKIPPED_ANALYZE)
> > 
> > can we just have 4 flags, SKIPPED_VACUUM, SKIPPED_ANALYZE,
> > SKIPPED_AUTOVACUUM, SKIPPED_AUTOANALYZE,
> > which can then remove the nested if/else and makes the mapping more obvious
> 
> I am fine with that. In that case, the nested logic would move to the
> caller side.
> 
> > 3/
> > For the sake of consistency, can we rename the fields from
> > 
> > skipped_vacuum_count to vacuum_skipped_count, etc. ? to be similar
> > to fields like vacuum_count
> 
> Hmm, I think skipped_vacuum_count is more consistent with
> fields like last_vacuum and total_vacuum_time, where the modifier
> comes before vacuum/analyze. What do you think about that?
> 
> > 4/
> > field documentation could be a bit better to match existing phrasing
> > 
> > For example, the timestamp fields:
> > 
> > -       Last time a manual vacuum on this table was attempted but skipped due to
> > -       lock unavailability (not counting <command>VACUUM FULL</command>)
> > +       The time of the last manual vacuum on this table that was skipped
> > +       due to lock unavailability (not counting <command>VACUUM FULL</command>)
> 
> I intended to keep consistency with the existing last_vacuum:
> 
>   Last time at which this table was manually vacuumed (not counting VACUUM FULL)
> 
> although "at which" was accidentally omitted. Your suggestion seems
> simpler and more natural to me. Should we prioritize that over consistency?
>  
> > and the counter fields
> > 
> > -       Number of times vacuums on this table have been attempted but skipped
> > +       Number of times a manual vacuum on this table has been skipped
> 
> The "a munual" was also accidentally omitted, so I'll fix it.
> 
> > 5/
> > Partitioned table asymmetry between vacuum_count and vacuum_skipped_count.
> > 
> > vacuum_count never increments on a the parenttable, because the parent is never
> > pocessed. On the other hand, if the manual VACUUM/ANALYZE is on the
> > parent table,
> > then we will skip all the children. So, we should still report the skip on the
> > parent table, but we should add a Notes section in the docs perhaps to
> > document this caveat?
> 
> Yeah, we cannot report skips on the children when a manual
> vacuum/analyze on the parent table is skipped. (It might be possible
> to obtain child information with NoLock, but that would not be safe.)
> 
> Therefore, I agree that the best we can do here is to add a note to the
> documentation of last_skipped_vacuum/analyze and skipped_vacuum/analyze_count.
> 
> For example:
> 
>   When a manual vacuum or analyze on a parent table in an inheritance
>   or partitioning hierarchy is skipped, the statistics are recorded
>   only for the parent table, not for its children.
> 
> > 6/
> > It would be nice to add a test for this, but this requires concurrency and I'm
> > not sure it's woth it.
> 
> I'm not sure what meaningful tests we could add for these statistics.
> I couldn't find any existing tests for fields like last_vacuum.

I've attached a patch reflecting your comments on items 1, 2, and 5.
As for items 3, 4, and 6, I am waiting for your comments, so the patch
is left unchanged for now.

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>

Attachments:

  [text/x-diff] v4-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch (24.5K, ../../20260413170551.5ec43ba5a2c848f0d46c6a0b@sraoss.co.jp/2-v4-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch)
  download | inline diff:
From 73fcdf90af7deb9044027b6dba4a69efde499f1c Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v4] Track skipped vacuum and analyze activity per relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_skipped_vacuum
-    last_skipped_autovacuum
-    last_skipped_analyze
-    last_skipped_autoanalyze
-    skipped_vacuum_count
-    skipped_autovacuum_count
-    skipped_analyze_count
-    skipped_autoanalyze_count

These field can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                 |  88 ++++++++++++++++
 src/backend/catalog/system_views.sql         |   8 ++
 src/backend/commands/vacuum.c                | 102 ++++++++++++++-----
 src/backend/utils/activity/pgstat_relation.c |  64 ++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          |  24 +++++
 src/include/catalog/pg_proc.dat              |  32 ++++++
 src/include/pgstat.h                         |  16 +++
 src/test/regress/expected/rules.out          |  24 +++++
 8 files changed, 335 insertions(+), 23 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..a9b579d87a9 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4387,6 +4387,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4397,6 +4407,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4406,6 +4426,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4416,6 +4446,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4426,6 +4466,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4436,6 +4486,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4445,6 +4505,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4455,6 +4525,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4510,6 +4590,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 73a1c1c4670..f509fc7876b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 99d0db82ed7..97cd5ea8d10 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,25 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int flags = 0;
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -802,7 +819,7 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		return rel;
 
 	/*
-	 * Relation could not be opened, hence generate if possible a log
+	 * Relation could not be opened hence generate if possible a log
 	 * informing on the situation.
 	 *
 	 * If the RangeVar is not defined, we do not have enough information to
@@ -905,7 +922,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -918,29 +934,69 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				int	flags = 0;
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				if ((options & VACOPT_VACUUM) != 0)
+					flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+				if ((options & VACOPT_ANALYZE) != 0)
+					flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+
+				pgstat_report_skipped_vacuum_analyze(relid, flags);
+
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index b2ca28f83ba..532d9023f8c 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,14 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +369,68 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+	{
+		tabentry->last_skipped_vacuum_time = ts;
+		tabentry->skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_skipped_autovacuum_time = ts;
+		tabentry->skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+	{
+		tabentry->last_skipped_analyze_time = ts;
+		tabentry->skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_skipped_autoanalyze_time = ts;
+		tabentry->skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 1408de387ea..90a8968faa0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..32debb34863 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5680,6 +5680,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..98941f953d1 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -703,6 +712,13 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..9b2075d3373 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2346,13 +2354,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2401,13 +2417,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-04-22 12:49  Sami Imseih <samimseih@gmail.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Sami Imseih @ 2026-04-22 12:49 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers

Thanks for the updated patch!

> I've attached a patch reflecting your comments on items 1, 2, and 5.
> As for items 3, 4, and 6, I am waiting for your comments, so the patch
> is left unchanged for now.

A few more comments:

1/

+            relid = RangeVarGetRelid(vrel->relation, NoLock, false);

Should this be called with "true" as the 3rd (missing_ok) argument, otherwise
we end up with an error instead of a "--- relation no longer exists" log. right?

2/

Can the isolation tests
src/test/isolation/specs/vacuum-skip-locked.spec be updated
to check pg_stat_user_tables as well?

3/ comment fix:

This:
* Relation could not be opened hence generate if possible a log

Should be:
* Relation could not be opened, hence generate if possible a log

--
Sami Imseih
Amazon Web Services (AWS)





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-04-27 11:32  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Sami Imseih <samimseih@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-04-27 11:32 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers

On Wed, 22 Apr 2026 07:49:55 -0500
Sami Imseih <samimseih@gmail.com> wrote:

Thank you for your comments!
> 
> 1/
> 
> +            relid = RangeVarGetRelid(vrel->relation, NoLock, false);
> 
> Should this be called with "true" as the 3rd (missing_ok) argument, otherwise
> we end up with an error instead of a "--- relation no longer exists" log. right?

No, it should be false. If missing_ok is true, VACUUM (SKIP_LOCKED) on a not-existing
table would emit a "skipping vacuum of ... --- relation no longer exists" message, but
it should be "relation ... does not exist".

> 2/
> 
> Can the isolation tests
> src/test/isolation/specs/vacuum-skip-locked.spec be updated
> to check pg_stat_user_tables as well?

Yes, we can. I've attached an updated patch including that test.

While working on the test, I noticed that skipped FULL VACUUM was counted
in the previous patch, so I fixed it not to avoid counting those cases.

> 3/ comment fix:
> 
> This:
> * Relation could not be opened hence generate if possible a log
> 
> Should be:
> * Relation could not be opened, hence generate if possible a log

Fixed.

The names of the new fields are still open. The current pattern is
"last_skipped_..." and "skipped_..._count". Alternatively, we could use
"..._last_skip" and "..._skip_count", which would be consistent with
slotsync_skip_count and slosync_last_skip.

Which do you think is better?

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>

Attachments:

  [text/x-diff] v5-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch (41.8K, ../../20260427203207.32aa6ca37f2a18a05508dfda@sraoss.co.jp/2-v5-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch)
  download | inline diff:
From 5b24f232fe55c76a2064dc3c20cb9dcca5cbb998 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v5] Track skipped vacuum and analyze activity per relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_skipped_vacuum
-    last_skipped_autovacuum
-    last_skipped_analyze
-    last_skipped_autoanalyze
-    skipped_vacuum_count
-    skipped_autovacuum_count
-    skipped_analyze_count
-    skipped_autoanalyze_count

These field can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 | 100 +++++--
 src/backend/utils/activity/pgstat_relation.c  |  64 +++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 +++
 src/include/pgstat.h                          |  16 ++
 .../isolation/expected/vacuum-skip-locked.out | 260 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  41 +--
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 601 insertions(+), 56 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..a9b579d87a9 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4387,6 +4387,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4397,6 +4407,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4406,6 +4426,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4416,6 +4446,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4426,6 +4466,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4436,6 +4486,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4445,6 +4505,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4455,6 +4525,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4510,6 +4590,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 73a1c1c4670..f509fc7876b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 99d0db82ed7..df6526225e4 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,25 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int flags = 0;
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -905,7 +922,6 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		Form_pg_class classForm;
 		bool		include_children;
 		bool		is_partitioned_table;
-		int			rvr_opts;
 
 		/*
 		 * Since autovacuum workers supply OIDs when calling vacuum(), no
@@ -918,29 +934,69 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 * below, as well as find_all_inheritors's expectation that the caller
 		 * holds some lock on the starting relation.
 		 */
-		rvr_opts = (options & VACOPT_SKIP_LOCKED) ? RVR_SKIP_LOCKED : 0;
-		relid = RangeVarGetRelidExtended(vrel->relation,
-										 AccessShareLock,
-										 rvr_opts,
-										 NULL, NULL);
-
-		/*
-		 * If the lock is unavailable, emit the same log statement that
-		 * vacuum_rel() and analyze_rel() would.
-		 */
-		if (!OidIsValid(relid))
+		if (!(options & VACOPT_SKIP_LOCKED))
 		{
-			if (options & VACOPT_VACUUM)
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping vacuum of \"%s\" --- lock not available",
-								vrel->relation->relname)));
-			else
-				ereport(WARNING,
-						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-						 errmsg("skipping analyze of \"%s\" --- lock not available",
+			relid = RangeVarGetRelidExtended(vrel->relation,
+											 AccessShareLock,
+											 0, NULL, NULL);
+			if (!OidIsValid(relid))
+				return vacrels;
+		}
+		else
+		{
+			/* Get relid for reporting before taking a lock */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+			if (!ConditionalLockRelationOid(relid, AccessShareLock))
+			{
+				int	flags = 0;
+				/*
+				 * If the lock is unavailable, emit the same log statement that
+				 * vacuum_rel() and analyze_rel() would.
+				 */
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping vacuum of \"%s\" --- lock not available",
 								vrel->relation->relname)));
-			return vacrels;
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
+							 errmsg("skipping analyze of \"%s\" --- lock not available",
+									vrel->relation->relname)));
+
+				if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+					flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+				if ((options & VACOPT_ANALYZE) != 0)
+					flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+
+				pgstat_report_skipped_vacuum_analyze(relid, flags);
+
+				return vacrels;
+			}
+
+			/*
+			 * Now that we have the lock, probe to see if the relation really
+			 * exists or not.
+			 */
+			if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(relid)))
+			{
+				if (options & VACOPT_VACUUM)
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping vacuum of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+				else
+					ereport(WARNING,
+							(errcode(ERRCODE_UNDEFINED_TABLE),
+							 errmsg("skipping analyze of \"%s\" --- relation no longer exists",
+									vrel->relation->relname)));
+
+				/* Release useless lock */
+				UnlockRelationOid(relid, AccessShareLock);
+
+				return vacrels;
+			}
 		}
 
 		/*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index b2ca28f83ba..532d9023f8c 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,14 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +369,68 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;			/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+	{
+		tabentry->last_skipped_vacuum_time = ts;
+		tabentry->skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_skipped_autovacuum_time = ts;
+		tabentry->skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+	{
+		tabentry->last_skipped_analyze_time = ts;
+		tabentry->skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_skipped_autoanalyze_time = ts;
+		tabentry->skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 1408de387ea..90a8968faa0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..32debb34863 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5680,6 +5680,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..98941f953d1 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -703,6 +712,13 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..e2cff175b7c 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +34,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +59,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +83,22 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +108,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +132,22 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +157,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +181,22 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +206,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +230,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +255,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +280,22 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   0|            1|                    0
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +305,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +330,22 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           1|                   0|            1|                    0
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +355,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +379,17 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..b0da75d4b6d 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,15 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +52,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..9b2075d3373 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2346,13 +2354,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2401,13 +2417,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-05-04 20:44  Sami Imseih <samimseih@gmail.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 2 replies; 45+ messages in thread

From: Sami Imseih @ 2026-05-04 20:44 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers

Thanks for the update!

> >
> > 1/
> >
> > +            relid = RangeVarGetRelid(vrel->relation, NoLock, false);
> >
> > Should this be called with "true" as the 3rd (missing_ok) argument, otherwise
> > we end up with an error instead of a "--- relation no longer exists" log. right?
>
> No, it should be false. If missing_ok is true, VACUUM (SKIP_LOCKED) on a not-existing
> table would emit a "skipping vacuum of ... --- relation no longer exists" message, but
> it should be "relation ... does not exist".

Yeah you are right.

But, after looking more into this, I still think the
expand_vacuum_rel() changes can be
improved. The branching
-                */
-               if (!OidIsValid(relid))
+               if (!(options & VACOPT_SKIP_LOCKED))
                {
-                       if (options & VACOPT_VACUUM)
-                               ereport(WARNING,
-
(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-                                                errmsg("skipping
vacuum of \"%s\" --- lock not available",
-
vrel->relation->relname)));
-                       else
-                               ereport(WARNING,
-
(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
-                                                errmsg("skipping
analyze of \"%s\" --- lock not available",
+                       relid = RangeVarGetRelidExtended(vrel->relation,
+
                  AccessShareLock,
+
                  0, NULL, NULL);
+                       if (!OidIsValid(relid))
+                               return vacrels;
+               }
+               else
+               {
+                       /* Get relid for reporting before taking a lock */
+                       relid = RangeVarGetRelid(vrel->relation, NoLock, false);
+
+                       if (!ConditionalLockRelationOid(relid, AccessShareLock))

is not needed. We can continue just using RangeVarGetRelidExtended()
with the rvr_opts and an AccessExclusiveLock, and once we need to
report that we cannot obtain the lock, RangeVarGetRelid() can be
called at that point for the purpose of calling
pgstat_report_skipped_vacuum_analyze(). This is safer than calling
ConditionalLockRelationOid() on a relid obtained with NoLock. See
attached v6.

>> 2/
>>
>> Can the isolation tests
>> src/test/isolation/specs/vacuum-skip-locked.spec be updated
>> to check pg_stat_user_tables as well?

> Yes, we can. I've attached an updated patch including that test.

> While working on the test, I noticed that skipped FULL VACUUM was counted
> in the previous patch, so I fixed it not to avoid counting those cases.

The tests looks good to me.

> The names of the new fields are still open. The current pattern is
> "last_skipped_..." and "skipped_..._count". Alternatively, we could use
> "..._last_skip" and "..._skip_count", which would be consistent with
> slotsync_skip_count and slosync_last_skip.

> Which do you think is better?

I think last_skipped_* is better since we use last_vacuum, last_autovacuum, etc.

--
Sami

Attachments:

  [application/octet-stream] v6-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch (39.6K, ../../CAA5RZ0vwgXZ5kF2GvYBR+Ma1LPSbDjE9pjANzSiUw3wmpv51PQ@mail.gmail.com/2-v6-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch)
  download | inline diff:
From 13e2d6b8390ea48cb4fd3e604a46bde02b750bde Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v6 1/1] Track skipped vacuum and analyze activity per relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_skipped_vacuum
-    last_skipped_autovacuum
-    last_skipped_analyze
-    last_skipped_autoanalyze
-    skipped_vacuum_count
-    skipped_autovacuum_count
-    skipped_analyze_count
-    skipped_autoanalyze_count

These field can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  31 +++
 src/backend/utils/activity/pgstat_relation.c  |  64 +++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 +++
 src/include/pgstat.h                          |  17 ++
 .../isolation/expected/vacuum-skip-locked.out | 260 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  41 +--
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 555 insertions(+), 34 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..a9b579d87a9 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4387,6 +4387,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4397,6 +4407,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4406,6 +4426,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4416,6 +4446,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4426,6 +4466,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4436,6 +4486,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4445,6 +4505,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4455,6 +4525,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4510,6 +4590,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 73a1c1c4670..f509fc7876b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 99d0db82ed7..51c5fc2fdaf 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,26 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int			flags = 0;
+
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -930,6 +948,8 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 */
 		if (!OidIsValid(relid))
 		{
+			int			flags = 0;
+
 			if (options & VACOPT_VACUUM)
 				ereport(WARNING,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
@@ -940,6 +960,17 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/* Get relid for statistics reporting */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+			if ((options & VACOPT_ANALYZE) != 0)
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+
+			pgstat_report_skipped_vacuum_analyze(relid, flags);
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index b2ca28f83ba..21d1b382ba0 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,14 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +369,68 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+	{
+		tabentry->last_skipped_vacuum_time = ts;
+		tabentry->skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_skipped_autovacuum_time = ts;
+		tabentry->skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+	{
+		tabentry->last_skipped_analyze_time = ts;
+		tabentry->skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_skipped_autoanalyze_time = ts;
+		tabentry->skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 7a9dfa9ba3b..aef291217b6 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fa9ae79082b..32debb34863 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5680,6 +5680,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..54b9fc60bc3 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -703,6 +712,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+														 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..e2cff175b7c 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +34,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +59,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +83,22 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +108,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +132,22 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +157,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +181,22 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +206,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +230,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +255,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +280,22 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   0|            1|                    0
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +305,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +330,22 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           1|                   0|            1|                    0
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +355,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +379,17 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..b0da75d4b6d 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,15 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +52,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..9b2075d3373 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2346,13 +2354,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2401,13 +2417,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-05-12 09:47  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Sami Imseih <samimseih@gmail.com>
  1 sibling, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-05-12 09:47 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers

On Mon, 4 May 2026 15:44:57 -0500
Sami Imseih <samimseih@gmail.com> wrote:

Thank you for your review!

> > > 1/
> > >
> > > +            relid = RangeVarGetRelid(vrel->relation, NoLock, false);
> > >
> > > Should this be called with "true" as the 3rd (missing_ok) argument, otherwise
> > > we end up with an error instead of a "--- relation no longer exists" log. right?
> >
> > No, it should be false. If missing_ok is true, VACUUM (SKIP_LOCKED) on a not-existing
> > table would emit a "skipping vacuum of ... --- relation no longer exists" message, but
> > it should be "relation ... does not exist".
> 
> Yeah you are right.
> 
> But, after looking more into this, I still think the
> expand_vacuum_rel() changes can be
> improved. The branching
> -                */
> -               if (!OidIsValid(relid))
> +               if (!(options & VACOPT_SKIP_LOCKED))
>                 {
> -                       if (options & VACOPT_VACUUM)
> -                               ereport(WARNING,
> -
> (errcode(ERRCODE_LOCK_NOT_AVAILABLE),
> -                                                errmsg("skipping
> vacuum of \"%s\" --- lock not available",
> -
> vrel->relation->relname)));
> -                       else
> -                               ereport(WARNING,
> -
> (errcode(ERRCODE_LOCK_NOT_AVAILABLE),
> -                                                errmsg("skipping
> analyze of \"%s\" --- lock not available",
> +                       relid = RangeVarGetRelidExtended(vrel->relation,
> +
>                   AccessShareLock,
> +
>                   0, NULL, NULL);
> +                       if (!OidIsValid(relid))
> +                               return vacrels;
> +               }
> +               else
> +               {
> +                       /* Get relid for reporting before taking a lock */
> +                       relid = RangeVarGetRelid(vrel->relation, NoLock, false);
> +
> +                       if (!ConditionalLockRelationOid(relid, AccessShareLock))
> 
> is not needed. We can continue just using RangeVarGetRelidExtended()
> with the rvr_opts and an AccessExclusiveLock, and once we need to
> report that we cannot obtain the lock, RangeVarGetRelid() can be
> called at that point for the purpose of calling
> pgstat_report_skipped_vacuum_analyze(). This is safer than calling
> ConditionalLockRelationOid() on a relid obtained with NoLock. See
> attached v6.

It seems good to me.

Initially, I was concerned that something might go wrong if a concurrent
session performed DROP TABLE or ALTER TABLE RENAME between RangeVarGetRelidExtended()
and RangeVarGetRelid(), but I could not find any actual issue. Even when the table
name is changed, the correct statistics entry is updated correctly.

So I'm fine with your version.

Regards,
Yugo Nagata

> >> 2/
> >>
> >> Can the isolation tests
> >> src/test/isolation/specs/vacuum-skip-locked.spec be updated
> >> to check pg_stat_user_tables as well?
> 
> > Yes, we can. I've attached an updated patch including that test.
> 
> > While working on the test, I noticed that skipped FULL VACUUM was counted
> > in the previous patch, so I fixed it not to avoid counting those cases.
> 
> The tests looks good to me.
> 
> > The names of the new fields are still open. The current pattern is
> > "last_skipped_..." and "skipped_..._count". Alternatively, we could use
> > "..._last_skip" and "..._skip_count", which would be consistent with
> > slotsync_skip_count and slosync_last_skip.
> 
> > Which do you think is better?
> 
> I think last_skipped_* is better since we use last_vacuum, last_autovacuum, etc.
> 
> --
> Sami


-- 
Yugo Nagata <nagata@sraoss.co.jp>





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-05-12 14:50  Sami Imseih <samimseih@gmail.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 0 replies; 45+ messages in thread

From: Sami Imseih @ 2026-05-12 14:50 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers

> So I'm fine with your version.

Thanks!

I marked the CF entry as RFC.

--
Sami





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-05-14 21:46  Zsolt Parragi <zsolt.parragi@percona.com>
  parent: Sami Imseih <samimseih@gmail.com>
  1 sibling, 1 reply; 45+ messages in thread

From: Zsolt Parragi @ 2026-05-14 21:46 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: Yugo Nagata <nagata@sraoss.co.jp>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers

Hello!

+ TimestampTz last_skipped_vacuum_time; /* user initiated vacuum */
+ PgStat_Counter skipped_vacuum_count;
+ TimestampTz last_skipped_autovacuum_time; /* autovacuum initiated */
+ PgStat_Counter skipped_autovacuum_count;
+ TimestampTz last_skipped_analyze_time; /* user initiated */
+ PgStat_Counter skipped_analyze_count;
+ TimestampTz last_skipped_autoanalyze_time; /* autovacuum initiated */
+ PgStat_Counter skipped_autoanalyze_count;
+

Doesn't these also require a PGSTAT_FILE_FORMAT_ID change?


There's also an asymmetric case for the skipped counters, is that intentional?

| Command                                 | `skipped_vacuum_count` |
`skipped_analyze_count` |
|-----------------------------------------|------------------------|-------------------------|
| `VACUUM (FULL, ANALYZE, SKIP_LOCKED) t` | 0                      | 1
                      |
| `VACUUM (ANALYZE, SKIP_LOCKED) t`       | 1                      | 1
                      |
| `VACUUM (FULL, SKIP_LOCKED) t`          | 0                      | 0
                      |


> Initially, I was concerned that something might go wrong if a concurrent
> session performed DROP TABLE or ALTER TABLE RENAME between RangeVarGetRelidExtended()
> and RangeVarGetRelid(), but I could not find any actual issue. Even when the table
> name is changed, the correct statistics entry is updated correctly.

A DROP TABLE can cause a missed skip in statistics, which is
reproducible with a custom injection point and tap test, see the
attached patch. The race window is quite minimal, but it exists.

Attachments:

  [application/octet-stream] 0001-DROP-TABLE-race-in-expand_vacuum_rel-skip-lock-path.patch.nocfbot (3.1K, ../../CAN4CZFO5BAr2uP=2CDDyX5ydFw8Ku=wtJ0ZtfLTKuWvXH6J2yQ@mail.gmail.com/2-0001-DROP-TABLE-race-in-expand_vacuum_rel-skip-lock-path.patch.nocfbot)
  download

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-05-15 00:59  Sami Imseih <samimseih@gmail.com>
  parent: Zsolt Parragi <zsolt.parragi@percona.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Sami Imseih @ 2026-05-15 00:59 UTC (permalink / raw)
  To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: Yugo Nagata <nagata@sraoss.co.jp>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers

> Doesn't these also require a PGSTAT_FILE_FORMAT_ID change?

right. that was missed. Fixed in the attached.

> There's also an asymmetric case for the skipped counters, is that intentional?
>
> | Command                                 | `skipped_vacuum_count` |
> `skipped_analyze_count` |
> |-----------------------------------------|------------------------|-------------------------|
> | `VACUUM (FULL, ANALYZE, SKIP_LOCKED) t` | 0                      | 1
>                       |
> | `VACUUM (ANALYZE, SKIP_LOCKED) t`       | 1                      | 1
>                       |
> | `VACUUM (FULL, SKIP_LOCKED) t`          | 0                      | 0

Yeah, this is because vacuum_count and last_vacuum also skip VACUUM FULL.
That was mentioned earlier in the thread.

> > Initially, I was concerned that something might go wrong if a concurrent
> > session performed DROP TABLE or ALTER TABLE RENAME between RangeVarGetRelidExtended()
> > and RangeVarGetRelid(), but I could not find any actual issue. Even when the table
> > name is changed, the correct statistics entry is updated correctly.
>
> A DROP TABLE can cause a missed skip in statistics, which is
> reproducible with a custom injection point and tap test, see the
> attached patch. The race window is quite minimal, but it exists.

If the table is dropped, there are no stats to update. right?

--
Sami

Attachments:

  [application/octet-stream] v7-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch (39.8K, ../../CAA5RZ0v-xJ5fdTvNOdfokbY10pjjP=x_sfQ6Td30bbooomysaw@mail.gmail.com/2-v7-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch)
  download | inline diff:
From 6f5294f1c09edf3f5cd11d9732d49ef74061f963 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v7 1/1] Track skipped vacuum and analyze activity per relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_skipped_vacuum
-    last_skipped_autovacuum
-    last_skipped_analyze
-    last_skipped_autoanalyze
-    skipped_vacuum_count
-    skipped_autovacuum_count
-    skipped_analyze_count
-    skipped_autoanalyze_count

These field can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  31 +++
 src/backend/utils/activity/pgstat_relation.c  |  64 +++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 +++
 src/include/pgstat.h                          |  19 +-
 .../isolation/expected/vacuum-skip-locked.out | 260 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  41 +--
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 556 insertions(+), 35 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..a9b579d87a9 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4387,6 +4387,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4397,6 +4407,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4406,6 +4426,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4416,6 +4446,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4426,6 +4466,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4436,6 +4486,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4445,6 +4505,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4455,6 +4525,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4510,6 +4590,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 73a1c1c4670..f509fc7876b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf64..019cfa03679 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,26 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int			flags = 0;
+
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -930,6 +948,8 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 */
 		if (!OidIsValid(relid))
 		{
+			int			flags = 0;
+
 			if (options & VACOPT_VACUUM)
 				ereport(WARNING,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
@@ -940,6 +960,17 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/* Get relid for statistics reporting */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+			if ((options & VACOPT_ANALYZE) != 0)
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+
+			pgstat_report_skipped_vacuum_analyze(relid, flags);
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index b2ca28f83ba..21d1b382ba0 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,14 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +369,68 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	/* block acquiring lock for the same reason as pgstat_report_autovac() */
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+	{
+		tabentry->last_skipped_vacuum_time = ts;
+		tabentry->skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_skipped_autovacuum_time = ts;
+		tabentry->skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+	{
+		tabentry->last_skipped_analyze_time = ts;
+		tabentry->skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_skipped_autoanalyze_time = ts;
+		tabentry->skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6f9c9c72de5..c45126f18c3 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..d72921a1626 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5681,6 +5681,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..e31871ed0cc 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
 {
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -703,6 +712,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+														 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..e2cff175b7c 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +34,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +59,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +83,22 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +108,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +132,22 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +157,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +181,22 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +206,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +230,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +255,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +280,22 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   0|            1|                    0
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +305,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +330,22 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           1|                   0|            1|                    0
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +355,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +379,17 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..b0da75d4b6d 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,15 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +52,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..9b2075d3373 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2346,13 +2354,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2401,13 +2417,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-05-15 05:03  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Sami Imseih <samimseih@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-05-15 05:03 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers

On Thu, 14 May 2026 19:59:18 -0500
Sami Imseih <samimseih@gmail.com> wrote:

> > Doesn't these also require a PGSTAT_FILE_FORMAT_ID change?
> 
> right. that was missed. Fixed in the attached.

Thank you for updating the patch.

> 
> > There's also an asymmetric case for the skipped counters, is that intentional?
> >
> > | Command                                 | `skipped_vacuum_count` |
> > `skipped_analyze_count` |
> > |-----------------------------------------|------------------------|-------------------------|
> > | `VACUUM (FULL, ANALYZE, SKIP_LOCKED) t` | 0                      | 1
> >                       |
> > | `VACUUM (ANALYZE, SKIP_LOCKED) t`       | 1                      | 1
> >                       |
> > | `VACUUM (FULL, SKIP_LOCKED) t`          | 0                      | 0
> 
> Yeah, this is because vacuum_count and last_vacuum also skip VACUUM FULL.
> That was mentioned earlier in the thread.

Right.
 
> > > Initially, I was concerned that something might go wrong if a concurrent
> > > session performed DROP TABLE or ALTER TABLE RENAME between RangeVarGetRelidExtended()
> > > and RangeVarGetRelid(), but I could not find any actual issue. Even when the table
> > > name is changed, the correct statistics entry is updated correctly.
> >
> > A DROP TABLE can cause a missed skip in statistics, which is
> > reproducible with a custom injection point and tap test, see the
> > attached patch. The race window is quite minimal, but it exists.
> 
> If the table is dropped, there are no stats to update. right?

In my analysis, even if the table is dropped or renamed just before calling RangeVarGetRelid()
(at the injection point you added), RangeVarGetRelid() still returns the table's OID. So it
seems possible that the statistics entry is updated locally, but it would be released shortly
afterward and thus eventually becomes invisible.

However, I'm not entirely sure whether this behavior is always guaranteed.
Could anyone clarify this?

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-05-15 09:02  Zsolt Parragi <zsolt.parragi@percona.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Zsolt Parragi @ 2026-05-15 09:02 UTC (permalink / raw)
  To: pgsql-hackers@lists.postgresql.org

> If the table is dropped, there are no stats to update. right?

Ops, right. I focused too much on "all warnings should be visible in
the statistic, so the sum of warnings and statistics should match",
but of course that's not the case.

> However, I'm not entirely sure whether this behavior is always guaranteed.
> Could anyone clarify this?

There's another different corner-case if I move the injection point
inside pgstat_report_skipped_vacuum_analyze, after releasing the
syscache.

If the drop happens at that point, we insert an orphaned record into
the statistics, and it will be visible with the internal functions
(e.g. pg_stat_get_skipped_autoanalyze_count).

It is still invisible in the pg_stat_all_tables view, but now that
I've looked more at the code, I think internally it will stay there
permanently, even surviving pg_stat_reset?

> RangeVarGetRelid() still returns the table's OID

Yes, I also reached the same conclusion, I started testing because I
tried to see if I could break the double relid retrieval by some
scenarios (alter rename + create, drop-create etc), but it's not
possible. The ereports can execute CHECK_FOR_INTERRUPTS, but that
never calls ProcessCatchupInterrupt, and because of that it never runs
AccceptInvalidationMessages. And that's when I noticed that the
warning isn't visible in the statistics at all, and got distracted...





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-05-15 16:12  Sami Imseih <samimseih@gmail.com>
  parent: Zsolt Parragi <zsolt.parragi@percona.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Sami Imseih @ 2026-05-15 16:12 UTC (permalink / raw)
  To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: pgsql-hackers@lists.postgresql.org

> > If the table is dropped, there are no stats to update. right?
>
> Ops, right. I focused too much on "all warnings should be visible in
> the statistic, so the sum of warnings and statistics should match",
> but of course that's not the case.
>
> > However, I'm not entirely sure whether this behavior is always guaranteed.
> > Could anyone clarify this?
>
> There's another different corner-case if I move the injection point
> inside pgstat_report_skipped_vacuum_analyze, after releasing the
> syscache.
>
> If the drop happens at that point, we insert an orphaned record into
> the statistics, and it will be visible with the internal functions
> (e.g. pg_stat_get_skipped_autoanalyze_count).
>
> It is still invisible in the pg_stat_all_tables view, but now that
> I've looked more at the code, I think internally it will stay there
> permanently, even surviving pg_stat_reset?

Yeah, you’re right. I just realized that pgstat_get_entry_ref_locked()
creates an entry if one does not already exist, which means we can
leak entries if the table is dropped concurrently. After releasing the
syscache entry, we still have the relid, but the table may be dropped
before the stats update runs. In that case, the stats update
recreates the relation entry, and that entry is then leaked
permanently.

The tricky part of this feature is that we cannot hold a lock while updating
the entry, because the whole point of the stat is to track cases where
we failed to acquire a lock.

So, it seems pgstat_init_function_usage() deals with the same pattern
and they deal with it by doing a second syscache lookup after creating
the entry, and if the relation is gone, drop the entry. We can do the same
thing here. See v8-0001.

I also attached a v8-0002 building on Zsolt's injection point test and his
earlier suggestion to "move the injection point inside
pgstat_report_skipped_vacuum_analyze, after releasing the syscache".
These are isolation tests that rely on injection points. The tests are:

1. Table Drop + rollback. Lock is skipped and table is dropped but rolled
back. The stats are updated.
2. Table Drop + commit. Lock is skipped and table is dropped and committed.
No table entry to record a stat for.

The tests use pg_stat_get_skipped_vacuum_count to verify that no orphaned
entry is left behind.

--
Sami

Attachments:

  [application/octet-stream] v8-0002-Add-injection-point-test-for-vacuum-skip_locked-s.patch (7.2K, ../../CAA5RZ0sVOTPCCbFfADO1+MTn60hmijP6QTDr0V=etQU=S5v=+g@mail.gmail.com/2-v8-0002-Add-injection-point-test-for-vacuum-skip_locked-s.patch)
  download | inline diff:
From 6bdc0246507d0e5688ae8592bd59f69eb61811b3 Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 15 May 2026 09:07:40 -0500
Subject: [PATCH v8 2/2] Add injection point test for vacuum skip_locked stats
 race

Add an isolation test exercising the race window between VACUUM
(SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops.

Two scenarios are tested:

1. Table dropped (committed) while vacuumer is blocked at the injection
   point: no orphaned stats entry is created.
2. DROP TABLE rolled back while vacuumer is blocked: skip is still
   recorded since the table and its stats entry survive.
---
 src/backend/utils/activity/pgstat_relation.c  |  2 +
 .../expected/vacuum_skip_lock_stats.out       | 91 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/vacuum_skip_lock_stats.spec         | 67 ++++++++++++++
 4 files changed, 161 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
 create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec

diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index f47c2958c1f..be6a6091460 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -21,6 +21,7 @@
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
@@ -391,6 +392,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 		return;					/* somebody deleted the rel, forget it */
 	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("expand-vacuum-rel-skip-locked", NULL);
 
 	/* Store the data in the table's hash table entry. */
 	ts = GetCurrentTimestamp();
diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..84c2180cc80
--- /dev/null
+++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
@@ -0,0 +1,91 @@
+Parsed test spec with 3 sessions
+
+starting permutation: lock vacuum unlock drop_table wakeup check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step drop_table: DROP TABLE test_skip;
+step wakeup: SELECT injection_points_wakeup('expand-vacuum-rel-skip-locked');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         0
+(1 row)
+
+step detach: SELECT injection_points_detach('expand-vacuum-rel-skip-locked');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+
+starting permutation: lock vacuum unlock rollback_drop wakeup check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step rollback_drop: 
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+
+step wakeup: SELECT injection_points_wakeup('expand-vacuum-rel-skip-locked');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         1
+(1 row)
+
+step detach: SELECT injection_points_detach('expand-vacuum-rel-skip-locked');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 59dba1cb023..48cacfb81a8 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -51,6 +51,7 @@ tests += {
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
+      'vacuum_skip_lock_stats',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
new file mode 100644
index 00000000000..c37294a3ba1
--- /dev/null
+++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
@@ -0,0 +1,67 @@
+# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting
+# and concurrent DROP TABLE.
+#
+# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped
+# statistics via pgstat_report_skipped_vacuum_analyze().  An injection
+# point after the syscache lookup but before the stats update allows us
+# to verify that a concurrent DROP does not leave orphaned stats entries.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE test_skip (id int);
+	INSERT INTO test_skip VALUES (1);
+	ANALYZE test_skip;
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE saved_oid (oid_val oid);
+	INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip';
+}
+
+teardown
+{
+	DROP TABLE IF EXISTS test_skip;
+	DROP TABLE IF EXISTS saved_oid;
+	DROP EXTENSION injection_points;
+}
+
+# s1: holds the lock so VACUUM skips the table
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+}
+step unlock	{ COMMIT; }
+
+# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip
+session s2
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('expand-vacuum-rel-skip-locked', 'wait');
+}
+step vacuum	{ VACUUM (SKIP_LOCKED) test_skip; }
+step detach	{ SELECT injection_points_detach('expand-vacuum-rel-skip-locked'); }
+
+# s3: drops table or wakes up the vacuumer
+session s3
+step drop_table	{ DROP TABLE test_skip; }
+step rollback_drop
+{
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+}
+step wakeup	{ SELECT injection_points_wakeup('expand-vacuum-rel-skip-locked'); }
+step check_stats
+{
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+}
+
+# Table dropped while vacuumer is blocked: no orphaned stats entry.
+permutation lock vacuum(wakeup) unlock drop_table wakeup check_stats detach
+
+# DROP rolled back while vacuumer is blocked: skip is still recorded.
+permutation lock vacuum(wakeup) unlock rollback_drop wakeup check_stats detach
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v8-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch (40.7K, ../../CAA5RZ0sVOTPCCbFfADO1+MTn60hmijP6QTDr0V=etQU=S5v=+g@mail.gmail.com/3-v8-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch)
  download | inline diff:
From fd771372255adb03592990c81a961b9750165a40 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v8 1/2] Track skipped vacuum and analyze activity per relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_skipped_vacuum
-    last_skipped_autovacuum
-    last_skipped_analyze
-    last_skipped_autoanalyze
-    skipped_vacuum_count
-    skipped_autovacuum_count
-    skipped_analyze_count
-    skipped_autoanalyze_count

These field can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  31 +++
 src/backend/utils/activity/pgstat_relation.c  |  87 ++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 +++
 src/include/pgstat.h                          |  19 +-
 .../isolation/expected/vacuum-skip-locked.out | 260 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  41 +--
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 579 insertions(+), 35 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..a9b579d87a9 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4387,6 +4387,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4397,6 +4407,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4406,6 +4426,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4416,6 +4446,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4426,6 +4466,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4436,6 +4486,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4445,6 +4505,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4455,6 +4525,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4510,6 +4590,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 73a1c1c4670..f509fc7876b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf64..019cfa03679 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,26 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int			flags = 0;
+
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -930,6 +948,8 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 */
 		if (!OidIsValid(relid))
 		{
+			int			flags = 0;
+
 			if (options & VACOPT_VACUUM)
 				ereport(WARNING,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
@@ -940,6 +960,17 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/* Get relid for statistics reporting */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+			if ((options & VACOPT_ANALYZE) != 0)
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+
+			pgstat_report_skipped_vacuum_analyze(relid, flags);
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index b2ca28f83ba..f47c2958c1f 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +370,90 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 * This mirrors the approach used in pgstat_init_function_usage() for
+	 * functions.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_unlock_entry(entry_ref);
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+	{
+		tabentry->last_skipped_vacuum_time = ts;
+		tabentry->skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_skipped_autovacuum_time = ts;
+		tabentry->skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+	{
+		tabentry->last_skipped_analyze_time = ts;
+		tabentry->skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_skipped_autoanalyze_time = ts;
+		tabentry->skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6f9c9c72de5..c45126f18c3 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..d72921a1626 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5681,6 +5681,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..e31871ed0cc 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
 {
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -703,6 +712,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+														 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..e2cff175b7c 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +34,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +59,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +83,22 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +108,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +132,22 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +157,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +181,22 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +206,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +230,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +255,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +280,22 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   0|            1|                    0
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +305,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +330,22 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           1|                   0|            1|                    0
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +355,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +379,17 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..b0da75d4b6d 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,15 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +52,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..9b2075d3373 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2346,13 +2354,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2401,13 +2417,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-05-15 20:21  Zsolt Parragi <zsolt.parragi@percona.com>
  parent: Sami Imseih <samimseih@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Zsolt Parragi @ 2026-05-15 20:21 UTC (permalink / raw)
  To: pgsql-hackers@lists.postgresql.org

Looks good and seems to handle the previous corner case, I only have
two very minor comments:

@@ -391,6 +392,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 		return;					/* somebody deleted the rel, forget it */
 	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("expand-vacuum-rel-skip-locked", NULL);

Nitpick, but now that the injection point lives inside
pgstat_report_skipped_vacuum_analyze, it should be named accordingly.

+	 * This mirrors the approach used in pgstat_init_function_usage() for
+	 * functions.

pgstat_init_function_usage only drops the entry conditionally if it
was created immediately before, so it is not entirely the same
pattern, I'm not sure if I would say "mirrors". Maybe similar to? But
this is again just nitpick.

The flow there (pending_entry, created flag => only rechecking on
freshly created) has the advantage that it only runs
AcceptInvalidationMessages+recheck when needed, but here it would also
require an additional pgstat_lock_entry call.





^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-05-16 14:29  Sami Imseih <samimseih@gmail.com>
  parent: Zsolt Parragi <zsolt.parragi@percona.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Sami Imseih @ 2026-05-16 14:29 UTC (permalink / raw)
  To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: pgsql-hackers@lists.postgresql.org

> +       INJECTION_POINT("expand-vacuum-rel-skip-locked", NULL);

> Nitpick, but now that the injection point lives inside
> pgstat_report_skipped_vacuum_analyze, it should be named accordingly.

right. I renamed it to skipped-vacuum-analyze-before-entry-lock

> Nitpick, but now that the injection point lives inside
> pgstat_report_skipped_vacuum_analyze, it should be named accordingly.
>
> +        * This mirrors the approach used in pgstat_init_function_usage() for
> +        * functions.

Fair. I just removed this comment. On re-reading, it really did not
add more value.

Attached v9.

Thanks!

--
Sami

Attachments:

  [application/octet-stream] v9-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch (40.6K, ../../CAA5RZ0spKxhnv0vCJu6nPe7mkOqOaXh83izkuvYHHzi4emY8kA@mail.gmail.com/2-v9-0001-Track-skipped-vacuum-and-analyze-activity-per-rel.patch)
  download | inline diff:
From 7c3ce7c9c896b2712ccf8cdb000bd90a513fd7bb Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v9 1/2] Track skipped vacuum and analyze activity per relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_skipped_vacuum
-    last_skipped_autovacuum
-    last_skipped_analyze
-    last_skipped_autoanalyze
-    skipped_vacuum_count
-    skipped_autovacuum_count
-    skipped_analyze_count
-    skipped_autoanalyze_count

These field can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  31 +++
 src/backend/utils/activity/pgstat_relation.c  |  85 ++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 +++
 src/include/pgstat.h                          |  19 +-
 .../isolation/expected/vacuum-skip-locked.out | 260 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  41 +--
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 577 insertions(+), 35 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 08d5b824552..a9b579d87a9 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4387,6 +4387,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4397,6 +4407,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4406,6 +4426,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4416,6 +4446,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4426,6 +4466,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4436,6 +4486,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4445,6 +4505,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4455,6 +4525,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4510,6 +4590,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 73a1c1c4670..f509fc7876b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_skipped_vacuum_time(C.oid) as last_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_skipped_autovacuum_time(C.oid) as last_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_skipped_analyze_time(C.oid) as last_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_skipped_autoanalyze_time(C.oid) as last_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_skipped_vacuum_count(C.oid) AS skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_skipped_autovacuum_count(C.oid) AS skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_skipped_analyze_count(C.oid) AS skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_skipped_autoanalyze_count(C.oid) AS skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf64..019cfa03679 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,26 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int			flags = 0;
+
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -930,6 +948,8 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 */
 		if (!OidIsValid(relid))
 		{
+			int			flags = 0;
+
 			if (options & VACOPT_VACUUM)
 				ereport(WARNING,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
@@ -940,6 +960,17 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/* Get relid for statistics reporting */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+			if ((options & VACOPT_ANALYZE) != 0)
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+
+			pgstat_report_skipped_vacuum_analyze(relid, flags);
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index b2ca28f83ba..be9611b829b 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +370,88 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_unlock_entry(entry_ref);
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+	{
+		tabentry->last_skipped_vacuum_time = ts;
+		tabentry->skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_skipped_autovacuum_time = ts;
+		tabentry->skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+	{
+		tabentry->last_skipped_analyze_time = ts;
+		tabentry->skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_skipped_autoanalyze_time = ts;
+		tabentry->skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6f9c9c72de5..c45126f18c3 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_analyze_count)
+
+/* pg_stat_get_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autoanalyze_count)
+
+/* pg_stat_get_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_autovacuum_count)
+
+/* pg_stat_get_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_analyze_time)
+
+/* pg_stat_get_last_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_autovacuum_time)
+
+/* pg_stat_get_last_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..d72921a1626 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5681,6 +5681,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..e31871ed0cc 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
 {
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter skipped_vacuum_count;
+	TimestampTz last_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autovacuum_count;
+	TimestampTz last_skipped_analyze_time;	/* user initiated */
+	PgStat_Counter skipped_analyze_count;
+	TimestampTz last_skipped_autoanalyze_time;	/* autovacuum initiated */
+	PgStat_Counter skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -703,6 +712,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+														 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..e2cff175b7c 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +34,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +59,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +83,22 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +108,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +132,22 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +157,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +181,22 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +206,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +230,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    0
+part2  |           1|                   0|            0|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +255,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    1
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +280,22 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           0|                   0|            1|                    0
+part2  |           0|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +305,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   1|            0|                    1
+part2  |           1|                   0|            1|                    0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +330,22 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            1|                    0
+part1  |           1|                   0|            1|                    0
+part2  |           1|                   0|            1|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +355,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +379,17 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|skipped_vacuum_count|analyze_count|skipped_analyze_count
+-------+------------+--------------------+-------------+---------------------
+parted |           0|                   0|            0|                    0
+part1  |           0|                   0|            0|                    0
+part2  |           0|                   0|            0|                    0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..b0da75d4b6d 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,15 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, skipped_vacuum_count,
+		   analyze_count, skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +52,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..9b2075d3373 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_skipped_vacuum_time(c.oid) AS last_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_skipped_autovacuum_time(c.oid) AS last_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_skipped_analyze_time(c.oid) AS last_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_skipped_autoanalyze_time(c.oid) AS last_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_skipped_vacuum_count(c.oid) AS skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_skipped_autovacuum_count(c.oid) AS skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_skipped_analyze_count(c.oid) AS skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_skipped_autoanalyze_count(c.oid) AS skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2346,13 +2354,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2401,13 +2417,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_skipped_vacuum,
     last_autovacuum,
+    last_skipped_autovacuum,
     last_analyze,
+    last_skipped_analyze,
     last_autoanalyze,
+    last_skipped_autoanalyze,
     vacuum_count,
+    skipped_vacuum_count,
     autovacuum_count,
+    skipped_autovacuum_count,
     analyze_count,
+    skipped_analyze_count,
     autoanalyze_count,
+    skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v9-0002-Add-injection-point-test-for-vacuum-skip_locked-s.patch (7.3K, ../../CAA5RZ0spKxhnv0vCJu6nPe7mkOqOaXh83izkuvYHHzi4emY8kA@mail.gmail.com/3-v9-0002-Add-injection-point-test-for-vacuum-skip_locked-s.patch)
  download | inline diff:
From b555f0e6d91f67fe59a300d1ec7f3f1d74fd760f Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 15 May 2026 09:07:40 -0500
Subject: [PATCH v9 2/2] Add injection point test for vacuum skip_locked stats

Add an isolation test exercising the race window between VACUUM
(SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops.

Two scenarios are tested:

1. Table dropped (committed) while vacuumer is blocked at the injection
   point: no orphaned stats entry is created.
2. DROP TABLE rolled back while vacuumer is blocked: skip is still
   recorded since the table and its stats entry survive.
---
 src/backend/utils/activity/pgstat_relation.c  |  2 +
 .../expected/vacuum_skip_lock_stats.out       | 91 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/vacuum_skip_lock_stats.spec         | 67 ++++++++++++++
 4 files changed, 161 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
 create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec

diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index be9611b829b..3f9faf4c78b 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -21,6 +21,7 @@
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
@@ -391,6 +392,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 		return;					/* somebody deleted the rel, forget it */
 	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("skipped-vacuum-analyze-before-entry-lock", NULL);
 
 	/* Store the data in the table's hash table entry. */
 	ts = GetCurrentTimestamp();
diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..0bb44b43632
--- /dev/null
+++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
@@ -0,0 +1,91 @@
+Parsed test spec with 3 sessions
+
+starting permutation: lock vacuum unlock drop_table wakeup check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step drop_table: DROP TABLE test_skip;
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         0
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+
+starting permutation: lock vacuum unlock rollback_drop wakeup check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step rollback_drop: 
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         1
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 59dba1cb023..48cacfb81a8 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -51,6 +51,7 @@ tests += {
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
+      'vacuum_skip_lock_stats',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
new file mode 100644
index 00000000000..de81e4c67c4
--- /dev/null
+++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
@@ -0,0 +1,67 @@
+# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting
+# and concurrent DROP TABLE.
+#
+# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped
+# statistics via pgstat_report_skipped_vacuum_analyze().  An injection
+# point after the syscache lookup but before the stats update allows us
+# to verify that a concurrent DROP does not leave orphaned stats entries.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE test_skip (id int);
+	INSERT INTO test_skip VALUES (1);
+	ANALYZE test_skip;
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE saved_oid (oid_val oid);
+	INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip';
+}
+
+teardown
+{
+	DROP TABLE IF EXISTS test_skip;
+	DROP TABLE IF EXISTS saved_oid;
+	DROP EXTENSION injection_points;
+}
+
+# s1: holds the lock so VACUUM skips the table
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+}
+step unlock	{ COMMIT; }
+
+# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip
+session s2
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('skipped-vacuum-analyze-before-entry-lock', 'wait');
+}
+step vacuum	{ VACUUM (SKIP_LOCKED) test_skip; }
+step detach	{ SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock'); }
+
+# s3: drops table or wakes up the vacuumer
+session s3
+step drop_table	{ DROP TABLE test_skip; }
+step rollback_drop
+{
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+}
+step wakeup	{ SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock'); }
+step check_stats
+{
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+}
+
+# Table dropped while vacuumer is blocked: no orphaned stats entry.
+permutation lock vacuum(wakeup) unlock drop_table wakeup check_stats detach
+
+# DROP rolled back while vacuumer is blocked: skip is still recorded.
+permutation lock vacuum(wakeup) unlock rollback_drop wakeup check_stats detach
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-07-11 08:23  Shinya Kato <shinya11.kato@gmail.com>
  parent: Sami Imseih <samimseih@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Shinya Kato @ 2026-07-11 08:23 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org

Hi,

On Sat, May 16, 2026 at 11:29 PM Sami Imseih <samimseih@gmail.com> wrote:
> Attached v9.

Thanks for working on this. I read through v9 and I am +1 on the feature.

It does not build against master for me:

pgstat_relation.c: In function ‘pgstat_report_skipped_vacuum_analyze’:
pgstat_relation.c:419:17: error: too few arguments to function
‘pgstat_drop_entry’
  419 |                 pgstat_drop_entry(PGSTAT_KIND_RELATION,
      |                 ^~~~~~~~~~~~~~~~~
In file included from pgstat_relation.c:27:
../../../../src/include/utils/pgstat_internal.h:811:13: note: declared here
  811 | extern bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid,
      |             ^~~~~~~~~~~~~~~~~
make[4]: *** [<builtin>: pgstat_relation.o] Error 1

pgstat_drop_entry() gained a missing_ok argument in commit 850b9218c8e
[1], so the call in pgstat_report_skipped_vacuum_analyze() needs a
fourth argument. Passing true, the way the function-usage path in
pgstat_function.c now does, builds cleanly.

On the field names: every skip these fields count is a lock skip, but
skipped_vacuum_count, last_skipped_vacuum and friends do not say so.

In the "Add autovacuum_warning to surface concurrent vacuum
collisions" thread [2], I raised another skip worth tracking, an
autovacuum worker skipping a table because another worker is already
vacuuming it, as an early sign that autovacuum is not keeping up.
There it was suggested to expose it as a per-table counter in
pg_stat_all_tables, the same surface this patch builds. That needs its
own discussion and I am not proposing to widen this patch for it.

But since another skip reason may be added later, I think the column
name should make clear the skip is due to lock unavailability. What do
you think?

[1] https://postgr.es/c/850b9218c8e
[2] https://www.postgresql.org/message-id/CALj2ACUYYv_z3C5VkcYmyezbqxUuJ%2B0H1GeNGZqPK3WFexmTYw%40mail.g...


-- 
Best regards,
Shinya Kato
NTT OSS Center






^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-07-21 18:12  Sami Imseih <samimseih@gmail.com>
  parent: Shinya Kato <shinya11.kato@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Sami Imseih @ 2026-07-21 18:12 UTC (permalink / raw)
  To: Shinya Kato <shinya11.kato@gmail.com>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org

> Thanks for working on this. I read through v9 and I am +1 on the feature.
>
> It does not build against master for me:
>
> pgstat_relation.c: In function ‘pgstat_report_skipped_vacuum_analyze’:
> pgstat_relation.c:419:17: error: too few arguments to function
> ‘pgstat_drop_entry’
>   419 |                 pgstat_drop_entry(PGSTAT_KIND_RELATION,
>       |                 ^~~~~~~~~~~~~~~~~
> In file included from pgstat_relation.c:27:
> ../../../../src/include/utils/pgstat_internal.h:811:13: note: declared here
>   811 | extern bool pgstat_drop_entry(PgStat_Kind kind, Oid dboid, uint64 objid,
>       |             ^~~~~~~~~~~~~~~~~
> make[4]: *** [<builtin>: pgstat_relation.o] Error 1
>
> pgstat_drop_entry() gained a missing_ok argument in commit 850b9218c8e
> [1], so the call in pgstat_report_skipped_vacuum_analyze() needs a
> fourth argument. Passing true, the way the function-usage path in
> pgstat_function.c now does, builds cleanly.

Correct. This is due to 850b9218c8e4. I added 'true' to silently ignore
concurrently dropped tables.

> On the field names: every skip these fields count is a lock skip, but
> skipped_vacuum_count, last_skipped_vacuum and friends do not say so.
>
> In the "Add autovacuum_warning to surface concurrent vacuum
> collisions" thread [2], I raised another skip worth tracking, an
> autovacuum worker skipping a table because another worker is already
> vacuuming it, as an early sign that autovacuum is not keeping up.
> There it was suggested to expose it as a per-table counter in
> pg_stat_all_tables, the same surface this patch builds. That needs its
> own discussion and I am not proposing to widen this patch for it.
>
> But since another skip reason may be added later, I think the column
> name should make clear the skip is due to lock unavailability. What do
> you think?
>
> [1] https://postgr.es/c/850b9218c8e
> [2] https://www.postgresql.org/message-id/CALj2ACUYYv_z3C5VkcYmyezbqxUuJ%2B0H1GeNGZqPK3WFexmTYw%40mail.g...

I am not sure about this. I read [1], and I really don't think that
showing the message " table ... is already being vacuumed by another worker"
or tracking a counter for this case is needed, but we should keep that
discussion
in that thread.

However, I still agree with your point about making the column name more
precise, so doing something like *_lock_skipped_* is better. I don't
foresee this,
but if we do ever track another skip reason besides locking, it will need to
be a separate counter.

See v10.

[1] https://www.postgresql.org/message-id/flat/CALj2ACUYYv_z3C5VkcYmyezbqxUuJ%2B0H1GeNGZqPK3WFexmTYw%40m...

--
Sami Imseih
Amazon Web Services (AWS)

Attachments:

  [application/octet-stream] v10-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch (42.3K, ../../CAA5RZ0txJaRyRnwWQiKEsOGYxU7FhKhX49qC5hZKPU7eSQz24Q@mail.gmail.com/2-v10-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch)
  download | inline diff:
From 848d3e1065c2fb3aa1c64e80c18c4dad6c4369a5 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v10 1/1] Track skipped vacuum and analyze activity per
 relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_lock_skipped_vacuum
-    last_lock_skipped_autovacuum
-    last_lock_skipped_analyze
-    last_lock_skipped_autoanalyze
-    lock_skipped_vacuum_count
-    lock_skipped_autovacuum_count
-    lock_skipped_analyze_count
-    lock_skipped_autoanalyze_count

The field names make explicit that the skip is due to lock
unavailability, distinguishing it from other reasons a vacuum or
analyze might not run on a relation.

These fields can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  31 +++
 src/backend/utils/activity/pgstat_relation.c  |  85 ++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 +++
 src/include/pgstat.h                          |  19 +-
 .../isolation/expected/vacuum-skip-locked.out | 260 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  41 +--
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 577 insertions(+), 35 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index d1a20d001e9..2912ec975d6 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4528,6 +4528,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4538,6 +4548,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4547,6 +4567,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4557,6 +4587,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4567,6 +4607,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4577,6 +4627,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4586,6 +4646,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4596,6 +4666,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4651,6 +4731,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 090281a03dd..d5e127f432d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_lock_skipped_vacuum_time(C.oid) as last_lock_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_lock_skipped_autovacuum_time(C.oid) as last_lock_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_lock_skipped_analyze_time(C.oid) as last_lock_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_lock_skipped_autoanalyze_time(C.oid) as last_lock_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_lock_skipped_vacuum_count(C.oid) AS lock_skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_lock_skipped_autovacuum_count(C.oid) AS lock_skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_lock_skipped_analyze_count(C.oid) AS lock_skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_lock_skipped_autoanalyze_count(C.oid) AS lock_skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..1b3ad4ee082 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,26 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int			flags = 0;
+
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -930,6 +948,8 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 */
 		if (!OidIsValid(relid))
 		{
+			int			flags = 0;
+
 			if (options & VACOPT_VACUUM)
 				ereport(WARNING,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
@@ -940,6 +960,17 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/* Get relid for statistics reporting */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+			if ((options & VACOPT_ANALYZE) != 0)
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+
+			pgstat_report_skipped_vacuum_analyze(relid, flags);
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 04f2eb21d0b..c17114aa6b2 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +370,88 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_unlock_entry(entry_ref);
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid, true);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+	{
+		tabentry->last_lock_skipped_vacuum_time = ts;
+		tabentry->lock_skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_lock_skipped_autovacuum_time = ts;
+		tabentry->lock_skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+	{
+		tabentry->last_lock_skipped_analyze_time = ts;
+		tabentry->lock_skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_lock_skipped_autoanalyze_time = ts;
+		tabentry->lock_skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..6a05c3660e2 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_lock_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_analyze_count)
+
+/* pg_stat_get_lock_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autoanalyze_count)
+
+/* pg_stat_get_lock_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autovacuum_count)
+
+/* pg_stat_get_lock_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_lock_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_analyze_time)
+
+/* pg_stat_get_last_lock_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_lock_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autovacuum_time)
+
+/* pg_stat_get_last_lock_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..df56444cd98 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5678,6 +5678,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_lock_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_lock_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_lock_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_lock_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_lock_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..249db7ffd0b 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
 {
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_lock_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter lock_skipped_vacuum_count;
+	TimestampTz last_lock_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter lock_skipped_autovacuum_count;
+	TimestampTz last_lock_skipped_analyze_time; /* user initiated */
+	PgStat_Counter lock_skipped_analyze_count;
+	TimestampTz last_lock_skipped_autoanalyze_time; /* autovacuum initiated */
+	PgStat_Counter lock_skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -716,6 +725,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+														 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..73a133ad9de 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +34,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +59,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +83,22 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +108,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +132,22 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +157,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +181,22 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +206,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +230,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +255,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +280,22 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           0|                        0|            1|                         0
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +305,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +330,22 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           1|                        0|            1|                         0
+part2  |           1|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +355,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +379,17 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..5b104439404 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,15 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +52,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 6a3341356da..3fcee4dc097 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_lock_skipped_vacuum_time(c.oid) AS last_lock_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_lock_skipped_autovacuum_time(c.oid) AS last_lock_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_lock_skipped_analyze_time(c.oid) AS last_lock_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_lock_skipped_autoanalyze_time(c.oid) AS last_lock_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_lock_skipped_vacuum_count(c.oid) AS lock_skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_lock_skipped_autovacuum_count(c.oid) AS lock_skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_lock_skipped_analyze_count(c.oid) AS lock_skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_lock_skipped_autoanalyze_count(c.oid) AS lock_skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2357,13 +2365,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2412,13 +2428,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-07-21 18:51  Sami Imseih <samimseih@gmail.com>
  parent: Sami Imseih <samimseih@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Sami Imseih @ 2026-07-21 18:51 UTC (permalink / raw)
  To: Shinya Kato <shinya11.kato@gmail.com>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org

Sorry, I missed attaching the injection point test.

v11 includes it.

--
Sami

Attachments:

  [application/octet-stream] v11-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch (42.3K, ../../CAA5RZ0spPHH-SAJsdn5AB8EQcdTQ53hULMSg2e0qufh9=g-2gg@mail.gmail.com/2-v11-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch)
  download | inline diff:
From 848d3e1065c2fb3aa1c64e80c18c4dad6c4369a5 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v11 1/2] Track skipped vacuum and analyze activity per
 relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_lock_skipped_vacuum
-    last_lock_skipped_autovacuum
-    last_lock_skipped_analyze
-    last_lock_skipped_autoanalyze
-    lock_skipped_vacuum_count
-    lock_skipped_autovacuum_count
-    lock_skipped_analyze_count
-    lock_skipped_autoanalyze_count

The field names make explicit that the skip is due to lock
unavailability, distinguishing it from other reasons a vacuum or
analyze might not run on a relation.

These fields can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  31 +++
 src/backend/utils/activity/pgstat_relation.c  |  85 ++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 +++
 src/include/pgstat.h                          |  19 +-
 .../isolation/expected/vacuum-skip-locked.out | 260 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  41 +--
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 577 insertions(+), 35 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index d1a20d001e9..2912ec975d6 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4528,6 +4528,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4538,6 +4548,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4547,6 +4567,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4557,6 +4587,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4567,6 +4607,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4577,6 +4627,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4586,6 +4646,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4596,6 +4666,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4651,6 +4731,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 090281a03dd..d5e127f432d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_lock_skipped_vacuum_time(C.oid) as last_lock_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_lock_skipped_autovacuum_time(C.oid) as last_lock_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_lock_skipped_analyze_time(C.oid) as last_lock_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_lock_skipped_autoanalyze_time(C.oid) as last_lock_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_lock_skipped_vacuum_count(C.oid) AS lock_skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_lock_skipped_autovacuum_count(C.oid) AS lock_skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_lock_skipped_analyze_count(C.oid) AS lock_skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_lock_skipped_autoanalyze_count(C.oid) AS lock_skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..1b3ad4ee082 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,26 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int			flags = 0;
+
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -930,6 +948,8 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 */
 		if (!OidIsValid(relid))
 		{
+			int			flags = 0;
+
 			if (options & VACOPT_VACUUM)
 				ereport(WARNING,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
@@ -940,6 +960,17 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/* Get relid for statistics reporting */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+				flags |= PGSTAT_REPORT_SKIPPED_VACUUM;
+			if ((options & VACOPT_ANALYZE) != 0)
+				flags |= PGSTAT_REPORT_SKIPPED_ANALYZE;
+
+			pgstat_report_skipped_vacuum_analyze(relid, flags);
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 04f2eb21d0b..c17114aa6b2 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +370,88 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_unlock_entry(entry_ref);
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid, true);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_SKIPPED_VACUUM)
+	{
+		tabentry->last_lock_skipped_vacuum_time = ts;
+		tabentry->lock_skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_lock_skipped_autovacuum_time = ts;
+		tabentry->lock_skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_SKIPPED_ANALYZE)
+	{
+		tabentry->last_lock_skipped_analyze_time = ts;
+		tabentry->lock_skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_lock_skipped_autoanalyze_time = ts;
+		tabentry->lock_skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..6a05c3660e2 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_lock_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_analyze_count)
+
+/* pg_stat_get_lock_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autoanalyze_count)
+
+/* pg_stat_get_lock_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autovacuum_count)
+
+/* pg_stat_get_lock_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_lock_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_analyze_time)
+
+/* pg_stat_get_last_lock_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_lock_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autovacuum_time)
+
+/* pg_stat_get_last_lock_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..df56444cd98 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5678,6 +5678,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
+  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last skipped auto vacuum time for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last skipped analyze time for a table',
+  proname => 'pg_stat_get_last_lock_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last skipped auto analyze time for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of skipped vacuum for a table',
+  proname => 'pg_stat_get_lock_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of skipped auto vacuum for a table',
+  proname => 'pg_stat_get_lock_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of skipped analyzes for a table',
+  proname => 'pg_stat_get_lock_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of skipped auto analyzes for a table',
+  proname => 'pg_stat_get_lock_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..249db7ffd0b 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
 {
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_lock_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter lock_skipped_vacuum_count;
+	TimestampTz last_lock_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter lock_skipped_autovacuum_count;
+	TimestampTz last_lock_skipped_analyze_time; /* user initiated */
+	PgStat_Counter lock_skipped_analyze_count;
+	TimestampTz last_lock_skipped_autoanalyze_time; /* autovacuum initiated */
+	PgStat_Counter lock_skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -716,6 +725,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+														 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..73a133ad9de 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +34,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +59,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +83,22 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +108,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +132,22 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +157,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +181,22 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +206,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +230,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +255,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +280,22 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           0|                        0|            1|                         0
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +305,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +330,22 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           1|                        0|            1|                         0
+part2  |           1|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +355,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +379,17 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..5b104439404 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,15 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +52,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 6a3341356da..3fcee4dc097 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_lock_skipped_vacuum_time(c.oid) AS last_lock_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_lock_skipped_autovacuum_time(c.oid) AS last_lock_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_lock_skipped_analyze_time(c.oid) AS last_lock_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_lock_skipped_autoanalyze_time(c.oid) AS last_lock_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_lock_skipped_vacuum_count(c.oid) AS lock_skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_lock_skipped_autovacuum_count(c.oid) AS lock_skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_lock_skipped_analyze_count(c.oid) AS lock_skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_lock_skipped_autoanalyze_count(c.oid) AS lock_skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2357,13 +2365,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2412,13 +2428,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.50.1 (Apple Git-155)



  [application/octet-stream] v11-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch (7.3K, ../../CAA5RZ0spPHH-SAJsdn5AB8EQcdTQ53hULMSg2e0qufh9=g-2gg@mail.gmail.com/3-v11-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch)
  download | inline diff:
From 4413618120c04d990a0229bd7d623ac10bb2b628 Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 15 May 2026 09:07:40 -0500
Subject: [PATCH v11 2/2] Add injection point test for vacuum skip_locked stats

Add an isolation test exercising the race window between VACUUM
(SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops.

Two scenarios are tested:

1. Table dropped (committed) while vacuumer is blocked at the injection
   point: no orphaned stats entry is created.
2. DROP TABLE rolled back while vacuumer is blocked: skip is still
   recorded since the table and its stats entry survive.
---
 src/backend/utils/activity/pgstat_relation.c  |  2 +
 .../expected/vacuum_skip_lock_stats.out       | 91 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/vacuum_skip_lock_stats.spec         | 67 ++++++++++++++
 4 files changed, 161 insertions(+)
 create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
 create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec

diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index c17114aa6b2..0aa59e7cf8d 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -21,6 +21,7 @@
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
@@ -391,6 +392,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 		return;					/* somebody deleted the rel, forget it */
 	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("skipped-vacuum-analyze-before-entry-lock", NULL);
 
 	/* Store the data in the table's hash table entry. */
 	ts = GetCurrentTimestamp();
diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..4d4aeeef9fa
--- /dev/null
+++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
@@ -0,0 +1,91 @@
+Parsed test spec with 3 sessions
+
+starting permutation: lock vacuum unlock drop_table wakeup check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step drop_table: DROP TABLE test_skip;
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         0
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+
+starting permutation: lock vacuum unlock rollback_drop wakeup check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step rollback_drop: 
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         1
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 59dba1cb023..48cacfb81a8 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -51,6 +51,7 @@ tests += {
       'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
+      'vacuum_skip_lock_stats',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
new file mode 100644
index 00000000000..c7840bd915e
--- /dev/null
+++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
@@ -0,0 +1,67 @@
+# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting
+# and concurrent DROP TABLE.
+#
+# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped
+# statistics via pgstat_report_skipped_vacuum_analyze().  An injection
+# point after the syscache lookup but before the stats update allows us
+# to verify that a concurrent DROP does not leave orphaned stats entries.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE test_skip (id int);
+	INSERT INTO test_skip VALUES (1);
+	ANALYZE test_skip;
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE saved_oid (oid_val oid);
+	INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip';
+}
+
+teardown
+{
+	DROP TABLE IF EXISTS test_skip;
+	DROP TABLE IF EXISTS saved_oid;
+	DROP EXTENSION injection_points;
+}
+
+# s1: holds the lock so VACUUM skips the table
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+}
+step unlock	{ COMMIT; }
+
+# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip
+session s2
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('skipped-vacuum-analyze-before-entry-lock', 'wait');
+}
+step vacuum	{ VACUUM (SKIP_LOCKED) test_skip; }
+step detach	{ SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock'); }
+
+# s3: drops table or wakes up the vacuumer
+session s3
+step drop_table	{ DROP TABLE test_skip; }
+step rollback_drop
+{
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+}
+step wakeup	{ SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock'); }
+step check_stats
+{
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+}
+
+# Table dropped while vacuumer is blocked: no orphaned stats entry.
+permutation lock vacuum(wakeup) unlock drop_table wakeup check_stats detach
+
+# DROP rolled back while vacuumer is blocked: skip is still recorded.
+permutation lock vacuum(wakeup) unlock rollback_drop wakeup check_stats detach
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-07-22 04:20  Shinya Kato <shinya11.kato@gmail.com>
  parent: Sami Imseih <samimseih@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Shinya Kato @ 2026-07-22 04:20 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org

On Wed, Jul 22, 2026 at 3:51 AM Sami Imseih <samimseih@gmail.com> wrote:
>
> Sorry, I missed attaching the injection point test.
>
> v11 includes it.

Thanks for the updated patches! I have a few comments.

In 0001,

> +step check_stat
> +{
> + SELECT relname,
> +        vacuum_count, lock_skipped_vacuum_count,
> +        analyze_count, lock_skipped_analyze_count
> + FROM pg_stat_all_tables
> + WHERE relname IN ('parted', 'part1', 'part2');
> +}

How about adding "ORDER BY relname" here? The row order is not
guaranteed otherwise.

> +{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
> +  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',

The proname now says lock_skipped, but the descr still says just
"skipped". I think the descr should mention the lock cause as well,
and the same applies to the other seven entries.

> +#define PGSTAT_REPORT_SKIPPED_VACUUM     (1 << 0)    /* vacuum is skipped */
> +#define PGSTAT_REPORT_SKIPPED_ANALYZE        (1 << 1)    /* analyze is skipped */

The struct fields were renamed to lock_skipped_*, but these flags and
pgstat_report_skipped_vacuum_analyze() keep the generic name. Should
we rename them too, for consistency?

In 0002,

> +      'vacuum_skip_lock_stats',

This also needs to be added to the ISOLATION list in
src/test/modules/injection_points/Makefile. Without it the test never
runs in the autoconf build. I confirmed it passes once registered
there.

-- 
Best regards,
Shinya Kato
NTT OSS Center






^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-07-24 16:48  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Shinya Kato <shinya11.kato@gmail.com>
  0 siblings, 2 replies; 45+ messages in thread

From: Yugo Nagata @ 2026-07-24 16:48 UTC (permalink / raw)
  To: Shinya Kato <shinya11.kato@gmail.com>; +Cc: Sami Imseih <samimseih@gmail.com>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org

Hi,

I realized that I had been accidentally dropped from the CC list and had
missed the recent discussions. I'm sorry for my late response, and thank
you for keeping this moving.

I've attached the v12 patch, which incorporates Kato-san's feedback.

Regarding the column names, I noticed the change from skipped_* to lock_skipped_*.
While I initially preferred skipped_*, I understand Kato-san's point considering
that there could be other reasons for skipping in the future.

> In 0001,
> 
> > +step check_stat
> > +{
> > + SELECT relname,
> > +        vacuum_count, lock_skipped_vacuum_count,
> > +        analyze_count, lock_skipped_analyze_count
> > + FROM pg_stat_all_tables
> > + WHERE relname IN ('parted', 'part1', 'part2');
> > +}
> 
> How about adding "ORDER BY relname" here? The row order is not
> guaranteed otherwise.

Agreed. Fixed.

> 
> > +{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
> > +  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
> 
> The proname now says lock_skipped, but the descr still says just
> "skipped". I think the descr should mention the lock cause as well,
> and the same applies to the other seven entries.

I changed them like:

'statistics: last vacuum skip time due to lock unavailability for a table'
'statistics: number of vacuum skips due to lock unavailability for a table'

How do these sound?

> > +#define PGSTAT_REPORT_SKIPPED_VACUUM     (1 << 0)    /* vacuum is skipped */
> > +#define PGSTAT_REPORT_SKIPPED_ANALYZE        (1 << 1)    /* analyze is skipped */
> 
> The struct fields were renamed to lock_skipped_*, but these flags and
> pgstat_report_skipped_vacuum_analyze() keep the generic name. Should
> we rename them too, for consistency?

I renamed them to PGSTAT_REPORT_LOCK_SKIPPED_*.

> 
> In 0002,
> 
> > +      'vacuum_skip_lock_stats',
> 
> This also needs to be added to the ISOLATION list in
> src/test/modules/injection_points/Makefile. Without it the test never
> runs in the autoconf build. I confirmed it passes once registered
> there.

I fixed the Makefile.

Also, I added a comment explaining the RangeVarGetRelid() call with
NoLock in expand_vacuum_rel(), including why NoLock is used and why it is safe
in this case.

+                       /*
+                        * Get relid for statistics reporting.
+                        *
+                        * Since we failed to acquire the lock, use NoLock here.  Although
+                        * a concurrent DDL may have dropped or renamed the relation,
+                        * RangeVarGetRelid() with NoLock does not check for invalidation
+                        * messages.
+                        */
+                       relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>

Attachments:

  [text/x-diff] v12-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch (7.9K, ../../20260725014832.b25dcd5b1a8c44f2e8720f8d@sraoss.co.jp/2-v12-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch)
  download | inline diff:
From 30eeb47a88c7d2f8c35f45499342caab5fa29b61 Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 24 Jul 2026 22:45:53 +0900
Subject: [PATCH v12 2/2] Add injection point test for vacuum skip_locked stats

Add an isolation test exercising the race window between VACUUM
(SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops.

Two scenarios are tested:

1. Table dropped (committed) while vacuumer is blocked at the injection
   point: no orphaned stats entry is created.
2. DROP TABLE rolled back while vacuumer is blocked: skip is still
   recorded since the table and its stats entry survive.
---
 src/backend/utils/activity/pgstat_relation.c  |  2 +
 src/test/modules/injection_points/Makefile    |  3 +-
 .../expected/vacuum_skip_lock_stats.out       | 91 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/vacuum_skip_lock_stats.spec         | 67 ++++++++++++++
 5 files changed, 163 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
 create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec

diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index d8259823925..6c1e2a6c0f2 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -21,6 +21,7 @@
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
@@ -391,6 +392,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 		return;					/* somebody deleted the rel, forget it */
 	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("skipped-vacuum-analyze-before-entry-lock", NULL);
 
 	/* Store the data in the table's hash table entry. */
 	ts = GetCurrentTimestamp();
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index fac80f3a4a7..b833d9d481d 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -20,7 +20,8 @@ ISOLATION = basic \
 	    repack_toast \
 	    syscache-update-pruned \
 	    wait_cleanup \
-	    heap_lock_update
+	    heap_lock_update \
+	    vacuum_skip_lock_stats
 
 # some isolation tests require wal_level=replica
 ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf
diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..4d4aeeef9fa
--- /dev/null
+++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
@@ -0,0 +1,91 @@
+Parsed test spec with 3 sessions
+
+starting permutation: lock vacuum unlock drop_table wakeup check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step drop_table: DROP TABLE test_skip;
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         0
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+
+starting permutation: lock vacuum unlock rollback_drop wakeup check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step rollback_drop: 
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         1
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 163b6374ebc..a96753fc326 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -52,6 +52,7 @@ tests += {
       'syscache-update-pruned',
       'wait_cleanup',
       'heap_lock_update',
+      'vacuum_skip_lock_stats',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
new file mode 100644
index 00000000000..c7840bd915e
--- /dev/null
+++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
@@ -0,0 +1,67 @@
+# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting
+# and concurrent DROP TABLE.
+#
+# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped
+# statistics via pgstat_report_skipped_vacuum_analyze().  An injection
+# point after the syscache lookup but before the stats update allows us
+# to verify that a concurrent DROP does not leave orphaned stats entries.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE test_skip (id int);
+	INSERT INTO test_skip VALUES (1);
+	ANALYZE test_skip;
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE saved_oid (oid_val oid);
+	INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip';
+}
+
+teardown
+{
+	DROP TABLE IF EXISTS test_skip;
+	DROP TABLE IF EXISTS saved_oid;
+	DROP EXTENSION injection_points;
+}
+
+# s1: holds the lock so VACUUM skips the table
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+}
+step unlock	{ COMMIT; }
+
+# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip
+session s2
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('skipped-vacuum-analyze-before-entry-lock', 'wait');
+}
+step vacuum	{ VACUUM (SKIP_LOCKED) test_skip; }
+step detach	{ SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock'); }
+
+# s3: drops table or wakes up the vacuumer
+session s3
+step drop_table	{ DROP TABLE test_skip; }
+step rollback_drop
+{
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+}
+step wakeup	{ SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock'); }
+step check_stats
+{
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+}
+
+# Table dropped while vacuumer is blocked: no orphaned stats entry.
+permutation lock vacuum(wakeup) unlock drop_table wakeup check_stats detach
+
+# DROP rolled back while vacuumer is blocked: skip is still recorded.
+permutation lock vacuum(wakeup) unlock rollback_drop wakeup check_stats detach
-- 
2.43.0

  [text/x-diff] v12-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch (42.8K, ../../20260725014832.b25dcd5b1a8c44f2e8720f8d@sraoss.co.jp/3-v12-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch)
  download | inline diff:
From 93f0fadf32aaced2c8a0fb633c9fedab69db3d8b Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v12 1/2] Track skipped vacuum and analyze activity per
 relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_lock_skipped_vacuum
-    last_lock_skipped_autovacuum
-    last_lock_skipped_analyze
-    last_lock_skipped_autoanalyze
-    lock_skipped_vacuum_count
-    lock_skipped_autovacuum_count
-    lock_skipped_analyze_count
-    lock_skipped_autoanalyze_count

The field names make explicit that the skip is due to lock
unavailability, distinguishing it from other reasons a vacuum or
analyze might not run on a relation.

These fields can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  38 +++
 src/backend/utils/activity/pgstat_relation.c  |  85 ++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 +++
 src/include/pgstat.h                          |  19 +-
 .../isolation/expected/vacuum-skip-locked.out | 260 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  42 +--
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 585 insertions(+), 35 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 1ce0ef00799..e4d6dda1166 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4678,6 +4678,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4688,6 +4698,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4697,6 +4717,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4707,6 +4737,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4717,6 +4757,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4727,6 +4777,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4736,6 +4796,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4746,6 +4816,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4801,6 +4881,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 090281a03dd..d5e127f432d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_lock_skipped_vacuum_time(C.oid) as last_lock_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_lock_skipped_autovacuum_time(C.oid) as last_lock_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_lock_skipped_analyze_time(C.oid) as last_lock_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_lock_skipped_autoanalyze_time(C.oid) as last_lock_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_lock_skipped_vacuum_count(C.oid) AS lock_skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_lock_skipped_autovacuum_count(C.oid) AS lock_skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_lock_skipped_analyze_count(C.oid) AS lock_skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_lock_skipped_autoanalyze_count(C.oid) AS lock_skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..613ff24d4f5 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,26 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int			flags = 0;
+
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -930,6 +948,8 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 */
 		if (!OidIsValid(relid))
 		{
+			int			flags = 0;
+
 			if (options & VACOPT_VACUUM)
 				ereport(WARNING,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
@@ -940,6 +960,24 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/*
+			 * Get relid for statistics reporting.
+			 *
+			 * Since we failed to acquire the lock, use NoLock here.  Although
+			 * a concurrent DDL may have dropped or renamed the relation,
+			 * RangeVarGetRelid() with NoLock does not check for invalidation
+			 * messages.
+			 */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+			if ((options & VACOPT_ANALYZE) != 0)
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+
+			pgstat_report_skipped_vacuum_analyze(relid, flags);
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 04f2eb21d0b..d8259823925 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +370,88 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_unlock_entry(entry_ref);
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid, true);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_VACUUM)
+	{
+		tabentry->last_lock_skipped_vacuum_time = ts;
+		tabentry->lock_skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_lock_skipped_autovacuum_time = ts;
+		tabentry->lock_skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE)
+	{
+		tabentry->last_lock_skipped_analyze_time = ts;
+		tabentry->lock_skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_lock_skipped_autoanalyze_time = ts;
+		tabentry->lock_skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..6a05c3660e2 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_lock_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_analyze_count)
+
+/* pg_stat_get_lock_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autoanalyze_count)
+
+/* pg_stat_get_lock_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autovacuum_count)
+
+/* pg_stat_get_lock_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_lock_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_analyze_time)
+
+/* pg_stat_get_last_lock_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_lock_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autovacuum_time)
+
+/* pg_stat_get_last_lock_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..d5484465af2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5678,6 +5678,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last auto vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last auto analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of auto vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of analyzes skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of auto analyzes skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..5146d7489a7 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
 {
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_lock_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter lock_skipped_vacuum_count;
+	TimestampTz last_lock_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter lock_skipped_autovacuum_count;
+	TimestampTz last_lock_skipped_analyze_time; /* user initiated */
+	PgStat_Counter lock_skipped_analyze_count;
+	TimestampTz last_lock_skipped_autoanalyze_time; /* autovacuum initiated */
+	PgStat_Counter lock_skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -716,6 +725,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_LOCK_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+															 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..73a133ad9de 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +34,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +59,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +83,22 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +108,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +132,22 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +157,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +181,22 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +206,22 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +230,22 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +255,22 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +280,22 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           0|                        0|            1|                         0
+part2  |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +305,22 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +330,22 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            1|                         0
+part1  |           1|                        0|            1|                         0
+part2  |           1|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +355,22 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +379,17 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2');
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+parted |           0|                        0|            0|                         0
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..e6cd1cf6f4f 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,16 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +53,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 6a3341356da..3fcee4dc097 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_lock_skipped_vacuum_time(c.oid) AS last_lock_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_lock_skipped_autovacuum_time(c.oid) AS last_lock_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_lock_skipped_analyze_time(c.oid) AS last_lock_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_lock_skipped_autoanalyze_time(c.oid) AS last_lock_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_lock_skipped_vacuum_count(c.oid) AS lock_skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_lock_skipped_autovacuum_count(c.oid) AS lock_skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_lock_skipped_analyze_count(c.oid) AS lock_skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_lock_skipped_autoanalyze_count(c.oid) AS lock_skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2357,13 +2365,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2412,13 +2428,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-07-24 17:51  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  1 sibling, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-07-24 17:51 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Shinya Kato <shinya11.kato@gmail.com>; Sami Imseih <samimseih@gmail.com>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org; Michael Paquier <michael@paquier.xyz>

Hi,

On Sat, 25 Jul 2026 01:48:32 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:

> Hi,
> 
> I realized that I had been accidentally dropped from the CC list and had
> missed the recent discussions. I'm sorry for my late response, and thank
> you for keeping this moving.
> 
> I've attached the v12 patch, which incorporates Kato-san's feedback.

Sorry for the noise, but I'm resubmitting v12 to include the missing expected
files for the isolation tests.

I've also added Michael back to the CC list, as both he and I had accidentally
been dropped from the recent thread.
> 
> Regarding the column names, I noticed the change from skipped_* to lock_skipped_*.
> While I initially preferred skipped_*, I understand Kato-san's point considering
> that there could be other reasons for skipping in the future.
> 
> > In 0001,
> > 
> > > +step check_stat
> > > +{
> > > + SELECT relname,
> > > +        vacuum_count, lock_skipped_vacuum_count,
> > > +        analyze_count, lock_skipped_analyze_count
> > > + FROM pg_stat_all_tables
> > > + WHERE relname IN ('parted', 'part1', 'part2');
> > > +}
> > 
> > How about adding "ORDER BY relname" here? The row order is not
> > guaranteed otherwise.
> 
> Agreed. Fixed.
> 
> > 
> > > +{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
> > > +  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
> > 
> > The proname now says lock_skipped, but the descr still says just
> > "skipped". I think the descr should mention the lock cause as well,
> > and the same applies to the other seven entries.
> 
> I changed them like:
> 
> 'statistics: last vacuum skip time due to lock unavailability for a table'
> 'statistics: number of vacuum skips due to lock unavailability for a table'
> 
> How do these sound?
> 
> > > +#define PGSTAT_REPORT_SKIPPED_VACUUM     (1 << 0)    /* vacuum is skipped */
> > > +#define PGSTAT_REPORT_SKIPPED_ANALYZE        (1 << 1)    /* analyze is skipped */
> > 
> > The struct fields were renamed to lock_skipped_*, but these flags and
> > pgstat_report_skipped_vacuum_analyze() keep the generic name. Should
> > we rename them too, for consistency?
> 
> I renamed them to PGSTAT_REPORT_LOCK_SKIPPED_*.
> 
> > 
> > In 0002,
> > 
> > > +      'vacuum_skip_lock_stats',
> > 
> > This also needs to be added to the ISOLATION list in
> > src/test/modules/injection_points/Makefile. Without it the test never
> > runs in the autoconf build. I confirmed it passes once registered
> > there.
> 
> I fixed the Makefile.
> 
> Also, I added a comment explaining the RangeVarGetRelid() call with
> NoLock in expand_vacuum_rel(), including why NoLock is used and why it is safe
> in this case.
> 
> +                       /*
> +                        * Get relid for statistics reporting.
> +                        *
> +                        * Since we failed to acquire the lock, use NoLock here.  Although
> +                        * a concurrent DDL may have dropped or renamed the relation,
> +                        * RangeVarGetRelid() with NoLock does not check for invalidation
> +                        * messages.
> +                        */
> +                       relid = RangeVarGetRelid(vrel->relation, NoLock, true);
> +


Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>

Attachments:

  [text/x-diff] v12-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch (7.9K, ../../20260725025159.95d1b037f833139ceb574fa7@sraoss.co.jp/2-v12-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch)
  download | inline diff:
From 8a2c6488bf7995d8dbc0627302e6c29cbc74bcc6 Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 24 Jul 2026 22:45:53 +0900
Subject: [PATCH v12 2/2] Add injection point test for vacuum skip_locked stats

Add an isolation test exercising the race window between VACUUM
(SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops.

Two scenarios are tested:

1. Table dropped (committed) while vacuumer is blocked at the injection
   point: no orphaned stats entry is created.
2. DROP TABLE rolled back while vacuumer is blocked: skip is still
   recorded since the table and its stats entry survive.
---
 src/backend/utils/activity/pgstat_relation.c  |  2 +
 src/test/modules/injection_points/Makefile    |  3 +-
 .../expected/vacuum_skip_lock_stats.out       | 91 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/vacuum_skip_lock_stats.spec         | 67 ++++++++++++++
 5 files changed, 163 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
 create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec

diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index d8259823925..6c1e2a6c0f2 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -21,6 +21,7 @@
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
@@ -391,6 +392,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 		return;					/* somebody deleted the rel, forget it */
 	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("skipped-vacuum-analyze-before-entry-lock", NULL);
 
 	/* Store the data in the table's hash table entry. */
 	ts = GetCurrentTimestamp();
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index fac80f3a4a7..b833d9d481d 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -20,7 +20,8 @@ ISOLATION = basic \
 	    repack_toast \
 	    syscache-update-pruned \
 	    wait_cleanup \
-	    heap_lock_update
+	    heap_lock_update \
+	    vacuum_skip_lock_stats
 
 # some isolation tests require wal_level=replica
 ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf
diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..4d4aeeef9fa
--- /dev/null
+++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
@@ -0,0 +1,91 @@
+Parsed test spec with 3 sessions
+
+starting permutation: lock vacuum unlock drop_table wakeup check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step drop_table: DROP TABLE test_skip;
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         0
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+
+starting permutation: lock vacuum unlock rollback_drop wakeup check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step rollback_drop: 
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         1
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 163b6374ebc..a96753fc326 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -52,6 +52,7 @@ tests += {
       'syscache-update-pruned',
       'wait_cleanup',
       'heap_lock_update',
+      'vacuum_skip_lock_stats',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
new file mode 100644
index 00000000000..c7840bd915e
--- /dev/null
+++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
@@ -0,0 +1,67 @@
+# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting
+# and concurrent DROP TABLE.
+#
+# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped
+# statistics via pgstat_report_skipped_vacuum_analyze().  An injection
+# point after the syscache lookup but before the stats update allows us
+# to verify that a concurrent DROP does not leave orphaned stats entries.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE test_skip (id int);
+	INSERT INTO test_skip VALUES (1);
+	ANALYZE test_skip;
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE saved_oid (oid_val oid);
+	INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip';
+}
+
+teardown
+{
+	DROP TABLE IF EXISTS test_skip;
+	DROP TABLE IF EXISTS saved_oid;
+	DROP EXTENSION injection_points;
+}
+
+# s1: holds the lock so VACUUM skips the table
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+}
+step unlock	{ COMMIT; }
+
+# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip
+session s2
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('skipped-vacuum-analyze-before-entry-lock', 'wait');
+}
+step vacuum	{ VACUUM (SKIP_LOCKED) test_skip; }
+step detach	{ SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock'); }
+
+# s3: drops table or wakes up the vacuumer
+session s3
+step drop_table	{ DROP TABLE test_skip; }
+step rollback_drop
+{
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+}
+step wakeup	{ SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock'); }
+step check_stats
+{
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+}
+
+# Table dropped while vacuumer is blocked: no orphaned stats entry.
+permutation lock vacuum(wakeup) unlock drop_table wakeup check_stats detach
+
+# DROP rolled back while vacuumer is blocked: skip is still recorded.
+permutation lock vacuum(wakeup) unlock rollback_drop wakeup check_stats detach
-- 
2.43.0

  [text/x-diff] v12-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch (43.1K, ../../20260725025159.95d1b037f833139ceb574fa7@sraoss.co.jp/3-v12-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch)
  download | inline diff:
From 0c86c9480ff81f738505ca153449a1f9c60c8a11 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v12 1/2] Track skipped vacuum and analyze activity per
 relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_lock_skipped_vacuum
-    last_lock_skipped_autovacuum
-    last_lock_skipped_analyze
-    last_lock_skipped_autoanalyze
-    lock_skipped_vacuum_count
-    lock_skipped_autovacuum_count
-    lock_skipped_analyze_count
-    lock_skipped_autoanalyze_count

The field names make explicit that the skip is due to lock
unavailability, distinguishing it from other reasons a vacuum or
analyze might not run on a relation.

These fields can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  38 +++
 src/backend/utils/activity/pgstat_relation.c  |  85 ++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 ++
 src/include/pgstat.h                          |  19 +-
 .../isolation/expected/vacuum-skip-locked.out | 276 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  42 ++-
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 601 insertions(+), 35 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 1ce0ef00799..e4d6dda1166 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4678,6 +4678,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4688,6 +4698,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4697,6 +4717,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4707,6 +4737,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4717,6 +4757,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4727,6 +4777,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4736,6 +4796,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4746,6 +4816,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4801,6 +4881,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 090281a03dd..d5e127f432d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_lock_skipped_vacuum_time(C.oid) as last_lock_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_lock_skipped_autovacuum_time(C.oid) as last_lock_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_lock_skipped_analyze_time(C.oid) as last_lock_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_lock_skipped_autoanalyze_time(C.oid) as last_lock_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_lock_skipped_vacuum_count(C.oid) AS lock_skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_lock_skipped_autovacuum_count(C.oid) AS lock_skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_lock_skipped_analyze_count(C.oid) AS lock_skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_lock_skipped_autoanalyze_count(C.oid) AS lock_skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..613ff24d4f5 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,26 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int			flags = 0;
+
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -930,6 +948,8 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 */
 		if (!OidIsValid(relid))
 		{
+			int			flags = 0;
+
 			if (options & VACOPT_VACUUM)
 				ereport(WARNING,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
@@ -940,6 +960,24 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/*
+			 * Get relid for statistics reporting.
+			 *
+			 * Since we failed to acquire the lock, use NoLock here.  Although
+			 * a concurrent DDL may have dropped or renamed the relation,
+			 * RangeVarGetRelid() with NoLock does not check for invalidation
+			 * messages.
+			 */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+			if ((options & VACOPT_ANALYZE) != 0)
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+
+			pgstat_report_skipped_vacuum_analyze(relid, flags);
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 04f2eb21d0b..d8259823925 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +370,88 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_unlock_entry(entry_ref);
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid, true);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_VACUUM)
+	{
+		tabentry->last_lock_skipped_vacuum_time = ts;
+		tabentry->lock_skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_lock_skipped_autovacuum_time = ts;
+		tabentry->lock_skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE)
+	{
+		tabentry->last_lock_skipped_analyze_time = ts;
+		tabentry->lock_skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_lock_skipped_autoanalyze_time = ts;
+		tabentry->lock_skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..6a05c3660e2 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_lock_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_analyze_count)
+
+/* pg_stat_get_lock_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autoanalyze_count)
+
+/* pg_stat_get_lock_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autovacuum_count)
+
+/* pg_stat_get_lock_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_lock_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_analyze_time)
+
+/* pg_stat_get_last_lock_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_lock_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autovacuum_time)
+
+/* pg_stat_get_last_lock_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..d5484465af2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5678,6 +5678,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last auto vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last auto analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of auto vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of analyzes skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of auto analyzes skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..5146d7489a7 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
 {
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_lock_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter lock_skipped_vacuum_count;
+	TimestampTz last_lock_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter lock_skipped_autovacuum_count;
+	TimestampTz last_lock_skipped_analyze_time; /* user initiated */
+	PgStat_Counter lock_skipped_analyze_count;
+	TimestampTz last_lock_skipped_autoanalyze_time; /* autovacuum initiated */
+	PgStat_Counter lock_skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -716,6 +725,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_LOCK_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+															 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..b1b25036163 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +35,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +61,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +86,23 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +112,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +137,23 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +163,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +188,23 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +214,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +239,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +265,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +291,23 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            1|                         0
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +317,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +343,23 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           1|                        0|            1|                         0
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +369,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +394,18 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..e6cd1cf6f4f 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,16 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +53,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 6a3341356da..3fcee4dc097 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_lock_skipped_vacuum_time(c.oid) AS last_lock_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_lock_skipped_autovacuum_time(c.oid) AS last_lock_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_lock_skipped_analyze_time(c.oid) AS last_lock_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_lock_skipped_autoanalyze_time(c.oid) AS last_lock_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_lock_skipped_vacuum_count(c.oid) AS lock_skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_lock_skipped_autovacuum_count(c.oid) AS lock_skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_lock_skipped_analyze_count(c.oid) AS lock_skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_lock_skipped_autoanalyze_count(c.oid) AS lock_skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2357,13 +2365,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2412,13 +2428,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-07-27 04:03  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-07-27 04:03 UTC (permalink / raw)
  To: Sami Imseih <samimseih@gmail.com>; +Cc: Shinya Kato <shinya11.kato@gmail.com>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org; Michael Paquier <michael@paquier.xyz>

Hi Sami,

On Sat, 25 Jul 2026 02:51:59 +0900
Yugo Nagata <nagata@sraoss.co.jp> wrote:

> Hi,
> 
> On Sat, 25 Jul 2026 01:48:32 +0900
> Yugo Nagata <nagata@sraoss.co.jp> wrote:
> 
> > Hi,
> > 
> > I realized that I had been accidentally dropped from the CC list and had
> > missed the recent discussions. I'm sorry for my late response, and thank
> > you for keeping this moving.
> > 
> > I've attached the v12 patch, which incorporates Kato-san's feedback.
> 
> Sorry for the noise, but I'm resubmitting v12 to include the missing expected
> files for the isolation tests.
> 
> I've also added Michael back to the CC list, as both he and I had accidentally
> been dropped from the recent thread.
> > 
> > Regarding the column names, I noticed the change from skipped_* to lock_skipped_*.
> > While I initially preferred skipped_*, I understand Kato-san's point considering
> > that there could be other reasons for skipping in the future.
> > 
> > > In 0001,
> > > 
> > > > +step check_stat
> > > > +{
> > > > + SELECT relname,
> > > > +        vacuum_count, lock_skipped_vacuum_count,
> > > > +        analyze_count, lock_skipped_analyze_count
> > > > + FROM pg_stat_all_tables
> > > > + WHERE relname IN ('parted', 'part1', 'part2');
> > > > +}
> > > 
> > > How about adding "ORDER BY relname" here? The row order is not
> > > guaranteed otherwise.
> > 
> > Agreed. Fixed.
> > 
> > > 
> > > > +{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
> > > > +  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
> > > 
> > > The proname now says lock_skipped, but the descr still says just
> > > "skipped". I think the descr should mention the lock cause as well,
> > > and the same applies to the other seven entries.
> > 
> > I changed them like:
> > 
> > 'statistics: last vacuum skip time due to lock unavailability for a table'
> > 'statistics: number of vacuum skips due to lock unavailability for a table'
> > 
> > How do these sound?
> > 
> > > > +#define PGSTAT_REPORT_SKIPPED_VACUUM     (1 << 0)    /* vacuum is skipped */
> > > > +#define PGSTAT_REPORT_SKIPPED_ANALYZE        (1 << 1)    /* analyze is skipped */
> > > 
> > > The struct fields were renamed to lock_skipped_*, but these flags and
> > > pgstat_report_skipped_vacuum_analyze() keep the generic name. Should
> > > we rename them too, for consistency?
> > 
> > I renamed them to PGSTAT_REPORT_LOCK_SKIPPED_*.
> > 
> > > 
> > > In 0002,
> > > 
> > > > +      'vacuum_skip_lock_stats',
> > > 
> > > This also needs to be added to the ISOLATION list in
> > > src/test/modules/injection_points/Makefile. Without it the test never
> > > runs in the autoconf build. I confirmed it passes once registered
> > > there.
> > 
> > I fixed the Makefile.

I found athat the injection_points's isolation test could fail [1]
because the check_stats step can be launched before the vacuum finishes.

I've attached a patch to fix this by inserting "noop" step in session s2
to ensure that the vacuum has finished. 

Does this look good to you?

[1] https://github.com/postgresql-cfbot/postgresql/actions/runs/30115503531/attempts/1
> > 
> > Also, I added a comment explaining the RangeVarGetRelid() call with
> > NoLock in expand_vacuum_rel(), including why NoLock is used and why it is safe
> > in this case.
> > 
> > +                       /*
> > +                        * Get relid for statistics reporting.
> > +                        *
> > +                        * Since we failed to acquire the lock, use NoLock here.  Although
> > +                        * a concurrent DDL may have dropped or renamed the relation,
> > +                        * RangeVarGetRelid() with NoLock does not check for invalidation
> > +                        * messages.
> > +                        */
> > +                       relid = RangeVarGetRelid(vrel->relation, NoLock, true);
> > +

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>

Attachments:

  [text/x-diff] v13-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch (8.0K, ../../20260727130341.6d2791b736304c7119249326@sraoss.co.jp/2-v13-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch)
  download | inline diff:
From a11b7b50536ce5e02fa96e553986927ebb62a6fa Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 24 Jul 2026 22:45:53 +0900
Subject: [PATCH v13 2/2] Add injection point test for vacuum skip_locked stats

Add an isolation test exercising the race window between VACUUM
(SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops.

Two scenarios are tested:

1. Table dropped (committed) while vacuumer is blocked at the injection
   point: no orphaned stats entry is created.
2. DROP TABLE rolled back while vacuumer is blocked: skip is still
   recorded since the table and its stats entry survive.
---
 src/backend/utils/activity/pgstat_relation.c  |  2 +
 src/test/modules/injection_points/Makefile    |  3 +-
 .../expected/vacuum_skip_lock_stats.out       | 93 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/vacuum_skip_lock_stats.spec         | 68 ++++++++++++++
 5 files changed, 166 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
 create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec

diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index d8259823925..6c1e2a6c0f2 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -21,6 +21,7 @@
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
@@ -391,6 +392,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 		return;					/* somebody deleted the rel, forget it */
 	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("skipped-vacuum-analyze-before-entry-lock", NULL);
 
 	/* Store the data in the table's hash table entry. */
 	ts = GetCurrentTimestamp();
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index fac80f3a4a7..b833d9d481d 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -20,7 +20,8 @@ ISOLATION = basic \
 	    repack_toast \
 	    syscache-update-pruned \
 	    wait_cleanup \
-	    heap_lock_update
+	    heap_lock_update \
+	    vacuum_skip_lock_stats
 
 # some isolation tests require wal_level=replica
 ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf
diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..0efed3db2db
--- /dev/null
+++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
@@ -0,0 +1,93 @@
+Parsed test spec with 3 sessions
+
+starting permutation: lock vacuum unlock drop_table wakeup noop check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step drop_table: DROP TABLE test_skip;
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step noop: 
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         0
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+
+starting permutation: lock vacuum unlock rollback_drop wakeup noop check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step rollback_drop: 
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step noop: 
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         1
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 163b6374ebc..a96753fc326 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -52,6 +52,7 @@ tests += {
       'syscache-update-pruned',
       'wait_cleanup',
       'heap_lock_update',
+      'vacuum_skip_lock_stats',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
new file mode 100644
index 00000000000..b277b1e80f7
--- /dev/null
+++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
@@ -0,0 +1,68 @@
+# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting
+# and concurrent DROP TABLE.
+#
+# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped
+# statistics via pgstat_report_skipped_vacuum_analyze().  An injection
+# point after the syscache lookup but before the stats update allows us
+# to verify that a concurrent DROP does not leave orphaned stats entries.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE test_skip (id int);
+	INSERT INTO test_skip VALUES (1);
+	ANALYZE test_skip;
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE saved_oid (oid_val oid);
+	INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip';
+}
+
+teardown
+{
+	DROP TABLE IF EXISTS test_skip;
+	DROP TABLE IF EXISTS saved_oid;
+	DROP EXTENSION injection_points;
+}
+
+# s1: holds the lock so VACUUM skips the table
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+}
+step unlock	{ COMMIT; }
+
+# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip
+session s2
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('skipped-vacuum-analyze-before-entry-lock', 'wait');
+}
+step vacuum	{ VACUUM (SKIP_LOCKED) test_skip; }
+step noop {}
+step detach	{ SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock'); }
+
+# s3: drops table or wakes up the vacuumer
+session s3
+step drop_table	{ DROP TABLE test_skip; }
+step rollback_drop
+{
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+}
+step wakeup	{ SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock'); }
+step check_stats
+{
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+}
+
+# Table dropped while vacuumer is blocked: no orphaned stats entry.
+permutation lock vacuum(wakeup) unlock drop_table wakeup noop check_stats detach
+
+# DROP rolled back while vacuumer is blocked: skip is still recorded.
+permutation lock vacuum(wakeup) unlock rollback_drop wakeup noop check_stats detach
-- 
2.43.0

  [text/x-diff] v13-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch (43.1K, ../../20260727130341.6d2791b736304c7119249326@sraoss.co.jp/3-v13-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch)
  download | inline diff:
From 0c86c9480ff81f738505ca153449a1f9c60c8a11 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v13 1/2] Track skipped vacuum and analyze activity per
 relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_lock_skipped_vacuum
-    last_lock_skipped_autovacuum
-    last_lock_skipped_analyze
-    last_lock_skipped_autoanalyze
-    lock_skipped_vacuum_count
-    lock_skipped_autovacuum_count
-    lock_skipped_analyze_count
-    lock_skipped_autoanalyze_count

The field names make explicit that the skip is due to lock
unavailability, distinguishing it from other reasons a vacuum or
analyze might not run on a relation.

These fields can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  38 +++
 src/backend/utils/activity/pgstat_relation.c  |  85 ++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 ++
 src/include/pgstat.h                          |  19 +-
 .../isolation/expected/vacuum-skip-locked.out | 276 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  42 ++-
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 601 insertions(+), 35 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 1ce0ef00799..e4d6dda1166 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4678,6 +4678,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4688,6 +4698,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4697,6 +4717,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4707,6 +4737,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4717,6 +4757,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4727,6 +4777,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4736,6 +4796,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4746,6 +4816,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4801,6 +4881,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 090281a03dd..d5e127f432d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_lock_skipped_vacuum_time(C.oid) as last_lock_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_lock_skipped_autovacuum_time(C.oid) as last_lock_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_lock_skipped_analyze_time(C.oid) as last_lock_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_lock_skipped_autoanalyze_time(C.oid) as last_lock_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_lock_skipped_vacuum_count(C.oid) AS lock_skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_lock_skipped_autovacuum_count(C.oid) AS lock_skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_lock_skipped_analyze_count(C.oid) AS lock_skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_lock_skipped_autoanalyze_count(C.oid) AS lock_skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..613ff24d4f5 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,26 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int			flags = 0;
+
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -930,6 +948,8 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 */
 		if (!OidIsValid(relid))
 		{
+			int			flags = 0;
+
 			if (options & VACOPT_VACUUM)
 				ereport(WARNING,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
@@ -940,6 +960,24 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/*
+			 * Get relid for statistics reporting.
+			 *
+			 * Since we failed to acquire the lock, use NoLock here.  Although
+			 * a concurrent DDL may have dropped or renamed the relation,
+			 * RangeVarGetRelid() with NoLock does not check for invalidation
+			 * messages.
+			 */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+			if ((options & VACOPT_ANALYZE) != 0)
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+
+			pgstat_report_skipped_vacuum_analyze(relid, flags);
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 04f2eb21d0b..d8259823925 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +370,88 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_unlock_entry(entry_ref);
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid, true);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_VACUUM)
+	{
+		tabentry->last_lock_skipped_vacuum_time = ts;
+		tabentry->lock_skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_lock_skipped_autovacuum_time = ts;
+		tabentry->lock_skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE)
+	{
+		tabentry->last_lock_skipped_analyze_time = ts;
+		tabentry->lock_skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_lock_skipped_autoanalyze_time = ts;
+		tabentry->lock_skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..6a05c3660e2 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_lock_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_analyze_count)
+
+/* pg_stat_get_lock_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autoanalyze_count)
+
+/* pg_stat_get_lock_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autovacuum_count)
+
+/* pg_stat_get_lock_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_lock_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_analyze_time)
+
+/* pg_stat_get_last_lock_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_lock_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autovacuum_time)
+
+/* pg_stat_get_last_lock_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..d5484465af2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5678,6 +5678,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last auto vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last auto analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of auto vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of analyzes skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of auto analyzes skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..5146d7489a7 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
 {
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_lock_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter lock_skipped_vacuum_count;
+	TimestampTz last_lock_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter lock_skipped_autovacuum_count;
+	TimestampTz last_lock_skipped_analyze_time; /* user initiated */
+	PgStat_Counter lock_skipped_analyze_count;
+	TimestampTz last_lock_skipped_autoanalyze_time; /* autovacuum initiated */
+	PgStat_Counter lock_skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -716,6 +725,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_LOCK_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+															 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..b1b25036163 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +35,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +61,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +86,23 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +112,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +137,23 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +163,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +188,23 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +214,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +239,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +265,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +291,23 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            1|                         0
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +317,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +343,23 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           1|                        0|            1|                         0
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +369,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +394,18 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..e6cd1cf6f4f 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,16 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +53,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 6a3341356da..3fcee4dc097 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_lock_skipped_vacuum_time(c.oid) AS last_lock_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_lock_skipped_autovacuum_time(c.oid) AS last_lock_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_lock_skipped_analyze_time(c.oid) AS last_lock_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_lock_skipped_autoanalyze_time(c.oid) AS last_lock_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_lock_skipped_vacuum_count(c.oid) AS lock_skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_lock_skipped_autovacuum_count(c.oid) AS lock_skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_lock_skipped_analyze_count(c.oid) AS lock_skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_lock_skipped_autoanalyze_count(c.oid) AS lock_skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2357,13 +2365,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2412,13 +2428,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-07-27 22:21  Sami Imseih <samimseih@gmail.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 0 replies; 45+ messages in thread

From: Sami Imseih @ 2026-07-27 22:21 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Shinya Kato <shinya11.kato@gmail.com>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Michael Paquier <michael@paquier.xyz>

Hi,

> I've attached a patch to fix this by inserting "noop" step in session s2
> to ensure that the vacuum has finished.
>
> Does this look good to you?
>
> [1] https://github.com/postgresql-cfbot/postgresql/actions/runs/30115503531/attempts/1

Yes, this looks correct to me.

Thanks!

--
Sami Imseih
Amazon Web Services (AWS)






^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-07-28 14:14  Shinya Kato <shinya11.kato@gmail.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  1 sibling, 1 reply; 45+ messages in thread

From: Shinya Kato @ 2026-07-28 14:14 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Sami Imseih <samimseih@gmail.com>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Michael Paquier <michael@paquier.xyz>

On Sat, Jul 25, 2026 at 1:48 AM Yugo Nagata <nagata@sraoss.co.jp> wrote:

> I've attached the v12 patch, which incorporates Kato-san's feedback.
>
> Regarding the column names, I noticed the change from skipped_* to lock_skipped_*.
> While I initially preferred skipped_*, I understand Kato-san's point considering
> that there could be other reasons for skipping in the future.

Thank you!

> > > +{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
> > > +  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
> >
> > The proname now says lock_skipped, but the descr still says just
> > "skipped". I think the descr should mention the lock cause as well,
> > and the same applies to the other seven entries.
>
> I changed them like:
>
> 'statistics: last vacuum skip time due to lock unavailability for a table'
> 'statistics: number of vacuum skips due to lock unavailability for a table'
>
> How do these sound?

These are well to me. One small thing about the analyze count entries:

> +{ oid => '8148', descr => 'statistics: number of analyzes skips due to lock unavailability for a table',

"analyzes skips" doubles the plural. Since "skips" is the noun here, I
think "analyze skips" is better, matching "vacuum skips" in 8146. The
same applies to 8149.




--
Best regards,
Shinya Kato
NTT OSS Center






^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-07-28 14:27  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Shinya Kato <shinya11.kato@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-07-28 14:27 UTC (permalink / raw)
  To: Shinya Kato <shinya11.kato@gmail.com>; +Cc: Sami Imseih <samimseih@gmail.com>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Michael Paquier <michael@paquier.xyz>

On Tue, 28 Jul 2026 23:14:32 +0900
Shinya Kato <shinya11.kato@gmail.com> wrote:

> On Sat, Jul 25, 2026 at 1:48 AM Yugo Nagata <nagata@sraoss.co.jp> wrote:
> 
> > I've attached the v12 patch, which incorporates Kato-san's feedback.
> >
> > Regarding the column names, I noticed the change from skipped_* to lock_skipped_*.
> > While I initially preferred skipped_*, I understand Kato-san's point considering
> > that there could be other reasons for skipping in the future.
> 
> Thank you!
> 
> > > > +{ oid => '8142', descr => 'statistics: last skipped vacuum time for a table',
> > > > +  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
> > >
> > > The proname now says lock_skipped, but the descr still says just
> > > "skipped". I think the descr should mention the lock cause as well,
> > > and the same applies to the other seven entries.
> >
> > I changed them like:
> >
> > 'statistics: last vacuum skip time due to lock unavailability for a table'
> > 'statistics: number of vacuum skips due to lock unavailability for a table'
> >
> > How do these sound?
> 
> These are well to me. One small thing about the analyze count entries:
> 
> > +{ oid => '8148', descr => 'statistics: number of analyzes skips due to lock unavailability for a table',
> 
> "analyzes skips" doubles the plural. Since "skips" is the noun here, I
> think "analyze skips" is better, matching "vacuum skips" in 8146. The
> same applies to 8149.

Thanks for pointing it out.
I've updated the patch to fix this.

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>

Attachments:

  [text/x-diff] v14-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch (8.0K, ../../20260728232710.a00f54d1136b788437b2eb7e@sraoss.co.jp/2-v14-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch)
  download | inline diff:
From d6fe3d4d0a2b7c6ae788f0b0ec3341ca11c0c9ed Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 24 Jul 2026 22:45:53 +0900
Subject: [PATCH v14 2/2] Add injection point test for vacuum skip_locked stats

Add an isolation test exercising the race window between VACUUM
(SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops.

Two scenarios are tested:

1. Table dropped (committed) while vacuumer is blocked at the injection
   point: no orphaned stats entry is created.
2. DROP TABLE rolled back while vacuumer is blocked: skip is still
   recorded since the table and its stats entry survive.
---
 src/backend/utils/activity/pgstat_relation.c  |  2 +
 src/test/modules/injection_points/Makefile    |  3 +-
 .../expected/vacuum_skip_lock_stats.out       | 93 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/vacuum_skip_lock_stats.spec         | 68 ++++++++++++++
 5 files changed, 166 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
 create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec

diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index d8259823925..6c1e2a6c0f2 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -21,6 +21,7 @@
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
@@ -391,6 +392,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 		return;					/* somebody deleted the rel, forget it */
 	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("skipped-vacuum-analyze-before-entry-lock", NULL);
 
 	/* Store the data in the table's hash table entry. */
 	ts = GetCurrentTimestamp();
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index fac80f3a4a7..b833d9d481d 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -20,7 +20,8 @@ ISOLATION = basic \
 	    repack_toast \
 	    syscache-update-pruned \
 	    wait_cleanup \
-	    heap_lock_update
+	    heap_lock_update \
+	    vacuum_skip_lock_stats
 
 # some isolation tests require wal_level=replica
 ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf
diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..0efed3db2db
--- /dev/null
+++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
@@ -0,0 +1,93 @@
+Parsed test spec with 3 sessions
+
+starting permutation: lock vacuum unlock drop_table wakeup noop check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step drop_table: DROP TABLE test_skip;
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step noop: 
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         0
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+
+starting permutation: lock vacuum unlock rollback_drop wakeup noop check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step rollback_drop: 
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step noop: 
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         1
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 163b6374ebc..a96753fc326 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -52,6 +52,7 @@ tests += {
       'syscache-update-pruned',
       'wait_cleanup',
       'heap_lock_update',
+      'vacuum_skip_lock_stats',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
new file mode 100644
index 00000000000..b277b1e80f7
--- /dev/null
+++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
@@ -0,0 +1,68 @@
+# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting
+# and concurrent DROP TABLE.
+#
+# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped
+# statistics via pgstat_report_skipped_vacuum_analyze().  An injection
+# point after the syscache lookup but before the stats update allows us
+# to verify that a concurrent DROP does not leave orphaned stats entries.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE test_skip (id int);
+	INSERT INTO test_skip VALUES (1);
+	ANALYZE test_skip;
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE saved_oid (oid_val oid);
+	INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip';
+}
+
+teardown
+{
+	DROP TABLE IF EXISTS test_skip;
+	DROP TABLE IF EXISTS saved_oid;
+	DROP EXTENSION injection_points;
+}
+
+# s1: holds the lock so VACUUM skips the table
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+}
+step unlock	{ COMMIT; }
+
+# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip
+session s2
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('skipped-vacuum-analyze-before-entry-lock', 'wait');
+}
+step vacuum	{ VACUUM (SKIP_LOCKED) test_skip; }
+step noop {}
+step detach	{ SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock'); }
+
+# s3: drops table or wakes up the vacuumer
+session s3
+step drop_table	{ DROP TABLE test_skip; }
+step rollback_drop
+{
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+}
+step wakeup	{ SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock'); }
+step check_stats
+{
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+}
+
+# Table dropped while vacuumer is blocked: no orphaned stats entry.
+permutation lock vacuum(wakeup) unlock drop_table wakeup noop check_stats detach
+
+# DROP rolled back while vacuumer is blocked: skip is still recorded.
+permutation lock vacuum(wakeup) unlock rollback_drop wakeup noop check_stats detach
-- 
2.43.0

  [text/x-diff] v14-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch (43.1K, ../../20260728232710.a00f54d1136b788437b2eb7e@sraoss.co.jp/3-v14-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch)
  download | inline diff:
From 0f81dae7224f12e0d03b84cec6acb9c742a95044 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v14 1/2] Track skipped vacuum and analyze activity per
 relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_lock_skipped_vacuum
-    last_lock_skipped_autovacuum
-    last_lock_skipped_analyze
-    last_lock_skipped_autoanalyze
-    lock_skipped_vacuum_count
-    lock_skipped_autovacuum_count
-    lock_skipped_analyze_count
-    lock_skipped_autoanalyze_count

The field names make explicit that the skip is due to lock
unavailability, distinguishing it from other reasons a vacuum or
analyze might not run on a relation.

These fields can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.
---
 doc/src/sgml/monitoring.sgml                  |  88 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  38 +++
 src/backend/utils/activity/pgstat_relation.c  |  85 ++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 ++
 src/include/pgstat.h                          |  19 +-
 .../isolation/expected/vacuum-skip-locked.out | 276 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  42 ++-
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 601 insertions(+), 35 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index a209e891b18..caf9c40254d 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4681,6 +4681,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table was attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4691,6 +4701,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4700,6 +4720,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table was attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4710,6 +4740,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time at which an analyze on this table by the autovacuum was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4720,6 +4760,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table have been attempted but skipped
+       due to lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4730,6 +4780,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4739,6 +4799,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table have been attempted but
+       skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4749,6 +4819,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4804,6 +4884,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped, the statistics are recorded only for
+    the parent table, not for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 090281a03dd..d5e127f432d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_lock_skipped_vacuum_time(C.oid) as last_lock_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_lock_skipped_autovacuum_time(C.oid) as last_lock_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_lock_skipped_analyze_time(C.oid) as last_lock_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_lock_skipped_autoanalyze_time(C.oid) as last_lock_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_lock_skipped_vacuum_count(C.oid) AS lock_skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_lock_skipped_autovacuum_count(C.oid) AS lock_skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_lock_skipped_analyze_count(C.oid) AS lock_skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_lock_skipped_autoanalyze_count(C.oid) AS lock_skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 38539a6fd3d..613ff24d4f5 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -793,8 +793,26 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 		rel = try_relation_open(relid, NoLock);
 	else
 	{
+		int			flags = 0;
+
 		rel = NULL;
 		rel_lock = false;
+
+		if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM;
+			else
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+		}
+		if ((options & VACOPT_ANALYZE) != 0)
+		{
+			if (AmAutoVacuumWorkerProcess())
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE;
+			else
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+		}
+		pgstat_report_skipped_vacuum_analyze(relid, flags);
 	}
 
 	/* if relation is opened, leave */
@@ -930,6 +948,8 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 		 */
 		if (!OidIsValid(relid))
 		{
+			int			flags = 0;
+
 			if (options & VACOPT_VACUUM)
 				ereport(WARNING,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
@@ -940,6 +960,24 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/*
+			 * Get relid for statistics reporting.
+			 *
+			 * Since we failed to acquire the lock, use NoLock here.  Although
+			 * a concurrent DDL may have dropped or renamed the relation,
+			 * RangeVarGetRelid() with NoLock does not check for invalidation
+			 * messages.
+			 */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+			if ((options & VACOPT_ANALYZE) != 0)
+				flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+
+			pgstat_report_skipped_vacuum_analyze(relid, flags);
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 04f2eb21d0b..d8259823925 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -367,6 +370,88 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum or/and analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	bool		isshared;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	isshared = ((Form_pg_class) GETSTRUCT(classTup))->relisshared;
+	ReleaseSysCache(classTup);
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_unlock_entry(entry_ref);
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid, true);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_VACUUM)
+	{
+		tabentry->last_lock_skipped_vacuum_time = ts;
+		tabentry->lock_skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_lock_skipped_autovacuum_time = ts;
+		tabentry->lock_skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE)
+	{
+		tabentry->last_lock_skipped_analyze_time = ts;
+		tabentry->lock_skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_lock_skipped_autoanalyze_time = ts;
+		tabentry->lock_skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 565d0e70768..6a05c3660e2 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_lock_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_analyze_count)
+
+/* pg_stat_get_lock_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autoanalyze_count)
+
+/* pg_stat_get_lock_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autovacuum_count)
+
+/* pg_stat_get_lock_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -170,6 +182,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_lock_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_analyze_time)
+
+/* pg_stat_get_last_lock_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_lock_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autovacuum_time)
+
+/* pg_stat_get_last_lock_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..4740500ab30 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5678,6 +5678,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last auto vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last auto analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of auto vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of analyze skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of auto analyze skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 58a44857f13..5146d7489a7 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
 {
@@ -479,6 +479,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_lock_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter lock_skipped_vacuum_count;
+	TimestampTz last_lock_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter lock_skipped_autovacuum_count;
+	TimestampTz last_lock_skipped_analyze_time; /* user initiated */
+	PgStat_Counter lock_skipped_analyze_count;
+	TimestampTz last_lock_skipped_autoanalyze_time; /* autovacuum initiated */
+	PgStat_Counter lock_skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -716,6 +725,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_flush_backend() */
+#define PGSTAT_REPORT_LOCK_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+															 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..b1b25036163 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +35,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_share analyze_specified commit
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +61,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +86,23 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +112,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_analyze_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +137,23 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_full_specified commit
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +163,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +188,23 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +214,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +239,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +265,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +291,23 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            1|                         0
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +317,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +343,23 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           1|                        0|            1|                         0
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +369,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +394,18 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..e6cd1cf6f4f 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -33,6 +33,16 @@ step commit
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -43,19 +53,19 @@ step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 6a3341356da..3fcee4dc097 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_lock_skipped_vacuum_time(c.oid) AS last_lock_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_lock_skipped_autovacuum_time(c.oid) AS last_lock_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_lock_skipped_analyze_time(c.oid) AS last_lock_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_lock_skipped_autoanalyze_time(c.oid) AS last_lock_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_lock_skipped_vacuum_count(c.oid) AS lock_skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_lock_skipped_autovacuum_count(c.oid) AS lock_skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_lock_skipped_analyze_count(c.oid) AS lock_skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_lock_skipped_autoanalyze_count(c.oid) AS lock_skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2357,13 +2365,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2412,13 +2428,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.43.0

^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-08-12 03:00  Shinya Kato <shinya11.kato@gmail.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 1 reply; 45+ messages in thread

From: Shinya Kato @ 2026-08-12 03:00 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Sami Imseih <samimseih@gmail.com>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Michael Paquier <michael@paquier.xyz>

On Tue, Jul 28, 2026 at 11:27 PM Yugo Nagata <nagata@sraoss.co.jp> wrote:

> Thanks for pointing it out.
> I've updated the patch to fix this.

Thanks. v14 applies cleanly to master and check-world passes for me.
This time I looked at the reporting path itself and have a few
comments.

1. pgstat_report_skipped_vacuum_analyze() accesses the catalog while
holding the stats entry lock.

pgstat_get_entry_ref_locked() returns with the entry's LWLock held,
and the re-check runs under it. A catcache miss there opens pg_class,
so we can wait for a heavyweight lock while holding an LWLock, which
can deadlock. The deadlock detector does not see the LWLock side, and
LWLockAcquire() holds interrupts, so pg_cancel_backend() does not
break the wait either.

Could the re-check simply run after pgstat_unlock_entry()? The same
case then cancels immediately.

2. A skipped TOAST vacuum is also counted as a skipped analyze.

vacuum_rel() copies params for the TOAST recursion and only adds
VACOPT_PROCESS_MAIN, so VACOPT_ANALYZE survives into the recursive
call, and a contended lock on the toast table bumps its
lock_skipped_analyze_count as well. The comment just above that code
says analyze is never done on toast tables, so clearing the flag next
to the VACOPT_PROCESS_MAIN line looks right to me:

```
+ toast_vacuum_params.options &= ~VACOPT_ANALYZE;
```

3. Relations that can never be vacuumed get a stats entry.

The skip is reported before the relkind checks in vacuum_rel() and
analyze_rel(), so VACUUM (SKIP_LOCKED) on a locked view creates a
relation stats entry and bumps the counters. pg_stat_all_tables
filters on relkind, so nothing can ever display them. The function
already reads the pg_class tuple for relisshared, so it could pick up
relkind at the same time and return early for the relkinds those two
functions reject.

4. Smaller things:

- pgstat.h says "/* flags for pgstat_flush_backend() */" above the new
PGSTAT_REPORT_LOCK_SKIPPED_* macros. They are the flags of
pgstat_report_skipped_vacuum_analyze().

- The docs never mention SKIP_LOCKED. A manual command waits for the
lock otherwise, so the four manual columns only advance for commands
using that option. A sentence in the new note would cover it.

- last_lock_skipped_autoanalyze says "by the autovacuum" and "Last
time at which", while the three sibling entries say "by the autovacuum
daemon" and "Last time".

- The four non-auto descrs in pg_proc.dat drop "manual", although the
existing entries next to them say things like 'statistics: last manual
vacuum time for a table'.


Thoughts?

-- 
Shinya Kato
NTT OSS Center






^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-09-14 19:43  Sami Imseih <samimseih.pg@gmail.com>
  parent: Shinya Kato <shinya11.kato@gmail.com>
  0 siblings, 2 replies; 45+ messages in thread

From: Sami Imseih @ 2026-09-14 19:43 UTC (permalink / raw)
  To: Shinya Kato <shinya11.kato@gmail.com>; +Cc: Yugo Nagata <nagata@sraoss.co.jp>; Sami Imseih <samimseih@gmail.com>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Michael Paquier <michael@paquier.xyz>

Hi,

> 1. pgstat_report_skipped_vacuum_analyze() accesses the catalog while
> holding the stats entry lock.
>
> pgstat_get_entry_ref_locked() returns with the entry's LWLock held,
> and the re-check runs under it. A catcache miss there opens pg_class,
> so we can wait for a heavyweight lock while holding an LWLock, which
> can deadlock. The deadlock detector does not see the LWLock side, and
> LWLockAcquire() holds interrupts, so pg_cancel_backend() does not
> break the wait either.
>
> Could the re-check simply run after pgstat_unlock_entry()? The same
> case then cancels immediately.

Right. That is a good point. The cost becomes updating an entry that
will, on the rare occurrence, be immediately dropped. That is not a
big deal.

> 2. A skipped TOAST vacuum is also counted as a skipped analyze.
>
> vacuum_rel() copies params for the TOAST recursion and only adds
> VACOPT_PROCESS_MAIN, so VACOPT_ANALYZE survives into the recursive
> call, and a contended lock on the toast table bumps its
> lock_skipped_analyze_count as well. The comment just above that code
> says analyze is never done on toast tables, so clearing the flag next
> to the VACOPT_PROCESS_MAIN line looks right to me:
>
> ```
> + toast_vacuum_params.options &= ~VACOPT_ANALYZE;
> ```

Right. Fixed.

> 3. Relations that can never be vacuumed get a stats entry.
>
> The skip is reported before the relkind checks in vacuum_rel() and
> analyze_rel(), so VACUUM (SKIP_LOCKED) on a locked view creates a
> relation stats entry and bumps the counters. pg_stat_all_tables
> filters on relkind, so nothing can ever display them. The function
> already reads the pg_class tuple for relisshared, so it could pick up
> relkind at the same time and return early for the relkinds those two
> functions reject.

Added a filter to only track stats for any relation kind that can be displayed.
I added a test for the view case of this also.

It should be noted that we don't show foreign tables in pg_stat_all_tables,
so while these could be skipped in analyze, they do not have a way to be
reported.

Also, a a follow-up we may want to clean up cases in which relations that
cannot be vacuumed or analyzed could log a "lock not available"

```
postgres=#   VACUUM (SKIP_LOCKED) codex_skip_test.v;
WARNING:  skipping vacuum of "v" --- lock not available
VACUUM
```

This is less about stats reporting and more about filtering out such
relations earlier in expand_vacuum_rel() for manually specified
VACUUM/ANALYZE targets. Not a big thing, but it would avoid an extra
syscache lookup and a misleading "lock not available" warning.

> 4. Smaller things:
>
> - pgstat.h says "/* flags for pgstat_flush_backend() */" above the new
> PGSTAT_REPORT_LOCK_SKIPPED_* macros. They are the flags of
> pgstat_report_skipped_vacuum_analyze().

Fixed.

> - The docs never mention SKIP_LOCKED. A manual command waits for the
> lock otherwise, so the four manual columns only advance for commands
> using that option. A sentence in the new note would cover it.

I just added the SKIP_LOCKED callut in the description for the columns.

> - last_lock_skipped_autoanalyze says "by the autovacuum" and "Last
> time at which", while the three sibling entries say "by the autovacuum
> daemon" and "Last time".

Fixed

> - The four non-auto descrs in pg_proc.dat drop "manual", although the
> existing entries next to them say things like 'statistics: last manual
> vacuum time for a table'.

Fixed.

--
Sami Imseih
Amazon Web Services (AWS)

Attachments:

  [application/octet-stream] v15-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch (7.9K, ../../CAN12+YJ+F76jWvauHGZx1_1v0Y3WWMearw7UzXpaH=301Q37jQ@mail.gmail.com/2-v15-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch)
  download | inline diff:
From 33799bab5151a8aeab90259af95f18805e5c355c Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 24 Jul 2026 22:45:53 +0900
Subject: [PATCH v15 2/2] Add injection point test for vacuum skip_locked stats

Add an isolation test exercising the race window between VACUUM
(SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops.

Two scenarios are tested:

1. Table dropped (committed) while vacuumer is blocked at the injection
   point: no orphaned stats entry is created.
2. DROP TABLE rolled back while vacuumer is blocked: skip is still
   recorded since the table and its stats entry survive.
---
 src/backend/utils/activity/pgstat_relation.c  |  2 +
 src/test/modules/injection_points/Makefile    |  3 +-
 .../expected/vacuum_skip_lock_stats.out       | 93 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/vacuum_skip_lock_stats.spec         | 68 ++++++++++++++
 5 files changed, 166 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
 create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec

diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index e36fa0c4600..4265524bf51 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -21,6 +21,7 @@
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
@@ -448,6 +449,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 	isshared = classForm->relisshared;
 	relkind = classForm->relkind;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("skipped-vacuum-analyze-before-entry-lock", NULL);
 
 	/*
 	 * Only track skipped vacuum/analyze activity for relkinds exposed by
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 408a35c3c21..0ec89c64629 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -26,7 +26,8 @@ ISOLATION = basic \
 	    ri_fastpath_snapshot \
 	    syscache-update-pruned \
 	    wait_cleanup \
-	    heap_lock_update
+	    heap_lock_update \
+	    vacuum_skip_lock_stats
 
 # some isolation tests require wal_level=replica
 ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf
diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..0efed3db2db
--- /dev/null
+++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
@@ -0,0 +1,93 @@
+Parsed test spec with 3 sessions
+
+starting permutation: lock vacuum unlock drop_table wakeup noop check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step drop_table: DROP TABLE test_skip;
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step noop: 
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         0
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+
+starting permutation: lock vacuum unlock rollback_drop wakeup noop check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step rollback_drop: 
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step noop: 
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         1
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index a7b40e084f6..3b719c40225 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -56,6 +56,7 @@ tests += {
       'syscache-update-pruned',
       'wait_cleanup',
       'heap_lock_update',
+      'vacuum_skip_lock_stats',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
new file mode 100644
index 00000000000..b277b1e80f7
--- /dev/null
+++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
@@ -0,0 +1,68 @@
+# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting
+# and concurrent DROP TABLE.
+#
+# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped
+# statistics via pgstat_report_skipped_vacuum_analyze().  An injection
+# point after the syscache lookup but before the stats update allows us
+# to verify that a concurrent DROP does not leave orphaned stats entries.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE test_skip (id int);
+	INSERT INTO test_skip VALUES (1);
+	ANALYZE test_skip;
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE saved_oid (oid_val oid);
+	INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip';
+}
+
+teardown
+{
+	DROP TABLE IF EXISTS test_skip;
+	DROP TABLE IF EXISTS saved_oid;
+	DROP EXTENSION injection_points;
+}
+
+# s1: holds the lock so VACUUM skips the table
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+}
+step unlock	{ COMMIT; }
+
+# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip
+session s2
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('skipped-vacuum-analyze-before-entry-lock', 'wait');
+}
+step vacuum	{ VACUUM (SKIP_LOCKED) test_skip; }
+step noop {}
+step detach	{ SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock'); }
+
+# s3: drops table or wakes up the vacuumer
+session s3
+step drop_table	{ DROP TABLE test_skip; }
+step rollback_drop
+{
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+}
+step wakeup	{ SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock'); }
+step check_stats
+{
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+}
+
+# Table dropped while vacuumer is blocked: no orphaned stats entry.
+permutation lock vacuum(wakeup) unlock drop_table wakeup noop check_stats detach
+
+# DROP rolled back while vacuumer is blocked: skip is still recorded.
+permutation lock vacuum(wakeup) unlock rollback_drop wakeup noop check_stats detach
-- 
2.50.1



  [application/octet-stream] v15-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch (46.6K, ../../CAN12+YJ+F76jWvauHGZx1_1v0Y3WWMearw7UzXpaH=301Q37jQ@mail.gmail.com/3-v15-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch)
  download | inline diff:
From e7bf539df780c6867a937b76df5850e2e217ae5d Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v15 1/2] Track skipped vacuum and analyze activity per
 relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_lock_skipped_vacuum
-    last_lock_skipped_autovacuum
-    last_lock_skipped_analyze
-    last_lock_skipped_autoanalyze
-    lock_skipped_vacuum_count
-    lock_skipped_autovacuum_count
-    lock_skipped_analyze_count
-    lock_skipped_autoanalyze_count

The field names make explicit that the skip is due to lock
unavailability, distinguishing it from other reasons a vacuum or
analyze might not run on a relation.

These fields can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.

Because this patch adds new built-in statistics functions and extends
pg_stat_all_tables with new columns, it also requires a catversion bump.
---
 doc/src/sgml/monitoring.sgml                  |  93 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  46 +++
 src/backend/utils/activity/pgstat_relation.c  | 111 +++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/catversion.h              |   2 +-
 src/include/catalog/pg_proc.dat               |  32 ++
 src/include/pgstat.h                          |  17 +
 .../isolation/expected/vacuum-skip-locked.out | 296 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  56 +++-
 src/test/regress/expected/rules.out           |  24 ++
 11 files changed, 674 insertions(+), 35 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 62dadf3e86c..45576998cc5 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4679,6 +4679,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table using
+       <literal>SKIP_LOCKED</literal> was attempted but skipped due to lock
+       unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4689,6 +4700,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4698,6 +4719,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table using
+       <literal>SKIP_LOCKED</literal> was attempted but skipped due to lock
+       unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4708,6 +4740,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time an analyze on this table by the autovacuum daemon was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4718,6 +4760,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table using
+       <literal>SKIP_LOCKED</literal> have been attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4728,6 +4781,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4737,6 +4800,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table using
+       <literal>SKIP_LOCKED</literal> have been attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4747,6 +4821,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4802,6 +4886,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped before its child relations are
+    processed, the statistics are recorded only for the parent table, not
+    for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index ad340887f54..6435a6c0aaa 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_lock_skipped_vacuum_time(C.oid) as last_lock_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_lock_skipped_autovacuum_time(C.oid) as last_lock_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_lock_skipped_analyze_time(C.oid) as last_lock_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_lock_skipped_autoanalyze_time(C.oid) as last_lock_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_lock_skipped_vacuum_count(C.oid) AS lock_skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_lock_skipped_autovacuum_count(C.oid) AS lock_skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_lock_skipped_analyze_count(C.oid) AS lock_skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_lock_skipped_autoanalyze_count(C.oid) AS lock_skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index d8c2f33c615..6a1c1704e54 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -132,6 +132,7 @@ static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
+static int	vacuum_relation_skip_locked_flags(uint32 options);
 
 /*
  * GUC check function to ensure GUC value specified is within the allowable
@@ -154,6 +155,33 @@ check_vacuum_buffer_usage_limit(int *newval, void **extra,
 	return false;
 }
 
+/*
+ * Return skipped-stats flags for a lock skip.
+ */
+static int
+vacuum_relation_skip_locked_flags(uint32 options)
+{
+	int			flags = 0;
+
+	if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+	{
+		if (AmAutoVacuumWorkerProcess())
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM;
+		else
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+	}
+
+	if ((options & VACOPT_ANALYZE) != 0)
+	{
+		if (AmAutoVacuumWorkerProcess())
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE;
+		else
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+	}
+
+	return flags;
+}
+
 /*
  * Primary entry point for manual VACUUM and ANALYZE commands
  *
@@ -815,6 +843,9 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 vacuum_relation_skip_locked_flags(options));
 	}
 
 	/* if relation is opened, leave */
@@ -960,6 +991,20 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/*
+			 * Get relid for statistics reporting.
+			 *
+			 * Since we failed to acquire the lock, use NoLock here.  Although
+			 * a concurrent DDL may have dropped or renamed the relation,
+			 * RangeVarGetRelid() with NoLock does not check for invalidation
+			 * messages.
+			 */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			pgstat_report_skipped_vacuum_analyze(relid,
+												 vacuum_relation_skip_locked_flags(options));
+
 			return vacrels;
 		}
 
@@ -2385,6 +2430,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		 * lock on the main relation that prevents concurrent deletion.
 		 */
 		toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
+		toast_vacuum_params.options &= ~VACOPT_ANALYZE;
 		toast_vacuum_params.toast_parent = relid;
 
 		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 5c70543ba88..e36fa0c4600 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -420,6 +423,114 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum and/or analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	Form_pg_class classForm;
+	bool		isshared;
+	char		relkind;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	classForm = (Form_pg_class) GETSTRUCT(classTup);
+	isshared = classForm->relisshared;
+	relkind = classForm->relkind;
+	ReleaseSysCache(classTup);
+
+	/*
+	 * Only track skipped vacuum/analyze activity for relkinds exposed by
+	 * pg_stat_all_tables.
+	 */
+	if (relkind != RELKIND_RELATION &&
+		relkind != RELKIND_MATVIEW &&
+		relkind != RELKIND_TOASTVALUE &&
+		relkind != RELKIND_PARTITIONED_TABLE)
+		flags &= ~(PGSTAT_REPORT_LOCK_SKIPPED_VACUUM |
+				   PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM);
+
+	if (relkind != RELKIND_RELATION &&
+		relkind != RELKIND_MATVIEW &&
+		relkind != RELKIND_PARTITIONED_TABLE)
+		flags &= ~(PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE |
+				   PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE);
+
+	if (!flags)
+		return;
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_VACUUM)
+	{
+		tabentry->last_lock_skipped_vacuum_time = ts;
+		tabentry->lock_skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_lock_skipped_autovacuum_time = ts;
+		tabentry->lock_skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE)
+	{
+		tabentry->last_lock_skipped_analyze_time = ts;
+		tabentry->lock_skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_lock_skipped_autoanalyze_time = ts;
+		tabentry->lock_skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.  This
+	 * has to happen after releasing the stats entry lock, because resolving a
+	 * catcache miss may need to access pg_class and wait for a heavyweight
+	 * lock while still holding that LWLock, which risks deadlock.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid, true);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 64b6f60516c..844a023debd 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_lock_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_analyze_count)
+
+/* pg_stat_get_lock_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autoanalyze_count)
+
+/* pg_stat_get_lock_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autovacuum_count)
+
+/* pg_stat_get_lock_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -204,6 +216,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_lock_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_analyze_time)
+
+/* pg_stat_get_last_lock_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_lock_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autovacuum_time)
+
+/* pg_stat_get_last_lock_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index f28b8db6e6f..7d9ec1be47e 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202609141
+#define CATALOG_VERSION_NO	202609142
 
 #endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f46427258e3..eaea44ce992 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5675,6 +5675,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last manual vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last auto vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last manual analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last auto analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of manual vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of auto vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of manual analyze skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of auto analyze skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 187d82c96fe..1a0f04a3cf8 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -536,6 +536,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_lock_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter lock_skipped_vacuum_count;
+	TimestampTz last_lock_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter lock_skipped_autovacuum_count;
+	TimestampTz last_lock_skipped_analyze_time; /* user initiated */
+	PgStat_Counter lock_skipped_analyze_count;
+	TimestampTz last_lock_skipped_autoanalyze_time; /* autovacuum initiated */
+	PgStat_Counter lock_skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -787,6 +796,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_report_skipped_vacuum_analyze() */
+#define PGSTAT_REPORT_LOCK_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+															 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..3712ea311e4 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +35,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_share analyze_specified commit
+
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +61,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_share analyze_all_parts commit
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +86,23 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +112,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_all_parts commit
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +137,23 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_full_specified commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +163,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_share vac_full_all_parts commit
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +188,23 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +214,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +239,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +265,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +291,23 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            1|                         0
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +317,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +343,23 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           1|                        0|            1|                         0
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +369,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +394,38 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_view vac_analyze_view commit check_view_stat
+step lock_view: 
+	BEGIN;
+	LOCK TABLE partv IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "partv" --- lock not available
+step vac_analyze_view: VACUUM (ANALYZE, SKIP_LOCKED) partv;
+step commit: 
+	COMMIT;
+
+step check_view_stat: 
+	SELECT pg_stat_get_lock_skipped_vacuum_count('partv'::regclass) AS vac_skip,
+		   pg_stat_get_lock_skipped_analyze_count('partv'::regclass) AS anl_skip;
+
+vac_skip|anl_skip
+--------+--------
+       0|       0
+(1 row)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..b03ea89c039 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -10,10 +10,12 @@ setup
 	ALTER TABLE part1 SET (autovacuum_enabled = false);
 	CREATE TABLE part2 PARTITION OF parted FOR VALUES IN (2);
 	ALTER TABLE part2 SET (autovacuum_enabled = false);
+	CREATE VIEW partv AS SELECT * FROM part1;
 }
 
 teardown
 {
+	DROP VIEW IF EXISTS partv;
 	DROP TABLE IF EXISTS parted;
 }
 
@@ -28,11 +30,31 @@ step lock_access_exclusive
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
 }
+step lock_view
+{
+	BEGIN;
+	LOCK TABLE partv IN ACCESS EXCLUSIVE MODE;
+}
 step commit
 {
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+}
+step check_view_stat
+{
+	SELECT pg_stat_get_lock_skipped_vacuum_count('partv'::regclass) AS vac_skip,
+		   pg_stat_get_lock_skipped_analyze_count('partv'::regclass) AS anl_skip;
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -40,22 +62,24 @@ step analyze_specified		{ ANALYZE (SKIP_LOCKED) part1, part2; }
 step analyze_all_parts		{ ANALYZE (SKIP_LOCKED) parted; }
 step vac_analyze_specified	{ VACUUM (ANALYZE, SKIP_LOCKED) part1, part2; }
 step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
+step vac_analyze_view		{ VACUUM (ANALYZE, SKIP_LOCKED) partv; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
+permutation lock_view vac_analyze_view commit check_view_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 4a8cc759d7b..a436fd409d0 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_lock_skipped_vacuum_time(c.oid) AS last_lock_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_lock_skipped_autovacuum_time(c.oid) AS last_lock_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_lock_skipped_analyze_time(c.oid) AS last_lock_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_lock_skipped_autoanalyze_time(c.oid) AS last_lock_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_lock_skipped_vacuum_count(c.oid) AS lock_skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_lock_skipped_autovacuum_count(c.oid) AS lock_skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_lock_skipped_analyze_count(c.oid) AS lock_skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_lock_skipped_autoanalyze_count(c.oid) AS lock_skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2358,13 +2366,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2413,13 +2429,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.50.1



^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-09-14 19:52  Sami Imseih <samimseih.pg@gmail.com>
  parent: Sami Imseih <samimseih.pg@gmail.com>
  1 sibling, 0 replies; 45+ messages in thread

From: Sami Imseih @ 2026-09-14 19:52 UTC (permalink / raw)
  To: Shinya Kato <shinya11.kato@gmail.com>; Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Michael Paquier <michael@paquier.xyz>

sorry for the noise. I removed my old email address
from this thread.

--
Sami Imseih
Amazon Web Services (AWS)






^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-09-16 12:48  Shinya Kato <shinya11.kato@gmail.com>
  parent: Sami Imseih <samimseih.pg@gmail.com>
  1 sibling, 1 reply; 45+ messages in thread

From: Shinya Kato @ 2026-09-16 12:48 UTC (permalink / raw)
  To: Sami Imseih <samimseih.pg@gmail.com>; +Cc: Yugo Nagata <nagata@sraoss.co.jp>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Michael Paquier <michael@paquier.xyz>

Thanks for updating the patches!

On Tue, Sep 15, 2026 at 4:43 AM Sami Imseih <samimseih.pg@gmail.com> wrote:
> This is less about stats reporting and more about filtering out such
> relations earlier in expand_vacuum_rel() for manually specified
> VACUUM/ANALYZE targets. Not a big thing, but it would avoid an extra
> syscache lookup and a misleading "lock not available" warning.

I would like to fold one more case into that follow-up. The skip is
reported before vacuum_is_permitted_for_relation(), so a user with no
rights on the table can still move its counters.

The callback argument of RangeVarGetRelidExtended() looks like the
right place for all of this. It runs before the lock attempt, and the
comment there already says that is where permission checks belong. It
also hands us relId, so expand_vacuum_rel() would no longer need the
second RangeVarGetRelid(..., NoLock, true) lookup.


-- 
Shinya Kato
NTT OSS Center






^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-09-16 15:30  Sami Imseih <samimseih.pg@gmail.com>
  parent: Shinya Kato <shinya11.kato@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Sami Imseih @ 2026-09-16 15:30 UTC (permalink / raw)
  To: Shinya Kato <shinya11.kato@gmail.com>; Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Michael Paquier <michael@paquier.xyz>

> On Tue, Sep 15, 2026 at 4:43 AM Sami Imseih <samimseih.pg@gmail.com> wrote:
> > This is less about stats reporting and more about filtering out such
> > relations earlier in expand_vacuum_rel() for manually specified
> > VACUUM/ANALYZE targets. Not a big thing, but it would avoid an extra
> > syscache lookup and a misleading "lock not available" warning.
>
> I would like to fold one more case into that follow-up. The skip is
> reported before vacuum_is_permitted_for_relation(), so a user with no
> rights on the table can still move its counters.
>
> The callback argument of RangeVarGetRelidExtended() looks like the
> right place for all of this. It runs before the lock attempt, and the
> comment there already says that is where permission checks belong. It
> also hands us relId, so expand_vacuum_rel() would no longer need the
> second RangeVarGetRelid(..., NoLock, true) lookup.

It may be worth cleaning this stuff up if someone is interested in taking
this on.

v16 is just a rebase. I also took out the catversion change for now, but made
it clear it's required in the commit message.

--
Sami Imseih
Amazon Web Services (AWS)

Attachments:

  [application/octet-stream] v16-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch (46.3K, ../../CAN12+YLDPuW8iF=UqXyLzmjM9MkHbx2YwwKq+8G2-HcO8Kzyfg@mail.gmail.com/2-v16-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch)
  download | inline diff:
From ff23f731e7fe7ebc9203c63a4df3e62e8377cae6 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v16 1/2] Track skipped vacuum and analyze activity per
 relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_lock_skipped_vacuum
-    last_lock_skipped_autovacuum
-    last_lock_skipped_analyze
-    last_lock_skipped_autoanalyze
-    lock_skipped_vacuum_count
-    lock_skipped_autovacuum_count
-    lock_skipped_analyze_count
-    lock_skipped_autoanalyze_count

The field names make explicit that the skip is due to lock
unavailability, distinguishing it from other reasons a vacuum or
analyze might not run on a relation.

These fields can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.

Because this patch adds new built-in statistics functions and extends
pg_stat_all_tables with new columns.

Catalog version bump required
---
 doc/src/sgml/monitoring.sgml                  |  93 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  46 +++
 src/backend/utils/activity/pgstat_relation.c  | 111 +++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 ++
 src/include/pgstat.h                          |  17 +
 .../isolation/expected/vacuum-skip-locked.out | 296 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  56 +++-
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 673 insertions(+), 34 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 62dadf3e86c..45576998cc5 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4679,6 +4679,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table using
+       <literal>SKIP_LOCKED</literal> was attempted but skipped due to lock
+       unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4689,6 +4700,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4698,6 +4719,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table using
+       <literal>SKIP_LOCKED</literal> was attempted but skipped due to lock
+       unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4708,6 +4740,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time an analyze on this table by the autovacuum daemon was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4718,6 +4760,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table using
+       <literal>SKIP_LOCKED</literal> have been attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4728,6 +4781,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4737,6 +4800,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table using
+       <literal>SKIP_LOCKED</literal> have been attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4747,6 +4821,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4802,6 +4886,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped before its child relations are
+    processed, the statistics are recorded only for the parent table, not
+    for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index ad340887f54..6435a6c0aaa 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_lock_skipped_vacuum_time(C.oid) as last_lock_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_lock_skipped_autovacuum_time(C.oid) as last_lock_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_lock_skipped_analyze_time(C.oid) as last_lock_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_lock_skipped_autoanalyze_time(C.oid) as last_lock_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_lock_skipped_vacuum_count(C.oid) AS lock_skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_lock_skipped_autovacuum_count(C.oid) AS lock_skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_lock_skipped_analyze_count(C.oid) AS lock_skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_lock_skipped_autoanalyze_count(C.oid) AS lock_skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index d8c2f33c615..6a1c1704e54 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -132,6 +132,7 @@ static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
+static int	vacuum_relation_skip_locked_flags(uint32 options);
 
 /*
  * GUC check function to ensure GUC value specified is within the allowable
@@ -154,6 +155,33 @@ check_vacuum_buffer_usage_limit(int *newval, void **extra,
 	return false;
 }
 
+/*
+ * Return skipped-stats flags for a lock skip.
+ */
+static int
+vacuum_relation_skip_locked_flags(uint32 options)
+{
+	int			flags = 0;
+
+	if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+	{
+		if (AmAutoVacuumWorkerProcess())
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM;
+		else
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+	}
+
+	if ((options & VACOPT_ANALYZE) != 0)
+	{
+		if (AmAutoVacuumWorkerProcess())
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE;
+		else
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+	}
+
+	return flags;
+}
+
 /*
  * Primary entry point for manual VACUUM and ANALYZE commands
  *
@@ -815,6 +843,9 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 vacuum_relation_skip_locked_flags(options));
 	}
 
 	/* if relation is opened, leave */
@@ -960,6 +991,20 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/*
+			 * Get relid for statistics reporting.
+			 *
+			 * Since we failed to acquire the lock, use NoLock here.  Although
+			 * a concurrent DDL may have dropped or renamed the relation,
+			 * RangeVarGetRelid() with NoLock does not check for invalidation
+			 * messages.
+			 */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			pgstat_report_skipped_vacuum_analyze(relid,
+												 vacuum_relation_skip_locked_flags(options));
+
 			return vacrels;
 		}
 
@@ -2385,6 +2430,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		 * lock on the main relation that prevents concurrent deletion.
 		 */
 		toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
+		toast_vacuum_params.options &= ~VACOPT_ANALYZE;
 		toast_vacuum_params.toast_parent = relid;
 
 		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 5c70543ba88..e36fa0c4600 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -420,6 +423,114 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum and/or analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	Form_pg_class classForm;
+	bool		isshared;
+	char		relkind;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	classForm = (Form_pg_class) GETSTRUCT(classTup);
+	isshared = classForm->relisshared;
+	relkind = classForm->relkind;
+	ReleaseSysCache(classTup);
+
+	/*
+	 * Only track skipped vacuum/analyze activity for relkinds exposed by
+	 * pg_stat_all_tables.
+	 */
+	if (relkind != RELKIND_RELATION &&
+		relkind != RELKIND_MATVIEW &&
+		relkind != RELKIND_TOASTVALUE &&
+		relkind != RELKIND_PARTITIONED_TABLE)
+		flags &= ~(PGSTAT_REPORT_LOCK_SKIPPED_VACUUM |
+				   PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM);
+
+	if (relkind != RELKIND_RELATION &&
+		relkind != RELKIND_MATVIEW &&
+		relkind != RELKIND_PARTITIONED_TABLE)
+		flags &= ~(PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE |
+				   PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE);
+
+	if (!flags)
+		return;
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_VACUUM)
+	{
+		tabentry->last_lock_skipped_vacuum_time = ts;
+		tabentry->lock_skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_lock_skipped_autovacuum_time = ts;
+		tabentry->lock_skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE)
+	{
+		tabentry->last_lock_skipped_analyze_time = ts;
+		tabentry->lock_skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_lock_skipped_autoanalyze_time = ts;
+		tabentry->lock_skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.  This
+	 * has to happen after releasing the stats entry lock, because resolving a
+	 * catcache miss may need to access pg_class and wait for a heavyweight
+	 * lock while still holding that LWLock, which risks deadlock.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid, true);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 64b6f60516c..844a023debd 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_lock_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_analyze_count)
+
+/* pg_stat_get_lock_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autoanalyze_count)
+
+/* pg_stat_get_lock_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autovacuum_count)
+
+/* pg_stat_get_lock_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -204,6 +216,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_lock_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_analyze_time)
+
+/* pg_stat_get_last_lock_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_lock_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autovacuum_time)
+
+/* pg_stat_get_last_lock_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f46427258e3..eaea44ce992 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5675,6 +5675,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last manual vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last auto vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last manual analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last auto analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of manual vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of auto vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of manual analyze skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of auto analyze skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 187d82c96fe..1a0f04a3cf8 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -536,6 +536,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_lock_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter lock_skipped_vacuum_count;
+	TimestampTz last_lock_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter lock_skipped_autovacuum_count;
+	TimestampTz last_lock_skipped_analyze_time; /* user initiated */
+	PgStat_Counter lock_skipped_analyze_count;
+	TimestampTz last_lock_skipped_autoanalyze_time; /* autovacuum initiated */
+	PgStat_Counter lock_skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -787,6 +796,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_report_skipped_vacuum_analyze() */
+#define PGSTAT_REPORT_LOCK_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+															 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..3712ea311e4 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +35,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_share analyze_specified commit
+
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +61,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_share analyze_all_parts commit
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +86,23 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +112,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_all_parts commit
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +137,23 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_full_specified commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +163,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_share vac_full_all_parts commit
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +188,23 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +214,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +239,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +265,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +291,23 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            1|                         0
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +317,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +343,23 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           1|                        0|            1|                         0
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +369,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +394,38 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_view vac_analyze_view commit check_view_stat
+step lock_view: 
+	BEGIN;
+	LOCK TABLE partv IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "partv" --- lock not available
+step vac_analyze_view: VACUUM (ANALYZE, SKIP_LOCKED) partv;
+step commit: 
+	COMMIT;
+
+step check_view_stat: 
+	SELECT pg_stat_get_lock_skipped_vacuum_count('partv'::regclass) AS vac_skip,
+		   pg_stat_get_lock_skipped_analyze_count('partv'::regclass) AS anl_skip;
+
+vac_skip|anl_skip
+--------+--------
+       0|       0
+(1 row)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..b03ea89c039 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -10,10 +10,12 @@ setup
 	ALTER TABLE part1 SET (autovacuum_enabled = false);
 	CREATE TABLE part2 PARTITION OF parted FOR VALUES IN (2);
 	ALTER TABLE part2 SET (autovacuum_enabled = false);
+	CREATE VIEW partv AS SELECT * FROM part1;
 }
 
 teardown
 {
+	DROP VIEW IF EXISTS partv;
 	DROP TABLE IF EXISTS parted;
 }
 
@@ -28,11 +30,31 @@ step lock_access_exclusive
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
 }
+step lock_view
+{
+	BEGIN;
+	LOCK TABLE partv IN ACCESS EXCLUSIVE MODE;
+}
 step commit
 {
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+}
+step check_view_stat
+{
+	SELECT pg_stat_get_lock_skipped_vacuum_count('partv'::regclass) AS vac_skip,
+		   pg_stat_get_lock_skipped_analyze_count('partv'::regclass) AS anl_skip;
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -40,22 +62,24 @@ step analyze_specified		{ ANALYZE (SKIP_LOCKED) part1, part2; }
 step analyze_all_parts		{ ANALYZE (SKIP_LOCKED) parted; }
 step vac_analyze_specified	{ VACUUM (ANALYZE, SKIP_LOCKED) part1, part2; }
 step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
+step vac_analyze_view		{ VACUUM (ANALYZE, SKIP_LOCKED) partv; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
+permutation lock_view vac_analyze_view commit check_view_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 4a8cc759d7b..a436fd409d0 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_lock_skipped_vacuum_time(c.oid) AS last_lock_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_lock_skipped_autovacuum_time(c.oid) AS last_lock_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_lock_skipped_analyze_time(c.oid) AS last_lock_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_lock_skipped_autoanalyze_time(c.oid) AS last_lock_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_lock_skipped_vacuum_count(c.oid) AS lock_skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_lock_skipped_autovacuum_count(c.oid) AS lock_skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_lock_skipped_analyze_count(c.oid) AS lock_skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_lock_skipped_autoanalyze_count(c.oid) AS lock_skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2358,13 +2366,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2413,13 +2429,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.50.1



  [application/octet-stream] v16-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch (7.9K, ../../CAN12+YLDPuW8iF=UqXyLzmjM9MkHbx2YwwKq+8G2-HcO8Kzyfg@mail.gmail.com/3-v16-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch)
  download | inline diff:
From 8000f528cc43f64310210a45b2e569aa771df758 Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 24 Jul 2026 22:45:53 +0900
Subject: [PATCH v16 2/2] Add injection point test for vacuum skip_locked stats

Add an isolation test exercising the race window between VACUUM
(SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops.

Two scenarios are tested:

1. Table dropped (committed) while vacuumer is blocked at the injection
   point: no orphaned stats entry is created.
2. DROP TABLE rolled back while vacuumer is blocked: skip is still
   recorded since the table and its stats entry survive.
---
 src/backend/utils/activity/pgstat_relation.c  |  2 +
 src/test/modules/injection_points/Makefile    |  3 +-
 .../expected/vacuum_skip_lock_stats.out       | 93 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/vacuum_skip_lock_stats.spec         | 68 ++++++++++++++
 5 files changed, 166 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
 create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec

diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index e36fa0c4600..4265524bf51 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -21,6 +21,7 @@
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
@@ -448,6 +449,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 	isshared = classForm->relisshared;
 	relkind = classForm->relkind;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("skipped-vacuum-analyze-before-entry-lock", NULL);
 
 	/*
 	 * Only track skipped vacuum/analyze activity for relkinds exposed by
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 9d8b4b3540c..e2b91ab0e4c 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -27,7 +27,8 @@ ISOLATION = basic \
 	    syscache-update-pruned \
 	    wait_cleanup \
 	    heap_lock_update \
-	    on_conflict_probe_window
+	    on_conflict_probe_window \
+	    vacuum_skip_lock_stats
 
 # some isolation tests require wal_level=replica
 ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf
diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..0efed3db2db
--- /dev/null
+++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
@@ -0,0 +1,93 @@
+Parsed test spec with 3 sessions
+
+starting permutation: lock vacuum unlock drop_table wakeup noop check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step drop_table: DROP TABLE test_skip;
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step noop: 
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         0
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+
+starting permutation: lock vacuum unlock rollback_drop wakeup noop check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step rollback_drop: 
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step noop: 
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         1
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 80a09f34d78..574c151a44d 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -57,6 +57,7 @@ tests += {
       'wait_cleanup',
       'heap_lock_update',
       'on_conflict_probe_window',
+      'vacuum_skip_lock_stats',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
new file mode 100644
index 00000000000..b277b1e80f7
--- /dev/null
+++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
@@ -0,0 +1,68 @@
+# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting
+# and concurrent DROP TABLE.
+#
+# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped
+# statistics via pgstat_report_skipped_vacuum_analyze().  An injection
+# point after the syscache lookup but before the stats update allows us
+# to verify that a concurrent DROP does not leave orphaned stats entries.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE test_skip (id int);
+	INSERT INTO test_skip VALUES (1);
+	ANALYZE test_skip;
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE saved_oid (oid_val oid);
+	INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip';
+}
+
+teardown
+{
+	DROP TABLE IF EXISTS test_skip;
+	DROP TABLE IF EXISTS saved_oid;
+	DROP EXTENSION injection_points;
+}
+
+# s1: holds the lock so VACUUM skips the table
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+}
+step unlock	{ COMMIT; }
+
+# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip
+session s2
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('skipped-vacuum-analyze-before-entry-lock', 'wait');
+}
+step vacuum	{ VACUUM (SKIP_LOCKED) test_skip; }
+step noop {}
+step detach	{ SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock'); }
+
+# s3: drops table or wakes up the vacuumer
+session s3
+step drop_table	{ DROP TABLE test_skip; }
+step rollback_drop
+{
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+}
+step wakeup	{ SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock'); }
+step check_stats
+{
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+}
+
+# Table dropped while vacuumer is blocked: no orphaned stats entry.
+permutation lock vacuum(wakeup) unlock drop_table wakeup noop check_stats detach
+
+# DROP rolled back while vacuumer is blocked: skip is still recorded.
+permutation lock vacuum(wakeup) unlock rollback_drop wakeup noop check_stats detach
-- 
2.50.1



^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-09-18 13:05  Yugo Nagata <nagata@sraoss.co.jp>
  parent: Sami Imseih <samimseih.pg@gmail.com>
  0 siblings, 1 reply; 45+ messages in thread

From: Yugo Nagata @ 2026-09-18 13:05 UTC (permalink / raw)
  To: Sami Imseih <samimseih.pg@gmail.com>; +Cc: Shinya Kato <shinya11.kato@gmail.com>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Michael Paquier <michael@paquier.xyz>

On Wed, 16 Sep 2026 10:30:44 -0500
Sami Imseih <samimseih.pg@gmail.com> wrote:

> > On Tue, Sep 15, 2026 at 4:43 AM Sami Imseih <samimseih.pg@gmail.com> wrote:
> > > This is less about stats reporting and more about filtering out such
> > > relations earlier in expand_vacuum_rel() for manually specified
> > > VACUUM/ANALYZE targets. Not a big thing, but it would avoid an extra
> > > syscache lookup and a misleading "lock not available" warning.
> >
> > I would like to fold one more case into that follow-up. The skip is
> > reported before vacuum_is_permitted_for_relation(), so a user with no
> > rights on the table can still move its counters.
> >
> > The callback argument of RangeVarGetRelidExtended() looks like the
> > right place for all of this. It runs before the lock attempt, and the
> > comment there already says that is where permission checks belong. It
> > also hands us relId, so expand_vacuum_rel() would no longer need the
> > second RangeVarGetRelid(..., NoLock, true) lookup.
> 
> It may be worth cleaning this stuff up if someone is interested in taking
> this on.
> 
> v16 is just a rebase. I also took out the catversion change for now, but made
> it clear it's required in the commit message.

Thank you for updating the patch.
I'm fine with the changes you made incorporating Kato-san's suggestions.

One minor comment is:

 		toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
+		toast_vacuum_params.options &= ~VACOPT_ANALYZE;
 		toast_vacuum_params.toast_parent = relid;

I wonder if this change is still necessary now that the filtering for TOAST
tables has been added to pgstat_report_skipped_vacuum_analyze().

If we keep this change, should we also add a comment explaining
why it is needed?

Regards,
Yugo Nagata

-- 
Yugo Nagata <nagata@sraoss.co.jp>






^ permalink  raw  reply  [nested|flat] 45+ messages in thread

* Re: Track skipped tables during autovacuum and autoanalyze
@ 2026-09-18 14:52  Sami Imseih <samimseih.pg@gmail.com>
  parent: Yugo Nagata <nagata@sraoss.co.jp>
  0 siblings, 0 replies; 45+ messages in thread

From: Sami Imseih @ 2026-09-18 14:52 UTC (permalink / raw)
  To: Yugo Nagata <nagata@sraoss.co.jp>; +Cc: Shinya Kato <shinya11.kato@gmail.com>; Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Michael Paquier <michael@paquier.xyz>

> One minor comment is:
>
>                 toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
> +               toast_vacuum_params.options &= ~VACOPT_ANALYZE;
>                 toast_vacuum_params.toast_parent = relid;
>
> I wonder if this change is still necessary now that the filtering for TOAST
> tables has been added to pgstat_report_skipped_vacuum_analyze().
>
> If we keep this change, should we also add a comment explaining
> why it is needed?

Right. I removed it.

--
Sami

Attachments:

  [application/octet-stream] v17-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch (45.9K, ../../CAN12+YLZZVc=UMi0-gDNx=0UJ2PUdhzuRiXijPZuHaQ3S4raug@mail.gmail.com/2-v17-0001-Track-skipped-vacuum-and-analyze-activity-per-re.patch)
  download | inline diff:
From 710c48c39dbfeb8d3e60aab0486bc769f8fb8d93 Mon Sep 17 00:00:00 2001
From: Yugo Nagata <nagata@sraoss.co.jp>
Date: Tue, 24 Mar 2026 13:09:00 +0900
Subject: [PATCH v17 1/2] Track skipped vacuum and analyze activity per
 relation

This commit adds eight fields to the relation statistics that track
the last time vacuum or analyze has been attempted but skipped due to
lock unavailability, along with their counts:

-    last_lock_skipped_vacuum
-    last_lock_skipped_autovacuum
-    last_lock_skipped_analyze
-    last_lock_skipped_autoanalyze
-    lock_skipped_vacuum_count
-    lock_skipped_autovacuum_count
-    lock_skipped_analyze_count
-    lock_skipped_autoanalyze_count

The field names make explicit that the skip is due to lock
unavailability, distinguishing it from other reasons a vacuum or
analyze might not run on a relation.

These fields can help users confirm that autovacuum is actively attempting
to run on a table that has not been vacuumed or analyzed for a long time,
and that the lack of progress is due to repeated skips rather than inactivity.

Because this patch adds new built-in statistics functions and extends
pg_stat_all_tables with new columns.

Catalog version bump required
---
 doc/src/sgml/monitoring.sgml                  |  93 ++++++
 src/backend/catalog/system_views.sql          |   8 +
 src/backend/commands/vacuum.c                 |  45 +++
 src/backend/utils/activity/pgstat_relation.c  | 111 +++++++
 src/backend/utils/adt/pgstatfuncs.c           |  24 ++
 src/include/catalog/pg_proc.dat               |  32 ++
 src/include/pgstat.h                          |  17 +
 .../isolation/expected/vacuum-skip-locked.out | 296 ++++++++++++++++--
 .../isolation/specs/vacuum-skip-locked.spec   |  56 +++-
 src/test/regress/expected/rules.out           |  24 ++
 10 files changed, 672 insertions(+), 34 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 62dadf3e86c..45576998cc5 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -4679,6 +4679,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_vacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual vacuum on this table using
+       <literal>SKIP_LOCKED</literal> was attempted but skipped due to lock
+       unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autovacuum</structfield> <type>timestamp with time zone</type>
@@ -4689,6 +4700,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autovacuum</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a vacuum on this table by the autovacuum daemon was attempted
+       but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_analyze</structfield> <type>timestamp with time zone</type>
@@ -4698,6 +4719,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_analyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time a manual analyze on this table using
+       <literal>SKIP_LOCKED</literal> was attempted but skipped due to lock
+       unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>last_autoanalyze</structfield> <type>timestamp with time zone</type>
@@ -4708,6 +4740,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>last_lock_skipped_autoanalyze</structfield> <type>timestamp with time zone</type>
+      </para>
+      <para>
+       Last time an analyze on this table by the autovacuum daemon was
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>vacuum_count</structfield> <type>bigint</type>
@@ -4718,6 +4760,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_vacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual vacuums on this table using
+       <literal>SKIP_LOCKED</literal> have been attempted but skipped due to
+       lock unavailability (not counting <command>VACUUM FULL</command>)
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autovacuum_count</structfield> <type>bigint</type>
@@ -4728,6 +4781,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autovacuum_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times vacuums on this table by the autovacuum daemon have been
+       attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>analyze_count</structfield> <type>bigint</type>
@@ -4737,6 +4800,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_analyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times manual analyzes on this table using
+       <literal>SKIP_LOCKED</literal> have been attempted but skipped due to
+       lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>autoanalyze_count</structfield> <type>bigint</type>
@@ -4747,6 +4821,16 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>lock_skipped_autoanalyze_count</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of times analyzes on this table by the autovacuum daemon have
+       been attempted but skipped due to lock unavailability
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>total_vacuum_time</structfield> <type>double precision</type>
@@ -4802,6 +4886,15 @@ description | Waiting for a newly initialized WAL file to reach durable storage
    </tgroup>
   </table>
 
+  <note>
+   <para>
+    When a manual vacuum or analyze on a parent table in an inheritance or
+    partitioning hierarchy is skipped before its child relations are
+    processed, the statistics are recorded only for the parent table, not
+    for its children.
+   </para>
+  </note>
+
  </sect2>
 
  <sect2 id="monitoring-pg-stat-autovacuum-scores-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index ad340887f54..6435a6c0aaa 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -736,13 +736,21 @@ CREATE VIEW pg_stat_all_tables AS
             pg_stat_get_mod_since_analyze(C.oid) AS n_mod_since_analyze,
             pg_stat_get_ins_since_vacuum(C.oid) AS n_ins_since_vacuum,
             pg_stat_get_last_vacuum_time(C.oid) as last_vacuum,
+            pg_stat_get_last_lock_skipped_vacuum_time(C.oid) as last_lock_skipped_vacuum,
             pg_stat_get_last_autovacuum_time(C.oid) as last_autovacuum,
+            pg_stat_get_last_lock_skipped_autovacuum_time(C.oid) as last_lock_skipped_autovacuum,
             pg_stat_get_last_analyze_time(C.oid) as last_analyze,
+            pg_stat_get_last_lock_skipped_analyze_time(C.oid) as last_lock_skipped_analyze,
             pg_stat_get_last_autoanalyze_time(C.oid) as last_autoanalyze,
+            pg_stat_get_last_lock_skipped_autoanalyze_time(C.oid) as last_lock_skipped_autoanalyze,
             pg_stat_get_vacuum_count(C.oid) AS vacuum_count,
+            pg_stat_get_lock_skipped_vacuum_count(C.oid) AS lock_skipped_vacuum_count,
             pg_stat_get_autovacuum_count(C.oid) AS autovacuum_count,
+            pg_stat_get_lock_skipped_autovacuum_count(C.oid) AS lock_skipped_autovacuum_count,
             pg_stat_get_analyze_count(C.oid) AS analyze_count,
+            pg_stat_get_lock_skipped_analyze_count(C.oid) AS lock_skipped_analyze_count,
             pg_stat_get_autoanalyze_count(C.oid) AS autoanalyze_count,
+            pg_stat_get_lock_skipped_autoanalyze_count(C.oid) AS lock_skipped_autoanalyze_count,
             pg_stat_get_total_vacuum_time(C.oid) AS total_vacuum_time,
             pg_stat_get_total_autovacuum_time(C.oid) AS total_autovacuum_time,
             pg_stat_get_total_analyze_time(C.oid) AS total_analyze_time,
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index d8c2f33c615..7201b458c23 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -132,6 +132,7 @@ static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
+static int	vacuum_relation_skip_locked_flags(uint32 options);
 
 /*
  * GUC check function to ensure GUC value specified is within the allowable
@@ -154,6 +155,33 @@ check_vacuum_buffer_usage_limit(int *newval, void **extra,
 	return false;
 }
 
+/*
+ * Return skipped-stats flags for a lock skip.
+ */
+static int
+vacuum_relation_skip_locked_flags(uint32 options)
+{
+	int			flags = 0;
+
+	if ((options & VACOPT_VACUUM) != 0 && (options & VACOPT_FULL) == 0)
+	{
+		if (AmAutoVacuumWorkerProcess())
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM;
+		else
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_VACUUM;
+	}
+
+	if ((options & VACOPT_ANALYZE) != 0)
+	{
+		if (AmAutoVacuumWorkerProcess())
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE;
+		else
+			flags |= PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE;
+	}
+
+	return flags;
+}
+
 /*
  * Primary entry point for manual VACUUM and ANALYZE commands
  *
@@ -815,6 +843,9 @@ vacuum_open_relation(Oid relid, RangeVar *relation, uint32 options,
 	{
 		rel = NULL;
 		rel_lock = false;
+
+		pgstat_report_skipped_vacuum_analyze(relid,
+											 vacuum_relation_skip_locked_flags(options));
 	}
 
 	/* if relation is opened, leave */
@@ -960,6 +991,20 @@ expand_vacuum_rel(VacuumRelation *vrel, MemoryContext vac_context,
 						(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
 						 errmsg("skipping analyze of \"%s\" --- lock not available",
 								vrel->relation->relname)));
+
+			/*
+			 * Get relid for statistics reporting.
+			 *
+			 * Since we failed to acquire the lock, use NoLock here.  Although
+			 * a concurrent DDL may have dropped or renamed the relation,
+			 * RangeVarGetRelid() with NoLock does not check for invalidation
+			 * messages.
+			 */
+			relid = RangeVarGetRelid(vrel->relation, NoLock, true);
+
+			pgstat_report_skipped_vacuum_analyze(relid,
+												 vacuum_relation_skip_locked_flags(options));
+
 			return vacrels;
 		}
 
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index 5c70543ba88..e36fa0c4600 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -17,12 +17,15 @@
 
 #include "postgres.h"
 
+#include "access/htup_details.h"
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 
@@ -420,6 +423,114 @@ pgstat_report_analyze(Relation rel,
 	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
 }
 
+/*
+ * Report that the table was skipped during vacuum and/or analyze.
+ */
+void
+pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Relation *shtabentry;
+	PgStat_StatTabEntry *tabentry;
+	TimestampTz ts;
+	HeapTuple	classTup;
+	Form_pg_class classForm;
+	bool		isshared;
+	char		relkind;
+
+	if (!pgstat_track_counts || !flags)
+		return;
+
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+		return;					/* somebody deleted the rel, forget it */
+	classForm = (Form_pg_class) GETSTRUCT(classTup);
+	isshared = classForm->relisshared;
+	relkind = classForm->relkind;
+	ReleaseSysCache(classTup);
+
+	/*
+	 * Only track skipped vacuum/analyze activity for relkinds exposed by
+	 * pg_stat_all_tables.
+	 */
+	if (relkind != RELKIND_RELATION &&
+		relkind != RELKIND_MATVIEW &&
+		relkind != RELKIND_TOASTVALUE &&
+		relkind != RELKIND_PARTITIONED_TABLE)
+		flags &= ~(PGSTAT_REPORT_LOCK_SKIPPED_VACUUM |
+				   PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM);
+
+	if (relkind != RELKIND_RELATION &&
+		relkind != RELKIND_MATVIEW &&
+		relkind != RELKIND_PARTITIONED_TABLE)
+		flags &= ~(PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE |
+				   PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE);
+
+	if (!flags)
+		return;
+
+	/* Store the data in the table's hash table entry. */
+	ts = GetCurrentTimestamp();
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_RELATION,
+											isshared ? InvalidOid : MyDatabaseId,
+											relid, false);
+
+	shtabentry = (PgStatShared_Relation *) entry_ref->shared_stats;
+	tabentry = &shtabentry->stats;
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_VACUUM)
+	{
+		tabentry->last_lock_skipped_vacuum_time = ts;
+		tabentry->lock_skipped_vacuum_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM)
+	{
+		tabentry->last_lock_skipped_autovacuum_time = ts;
+		tabentry->lock_skipped_autovacuum_count++;
+	}
+
+	if (flags & PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE)
+	{
+		tabentry->last_lock_skipped_analyze_time = ts;
+		tabentry->lock_skipped_analyze_count++;
+	}
+	else if (flags & PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE)
+	{
+		tabentry->last_lock_skipped_autoanalyze_time = ts;
+		tabentry->lock_skipped_autoanalyze_count++;
+	}
+
+	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Re-check whether the relation still exists.  Unlike the normal stats
+	 * reporting path, we don't hold a relation lock here (the whole point of
+	 * SKIP_LOCKED is that we failed to acquire one).  Without a lock, a
+	 * concurrent DROP TABLE could commit between our earlier syscache lookup
+	 * and the stats entry creation above, leaving an orphaned stats entry.
+	 *
+	 * Accept cache invalidation messages first, so that a DROP that committed
+	 * after our earlier check is visible to the syscache lookup below.  This
+	 * has to happen after releasing the stats entry lock, because resolving a
+	 * catcache miss may need to access pg_class and wait for a heavyweight
+	 * lock while still holding that LWLock, which risks deadlock.
+	 */
+	AcceptInvalidationMessages();
+	classTup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(classTup))
+	{
+		pgstat_drop_entry(PGSTAT_KIND_RELATION,
+						  isshared ? InvalidOid : MyDatabaseId, relid, true);
+		return;
+	}
+	ReleaseSysCache(classTup);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io(false);
+	(void) pgstat_flush_backend(false, PGSTAT_BACKEND_FLUSH_IO);
+}
+
 /*
  * count a tuple insertion of n tuples
  */
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 64b6f60516c..844a023debd 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -84,6 +84,18 @@ PG_STAT_GET_RELENTRY_INT64(mod_since_analyze)
 /* pg_stat_get_numscans */
 PG_STAT_GET_RELENTRY_INT64(numscans)
 
+/* pg_stat_get_lock_skipped_analyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_analyze_count)
+
+/* pg_stat_get_lock_skipped_autoanalyze_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autoanalyze_count)
+
+/* pg_stat_get_lock_skipped_autovacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_autovacuum_count)
+
+/* pg_stat_get_lock_skipped_vacuum_count */
+PG_STAT_GET_RELENTRY_INT64(lock_skipped_vacuum_count)
+
 /* pg_stat_get_tuples_deleted */
 PG_STAT_GET_RELENTRY_INT64(tuples_deleted)
 
@@ -204,6 +216,18 @@ PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_vacuum_time)
 /* pg_stat_get_lastscan */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(lastscan)
 
+/* pg_stat_get_last_lock_skipped_analyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_analyze_time)
+
+/* pg_stat_get_last_lock_skipped_autoanalyze_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autoanalyze_time)
+
+/* pg_stat_get_last_lock_skipped_autovacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_autovacuum_time)
+
+/* pg_stat_get_last_lock_skipped_vacuum_time */
+PG_STAT_GET_RELENTRY_TIMESTAMPTZ(last_lock_skipped_vacuum_time)
+
 /* pg_stat_get_stat_reset_time */
 PG_STAT_GET_RELENTRY_TIMESTAMPTZ(stat_reset_time)
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f46427258e3..eaea44ce992 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5675,6 +5675,38 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
   proargnames => '{oid,score,xid_score,mxid_score,vacuum_score,vacuum_insert_score,analyze_score,do_vacuum,do_analyze,for_wraparound}',
   prosrc => 'pg_stat_get_autovacuum_scores' },
+{ oid => '8142', descr => 'statistics: last manual vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_vacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_vacuum_time' },
+{ oid => '8143', descr => 'statistics: last auto vacuum skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autovacuum_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autovacuum_time' },
+{ oid => '8144', descr => 'statistics: last manual analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_analyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_analyze_time' },
+{ oid => '8145', descr => 'statistics: last auto analyze skip time due to lock unavailability for a table',
+  proname => 'pg_stat_get_last_lock_skipped_autoanalyze_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_last_lock_skipped_autoanalyze_time' },
+{ oid => '8146', descr => 'statistics: number of manual vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_vacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_vacuum_count' },
+{ oid => '8147', descr => 'statistics: number of auto vacuum skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autovacuum_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autovacuum_count' },
+{ oid => '8148', descr => 'statistics: number of manual analyze skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_analyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_analyze_count' },
+{ oid => '8149', descr => 'statistics: number of auto analyze skips due to lock unavailability for a table',
+  proname => 'pg_stat_get_lock_skipped_autoanalyze_count', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_lock_skipped_autoanalyze_count' },
 { oid => '1936', descr => 'statistics: currently active backend IDs',
   proname => 'pg_stat_get_backend_idset', prorows => '100', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'int4',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 187d82c96fe..1a0f04a3cf8 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -536,6 +536,15 @@ typedef struct PgStat_StatTabEntry
 	TimestampTz last_autoanalyze_time;	/* autovacuum initiated */
 	PgStat_Counter autoanalyze_count;
 
+	TimestampTz last_lock_skipped_vacuum_time;	/* user initiated vacuum */
+	PgStat_Counter lock_skipped_vacuum_count;
+	TimestampTz last_lock_skipped_autovacuum_time;	/* autovacuum initiated */
+	PgStat_Counter lock_skipped_autovacuum_count;
+	TimestampTz last_lock_skipped_analyze_time; /* user initiated */
+	PgStat_Counter lock_skipped_analyze_count;
+	TimestampTz last_lock_skipped_autoanalyze_time; /* autovacuum initiated */
+	PgStat_Counter lock_skipped_autoanalyze_count;
+
 	PgStat_Counter total_vacuum_time;	/* times in milliseconds */
 	PgStat_Counter total_autovacuum_time;
 	PgStat_Counter total_analyze_time;
@@ -787,6 +796,14 @@ extern void pgstat_report_analyze(Relation rel,
 								  PgStat_Counter livetuples, PgStat_Counter deadtuples,
 								  bool resetcounter, TimestampTz starttime);
 
+/* flags for pgstat_report_skipped_vacuum_analyze() */
+#define PGSTAT_REPORT_LOCK_SKIPPED_VACUUM		(1 << 0)	/* vacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_ANALYZE		(1 << 1)	/* analyze is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOVACUUM	(1 << 2)	/* autovacuum is skipped */
+#define PGSTAT_REPORT_LOCK_SKIPPED_AUTOANALYZE	(1 << 3)	/* autoanalyze is
+															 * skipped */
+extern void pgstat_report_skipped_vacuum_analyze(Oid relid, int flags);
+
 /*
  * If stats are enabled, but pending data hasn't been prepared yet, call
  * pgstat_assoc_relation() to do so. See its comment for why this is done
diff --git a/src/test/isolation/expected/vacuum-skip-locked.out b/src/test/isolation/expected/vacuum-skip-locked.out
index 99db281a159..3712ea311e4 100644
--- a/src/test/isolation/expected/vacuum-skip-locked.out
+++ b/src/test/isolation/expected/vacuum-skip-locked.out
@@ -1,6 +1,6 @@
 Parsed test spec with 2 sessions
 
-starting permutation: lock_share vac_specified commit
+starting permutation: lock_share vac_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -10,8 +10,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_all_parts commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -20,8 +35,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_share analyze_specified commit
+
+starting permutation: lock_share analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -31,8 +61,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_share analyze_all_parts commit
+
+starting permutation: lock_share analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -41,8 +86,23 @@ step analyze_all_parts: ANALYZE (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_specified commit
+starting permutation: lock_share vac_analyze_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -52,8 +112,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_share vac_analyze_all_parts commit
+starting permutation: lock_share vac_analyze_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -62,8 +137,23 @@ step vac_analyze_all_parts: VACUUM (ANALYZE, SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
 
-starting permutation: lock_share vac_full_specified commit
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_share vac_full_specified commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -73,8 +163,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_share vac_full_all_parts commit
+
+starting permutation: lock_share vac_full_all_parts commit check_stat
 step lock_share: 
 	BEGIN;
 	LOCK part1 IN SHARE MODE;
@@ -83,8 +188,23 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_specified commit
+
+starting permutation: lock_access_exclusive vac_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -94,8 +214,23 @@ step vac_specified: VACUUM (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_all_parts commit
+
+starting permutation: lock_access_exclusive vac_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -104,8 +239,23 @@ step vac_all_parts: VACUUM (SKIP_LOCKED) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         0
+part2  |           1|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive analyze_specified commit
+
+starting permutation: lock_access_exclusive analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -115,8 +265,23 @@ step analyze_specified: ANALYZE (SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         1
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
 
-starting permutation: lock_access_exclusive analyze_all_parts commit
+starting permutation: lock_access_exclusive analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -126,8 +291,23 @@ step commit:
 	COMMIT;
 
 step analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_analyze_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            1|                         0
+part2  |           0|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_analyze_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -137,8 +317,23 @@ step vac_analyze_specified: VACUUM (ANALYZE, SKIP_LOCKED) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        1|            0|                         1
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_analyze_all_parts commit
+
+starting permutation: lock_access_exclusive vac_analyze_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -148,8 +343,23 @@ step commit:
 	COMMIT;
 
 step vac_analyze_all_parts: <... completed>
-
-starting permutation: lock_access_exclusive vac_full_specified commit
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           1|                        0|            1|                         0
+part2  |           1|                        0|            1|                         0
+parted |           0|                        0|            1|                         0
+(3 rows)
+
+
+starting permutation: lock_access_exclusive vac_full_specified commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -159,8 +369,23 @@ step vac_full_specified: VACUUM (SKIP_LOCKED, FULL) part1, part2;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
 
-starting permutation: lock_access_exclusive vac_full_all_parts commit
+
+starting permutation: lock_access_exclusive vac_full_all_parts commit check_stat
 step lock_access_exclusive: 
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
@@ -169,3 +394,38 @@ step vac_full_all_parts: VACUUM (SKIP_LOCKED, FULL) parted;
 step commit: 
 	COMMIT;
 
+step check_stat: 
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+
+relname|vacuum_count|lock_skipped_vacuum_count|analyze_count|lock_skipped_analyze_count
+-------+------------+-------------------------+-------------+--------------------------
+part1  |           0|                        0|            0|                         0
+part2  |           0|                        0|            0|                         0
+parted |           0|                        0|            0|                         0
+(3 rows)
+
+
+starting permutation: lock_view vac_analyze_view commit check_view_stat
+step lock_view: 
+	BEGIN;
+	LOCK TABLE partv IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "partv" --- lock not available
+step vac_analyze_view: VACUUM (ANALYZE, SKIP_LOCKED) partv;
+step commit: 
+	COMMIT;
+
+step check_view_stat: 
+	SELECT pg_stat_get_lock_skipped_vacuum_count('partv'::regclass) AS vac_skip,
+		   pg_stat_get_lock_skipped_analyze_count('partv'::regclass) AS anl_skip;
+
+vac_skip|anl_skip
+--------+--------
+       0|       0
+(1 row)
+
diff --git a/src/test/isolation/specs/vacuum-skip-locked.spec b/src/test/isolation/specs/vacuum-skip-locked.spec
index 3fad6e1c92a..b03ea89c039 100644
--- a/src/test/isolation/specs/vacuum-skip-locked.spec
+++ b/src/test/isolation/specs/vacuum-skip-locked.spec
@@ -10,10 +10,12 @@ setup
 	ALTER TABLE part1 SET (autovacuum_enabled = false);
 	CREATE TABLE part2 PARTITION OF parted FOR VALUES IN (2);
 	ALTER TABLE part2 SET (autovacuum_enabled = false);
+	CREATE VIEW partv AS SELECT * FROM part1;
 }
 
 teardown
 {
+	DROP VIEW IF EXISTS partv;
 	DROP TABLE IF EXISTS parted;
 }
 
@@ -28,11 +30,31 @@ step lock_access_exclusive
 	BEGIN;
 	LOCK part1 IN ACCESS EXCLUSIVE MODE;
 }
+step lock_view
+{
+	BEGIN;
+	LOCK TABLE partv IN ACCESS EXCLUSIVE MODE;
+}
 step commit
 {
 	COMMIT;
 }
 
+step check_stat
+{
+	SELECT relname,
+		   vacuum_count, lock_skipped_vacuum_count,
+		   analyze_count, lock_skipped_analyze_count
+	FROM pg_stat_all_tables
+	WHERE relname IN ('parted', 'part1', 'part2')
+	ORDER BY relname;
+}
+step check_view_stat
+{
+	SELECT pg_stat_get_lock_skipped_vacuum_count('partv'::regclass) AS vac_skip,
+		   pg_stat_get_lock_skipped_analyze_count('partv'::regclass) AS anl_skip;
+}
+
 session s2
 step vac_specified			{ VACUUM (SKIP_LOCKED) part1, part2; }
 step vac_all_parts			{ VACUUM (SKIP_LOCKED) parted; }
@@ -40,22 +62,24 @@ step analyze_specified		{ ANALYZE (SKIP_LOCKED) part1, part2; }
 step analyze_all_parts		{ ANALYZE (SKIP_LOCKED) parted; }
 step vac_analyze_specified	{ VACUUM (ANALYZE, SKIP_LOCKED) part1, part2; }
 step vac_analyze_all_parts	{ VACUUM (ANALYZE, SKIP_LOCKED) parted; }
+step vac_analyze_view		{ VACUUM (ANALYZE, SKIP_LOCKED) partv; }
 step vac_full_specified		{ VACUUM (SKIP_LOCKED, FULL) part1, part2; }
 step vac_full_all_parts		{ VACUUM (SKIP_LOCKED, FULL) parted; }
 
-permutation lock_share vac_specified commit
-permutation lock_share vac_all_parts commit
-permutation lock_share analyze_specified commit
-permutation lock_share analyze_all_parts commit
-permutation lock_share vac_analyze_specified commit
-permutation lock_share vac_analyze_all_parts commit
-permutation lock_share vac_full_specified commit
-permutation lock_share vac_full_all_parts commit
-permutation lock_access_exclusive vac_specified commit
-permutation lock_access_exclusive vac_all_parts commit
-permutation lock_access_exclusive analyze_specified commit
-permutation lock_access_exclusive analyze_all_parts commit
-permutation lock_access_exclusive vac_analyze_specified commit
-permutation lock_access_exclusive vac_analyze_all_parts commit
-permutation lock_access_exclusive vac_full_specified commit
-permutation lock_access_exclusive vac_full_all_parts commit
+permutation lock_share vac_specified commit check_stat
+permutation lock_share vac_all_parts commit check_stat
+permutation lock_share analyze_specified commit check_stat
+permutation lock_share analyze_all_parts commit check_stat
+permutation lock_share vac_analyze_specified commit check_stat
+permutation lock_share vac_analyze_all_parts commit check_stat
+permutation lock_share vac_full_specified commit check_stat
+permutation lock_share vac_full_all_parts commit check_stat
+permutation lock_access_exclusive vac_specified commit check_stat
+permutation lock_access_exclusive vac_all_parts commit check_stat
+permutation lock_access_exclusive analyze_specified commit check_stat
+permutation lock_access_exclusive analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_analyze_specified commit check_stat
+permutation lock_access_exclusive vac_analyze_all_parts commit check_stat
+permutation lock_access_exclusive vac_full_specified commit check_stat
+permutation lock_access_exclusive vac_full_all_parts commit check_stat
+permutation lock_view vac_analyze_view commit check_view_stat
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 4a8cc759d7b..a436fd409d0 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1835,13 +1835,21 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     pg_stat_get_mod_since_analyze(c.oid) AS n_mod_since_analyze,
     pg_stat_get_ins_since_vacuum(c.oid) AS n_ins_since_vacuum,
     pg_stat_get_last_vacuum_time(c.oid) AS last_vacuum,
+    pg_stat_get_last_lock_skipped_vacuum_time(c.oid) AS last_lock_skipped_vacuum,
     pg_stat_get_last_autovacuum_time(c.oid) AS last_autovacuum,
+    pg_stat_get_last_lock_skipped_autovacuum_time(c.oid) AS last_lock_skipped_autovacuum,
     pg_stat_get_last_analyze_time(c.oid) AS last_analyze,
+    pg_stat_get_last_lock_skipped_analyze_time(c.oid) AS last_lock_skipped_analyze,
     pg_stat_get_last_autoanalyze_time(c.oid) AS last_autoanalyze,
+    pg_stat_get_last_lock_skipped_autoanalyze_time(c.oid) AS last_lock_skipped_autoanalyze,
     pg_stat_get_vacuum_count(c.oid) AS vacuum_count,
+    pg_stat_get_lock_skipped_vacuum_count(c.oid) AS lock_skipped_vacuum_count,
     pg_stat_get_autovacuum_count(c.oid) AS autovacuum_count,
+    pg_stat_get_lock_skipped_autovacuum_count(c.oid) AS lock_skipped_autovacuum_count,
     pg_stat_get_analyze_count(c.oid) AS analyze_count,
+    pg_stat_get_lock_skipped_analyze_count(c.oid) AS lock_skipped_analyze_count,
     pg_stat_get_autoanalyze_count(c.oid) AS autoanalyze_count,
+    pg_stat_get_lock_skipped_autoanalyze_count(c.oid) AS lock_skipped_autoanalyze_count,
     pg_stat_get_total_vacuum_time(c.oid) AS total_vacuum_time,
     pg_stat_get_total_autovacuum_time(c.oid) AS total_autovacuum_time,
     pg_stat_get_total_analyze_time(c.oid) AS total_analyze_time,
@@ -2358,13 +2366,21 @@ pg_stat_sys_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
@@ -2413,13 +2429,21 @@ pg_stat_user_tables| SELECT relid,
     n_mod_since_analyze,
     n_ins_since_vacuum,
     last_vacuum,
+    last_lock_skipped_vacuum,
     last_autovacuum,
+    last_lock_skipped_autovacuum,
     last_analyze,
+    last_lock_skipped_analyze,
     last_autoanalyze,
+    last_lock_skipped_autoanalyze,
     vacuum_count,
+    lock_skipped_vacuum_count,
     autovacuum_count,
+    lock_skipped_autovacuum_count,
     analyze_count,
+    lock_skipped_analyze_count,
     autoanalyze_count,
+    lock_skipped_autoanalyze_count,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-- 
2.50.1



  [application/octet-stream] v17-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch (7.9K, ../../CAN12+YLZZVc=UMi0-gDNx=0UJ2PUdhzuRiXijPZuHaQ3S4raug@mail.gmail.com/3-v17-0002-Add-injection-point-test-for-vacuum-skip_locked-.patch)
  download | inline diff:
From 4e9b9973ee2b3e407ae194fcaad14cc2ce862f17 Mon Sep 17 00:00:00 2001
From: Sami Imseih <samimseih@gmail.com>
Date: Fri, 24 Jul 2026 22:45:53 +0900
Subject: [PATCH v17 2/2] Add injection point test for vacuum skip_locked stats

Add an isolation test exercising the race window between VACUUM
(SKIP_LOCKED) reporting a skipped vacuum and concurrent table drops.

Two scenarios are tested:

1. Table dropped (committed) while vacuumer is blocked at the injection
   point: no orphaned stats entry is created.
2. DROP TABLE rolled back while vacuumer is blocked: skip is still
   recorded since the table and its stats entry survive.
---
 src/backend/utils/activity/pgstat_relation.c  |  2 +
 src/test/modules/injection_points/Makefile    |  3 +-
 .../expected/vacuum_skip_lock_stats.out       | 93 +++++++++++++++++++
 src/test/modules/injection_points/meson.build |  1 +
 .../specs/vacuum_skip_lock_stats.spec         | 68 ++++++++++++++
 5 files changed, 166 insertions(+), 1 deletion(-)
 create mode 100644 src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
 create mode 100644 src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec

diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index e36fa0c4600..4265524bf51 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -21,6 +21,7 @@
 #include "access/twophase_rmgr.h"
 #include "access/xact.h"
 #include "catalog/catalog.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/pgstat_internal.h"
@@ -448,6 +449,7 @@ pgstat_report_skipped_vacuum_analyze(Oid relid, int flags)
 	isshared = classForm->relisshared;
 	relkind = classForm->relkind;
 	ReleaseSysCache(classTup);
+	INJECTION_POINT("skipped-vacuum-analyze-before-entry-lock", NULL);
 
 	/*
 	 * Only track skipped vacuum/analyze activity for relkinds exposed by
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index 9d8b4b3540c..e2b91ab0e4c 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -27,7 +27,8 @@ ISOLATION = basic \
 	    syscache-update-pruned \
 	    wait_cleanup \
 	    heap_lock_update \
-	    on_conflict_probe_window
+	    on_conflict_probe_window \
+	    vacuum_skip_lock_stats
 
 # some isolation tests require wal_level=replica
 ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/extra.conf
diff --git a/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
new file mode 100644
index 00000000000..0efed3db2db
--- /dev/null
+++ b/src/test/modules/injection_points/expected/vacuum_skip_lock_stats.out
@@ -0,0 +1,93 @@
+Parsed test spec with 3 sessions
+
+starting permutation: lock vacuum unlock drop_table wakeup noop check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step drop_table: DROP TABLE test_skip;
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step noop: 
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         0
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
+
+starting permutation: lock vacuum unlock rollback_drop wakeup noop check_stats detach
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step lock: 
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+
+s2: WARNING:  skipping vacuum of "test_skip" --- lock not available
+step vacuum: VACUUM (SKIP_LOCKED) test_skip; <waiting ...>
+step unlock: COMMIT;
+step rollback_drop: 
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+
+step wakeup: SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock');
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step vacuum: <... completed>
+step noop: 
+step check_stats: 
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+skip_count
+----------
+         1
+(1 row)
+
+step detach: SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock');
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index 80a09f34d78..574c151a44d 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -57,6 +57,7 @@ tests += {
       'wait_cleanup',
       'heap_lock_update',
       'on_conflict_probe_window',
+      'vacuum_skip_lock_stats',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
diff --git a/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
new file mode 100644
index 00000000000..b277b1e80f7
--- /dev/null
+++ b/src/test/modules/injection_points/specs/vacuum_skip_lock_stats.spec
@@ -0,0 +1,68 @@
+# Test for race conditions between VACUUM (SKIP_LOCKED) stats reporting
+# and concurrent DROP TABLE.
+#
+# When VACUUM (SKIP_LOCKED) cannot acquire a lock, it reports skipped
+# statistics via pgstat_report_skipped_vacuum_analyze().  An injection
+# point after the syscache lookup but before the stats update allows us
+# to verify that a concurrent DROP does not leave orphaned stats entries.
+
+setup
+{
+	CREATE EXTENSION injection_points;
+	CREATE TABLE test_skip (id int);
+	INSERT INTO test_skip VALUES (1);
+	ANALYZE test_skip;
+	SELECT pg_stat_force_next_flush();
+	CREATE TABLE saved_oid (oid_val oid);
+	INSERT INTO saved_oid SELECT oid FROM pg_class WHERE relname = 'test_skip';
+}
+
+teardown
+{
+	DROP TABLE IF EXISTS test_skip;
+	DROP TABLE IF EXISTS saved_oid;
+	DROP EXTENSION injection_points;
+}
+
+# s1: holds the lock so VACUUM skips the table
+session s1
+step lock
+{
+	BEGIN;
+	LOCK TABLE test_skip IN ACCESS EXCLUSIVE MODE;
+}
+step unlock	{ COMMIT; }
+
+# s2: runs VACUUM (SKIP_LOCKED), blocks at injection point after skip
+session s2
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('skipped-vacuum-analyze-before-entry-lock', 'wait');
+}
+step vacuum	{ VACUUM (SKIP_LOCKED) test_skip; }
+step noop {}
+step detach	{ SELECT injection_points_detach('skipped-vacuum-analyze-before-entry-lock'); }
+
+# s3: drops table or wakes up the vacuumer
+session s3
+step drop_table	{ DROP TABLE test_skip; }
+step rollback_drop
+{
+	BEGIN;
+	DROP TABLE test_skip;
+	ROLLBACK;
+}
+step wakeup	{ SELECT injection_points_wakeup('skipped-vacuum-analyze-before-entry-lock'); }
+step check_stats
+{
+	SELECT pg_stat_force_next_flush();
+	SELECT pg_stat_get_lock_skipped_vacuum_count(oid_val) AS skip_count
+	FROM saved_oid;
+}
+
+# Table dropped while vacuumer is blocked: no orphaned stats entry.
+permutation lock vacuum(wakeup) unlock drop_table wakeup noop check_stats detach
+
+# DROP rolled back while vacuumer is blocked: skip is still recorded.
+permutation lock vacuum(wakeup) unlock rollback_drop wakeup noop check_stats detach
-- 
2.50.1



^ permalink  raw  reply  [nested|flat] 45+ messages in thread


end of thread, other threads:[~2026-09-18 14:52 UTC | newest]

Thread overview: 45+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-03-24 06:11 Track skipped tables during autovacuum and autoanalyze Yugo Nagata <nagata@sraoss.co.jp>
2026-03-24 14:58 ` Sami Imseih <samimseih@gmail.com>
2026-03-24 16:28   ` Yugo Nagata <nagata@sraoss.co.jp>
2026-03-25 02:07     ` Michael Paquier <michael@paquier.xyz>
2026-03-25 04:24       ` Yugo Nagata <nagata@sraoss.co.jp>
2026-03-25 17:12         ` Sami Imseih <samimseih@gmail.com>
2026-03-25 23:26           ` Michael Paquier <michael@paquier.xyz>
2026-03-26 01:18             ` Yugo Nagata <nagata@sraoss.co.jp>
2026-03-26 01:31               ` Michael Paquier <michael@paquier.xyz>
2026-03-26 02:25                 ` Yugo Nagata <nagata@sraoss.co.jp>
2026-03-26 10:22     ` Yugo Nagata <nagata@sraoss.co.jp>
2026-03-26 23:07       ` Michael Paquier <michael@paquier.xyz>
2026-03-27 07:35         ` Yugo Nagata <nagata@sraoss.co.jp>
2026-03-27 16:48           ` Sami Imseih <samimseih@gmail.com>
2026-03-28 07:18             ` Yugo Nagata <nagata@sraoss.co.jp>
2026-04-13 08:05               ` Yugo Nagata <nagata@sraoss.co.jp>
2026-04-22 12:49                 ` Sami Imseih <samimseih@gmail.com>
2026-04-27 11:32                   ` Yugo Nagata <nagata@sraoss.co.jp>
2026-05-04 20:44                     ` Sami Imseih <samimseih@gmail.com>
2026-05-12 09:47                       ` Yugo Nagata <nagata@sraoss.co.jp>
2026-05-12 14:50                         ` Sami Imseih <samimseih@gmail.com>
2026-05-14 21:46                       ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-05-15 00:59                         ` Sami Imseih <samimseih@gmail.com>
2026-05-15 05:03                           ` Yugo Nagata <nagata@sraoss.co.jp>
2026-05-15 09:02                             ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-05-15 16:12                               ` Sami Imseih <samimseih@gmail.com>
2026-05-15 20:21                                 ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-05-16 14:29                                   ` Sami Imseih <samimseih@gmail.com>
2026-07-11 08:23                                     ` Shinya Kato <shinya11.kato@gmail.com>
2026-07-21 18:12                                       ` Sami Imseih <samimseih@gmail.com>
2026-07-21 18:51                                         ` Sami Imseih <samimseih@gmail.com>
2026-07-22 04:20                                           ` Shinya Kato <shinya11.kato@gmail.com>
2026-07-24 16:48                                             ` Yugo Nagata <nagata@sraoss.co.jp>
2026-07-24 17:51                                               ` Yugo Nagata <nagata@sraoss.co.jp>
2026-07-27 04:03                                                 ` Yugo Nagata <nagata@sraoss.co.jp>
2026-07-27 22:21                                                   ` Sami Imseih <samimseih@gmail.com>
2026-07-28 14:14                                               ` Shinya Kato <shinya11.kato@gmail.com>
2026-07-28 14:27                                                 ` Yugo Nagata <nagata@sraoss.co.jp>
2026-08-12 03:00                                                   ` Shinya Kato <shinya11.kato@gmail.com>
2026-09-14 19:43                                                     ` Sami Imseih <samimseih.pg@gmail.com>
2026-09-14 19:52                                                       ` Sami Imseih <samimseih.pg@gmail.com>
2026-09-16 12:48                                                       ` Shinya Kato <shinya11.kato@gmail.com>
2026-09-16 15:30                                                         ` Sami Imseih <samimseih.pg@gmail.com>
2026-09-18 13:05                                                           ` Yugo Nagata <nagata@sraoss.co.jp>
2026-09-18 14:52                                                             ` Sami Imseih <samimseih.pg@gmail.com>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox