Hi hackers,

Sometime ago I investigated slow query performance case of one customer and noticed that index-only scan has made a lot of heap fetches.


-> Index Only Scan using ix_client_objects_vendor_object_id on client_objects client_objects_1 (cost=0.56..2.78 rows=1 width=0) (actual time=0.006..0.006 rows=1 loops=208081) Index Cond: (vendor_object_id = vendor_objects.id) Heap Fetches: 208081 Buffers: shared hit=1092452 read=156034

So almost any index entry requires visibility check and index-only scan is actually normal index-scan.
It certainly have bad impact on performance.
I do not know what happen in this particular case, why pages are not marked as all-visible and why index-only scan plan was chosen by optimizer. Butthe problem can be quite easily reproduced. We can just populate table with some data with some other transaction with assigned XID active.
Then explicitly vacuum this tables or wait until autovacuum does it.
At this moment table has no more dead or inserted tuples so autovacuum will not be called for it. But heap pages of this table are still not marked as all-visible. And will never be marked as all-visible unless table is updated or is explicitly vacuumed.

This is why I think that it may be useful to add more columns to|pg_stat_all_tables|and|pg_stat_all_indexes|views providing information about heap visibility checks performed by index-only scan. And in addition to number of dead/inserted tuples add number of such visibility checks as criteria for performing autovacuum for the particular table.

Proposed patch is attached.
I am not quit happy with the test - it is intended to check if autovacuum is really triggered by this new criteria. But it depends on autovacuum activation frequency and may take several seconds.

Will be glad to receive any feedback.
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fea683cb49c..f8dc1d04deb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8929,6 +8929,28 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH 
csv;
        </listitem>
       </varlistentry>
 
+      <varlistentry id="guc-autovacuum-vacuum-check-threshold" 
xreflabel="autovacuum_vacuum_check_threshold">
+       <term><varname>autovacuum_vacuum_check_threshold</varname> 
(<type>integer</type>)
+       <indexterm>
+        <primary><varname>autovacuum_vacuum_check_threshold</varname></primary>
+        <secondary>configuration parameter</secondary>
+       </indexterm>
+       </term>
+       <listitem>
+        <para>
+         Specifies the number of heap tuple visibility checks by index-only 
scan needed to trigger a
+         <command>VACUUM</command> in any one table.
+         The default is 1000 tuples.  If -1 is specified, autovacuum will not
+         trigger a <command>VACUUM</command> operation on any tables based on
+         the number of visibility checks.
+         This parameter can only be set in the 
<filename>postgresql.conf</filename>
+         file or on the server command line;
+         but the setting can be overridden for individual tables by
+         changing table storage parameters.
+        </para>
+       </listitem>
+      </varlistentry>
+
       <varlistentry id="guc-autovacuum-analyze-threshold" 
xreflabel="autovacuum_analyze_threshold">
        <term><varname>autovacuum_analyze_threshold</varname> 
(<type>integer</type>)
        <indexterm>
@@ -8990,6 +9012,27 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH 
csv;
        </listitem>
       </varlistentry>
 
+      <varlistentry id="guc-autovacuum-vacuum-check-scale-factor" 
xreflabel="autovacuum_vacuum_check_scale_factor">
+       <term><varname>autovacuum_vacuum_check_scale_factor</varname> 
(<type>floating point</type>)
+       <indexterm>
+        
<primary><varname>autovacuum_vacuum_check_scale_factor</varname></primary>
+        <secondary>configuration parameter</secondary>
+       </indexterm>
+       </term>
+       <listitem>
+        <para>
+         Specifies a fraction of the table size to add to
+         <varname>autovacuum_vacuum_check_threshold</varname>
+         when deciding whether to trigger a <command>VACUUM</command>.
+         The default is 0.2 (20% of table size).
+         This parameter can only be set in the 
<filename>postgresql.conf</filename>
+         file or on the server command line;
+         but the setting can be overridden for individual tables by
+         changing table storage parameters.
+        </para>
+       </listitem>
+      </varlistentry>
+
       <varlistentry id="guc-autovacuum-analyze-scale-factor" 
xreflabel="autovacuum_analyze_scale_factor">
        <term><varname>autovacuum_analyze_scale_factor</varname> 
(<type>floating point</type>)
        <indexterm>
diff --git a/src/backend/access/common/reloptions.c 
b/src/backend/access/common/reloptions.c
index 46c1dce222d..f93cf1671ed 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -1869,6 +1869,8 @@ default_reloptions(Datum reloptions, bool validate, 
relopt_kind kind)
                offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, 
vacuum_max_threshold)},
                {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
                offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, 
vacuum_ins_threshold)},
+               {"autovacuum_vacuum_check_threshold", RELOPT_TYPE_INT,
+               offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, 
vacuum_check_threshold)},
                {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
                offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, 
analyze_threshold)},
                {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
@@ -1895,6 +1897,8 @@ default_reloptions(Datum reloptions, bool validate, 
relopt_kind kind)
                offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, 
vacuum_scale_factor)},
                {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
                offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, 
vacuum_ins_scale_factor)},
+               {"autovacuum_vacuum_check_scale_factor", RELOPT_TYPE_REAL,
+               offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, 
vacuum_check_scale_factor)},
                {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
                offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, 
analyze_scale_factor)},
                {"user_catalog_table", RELOPT_TYPE_BOOL,
diff --git a/src/backend/catalog/system_views.sql 
b/src/backend/catalog/system_views.sql
index 31d269b7ee0..dfaf8e51ac6 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -700,7 +700,9 @@ CREATE VIEW pg_stat_all_tables AS
             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,
-            pg_stat_get_total_autoanalyze_time(C.oid) AS total_autoanalyze_time
+            pg_stat_get_total_autoanalyze_time(C.oid) AS 
total_autoanalyze_time,
+            pg_stat_get_check_since_vacuum(C.oid) AS n_check_since_vacuum,
+            pg_stat_get_tuples_checked(C.oid) AS n_tup_check
     FROM pg_class C LEFT JOIN
          pg_index I ON C.oid = I.indrelid
          LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
@@ -721,7 +723,8 @@ CREATE VIEW pg_stat_xact_all_tables AS
             pg_stat_get_xact_tuples_updated(C.oid) AS n_tup_upd,
             pg_stat_get_xact_tuples_deleted(C.oid) AS n_tup_del,
             pg_stat_get_xact_tuples_hot_updated(C.oid) AS n_tup_hot_upd,
-            pg_stat_get_xact_tuples_newpage_updated(C.oid) AS n_tup_newpage_upd
+            pg_stat_get_xact_tuples_newpage_updated(C.oid) AS 
n_tup_newpage_upd,
+            pg_stat_get_xact_tuples_checked(C.oid) AS n_tup_check
     FROM pg_class C LEFT JOIN
          pg_index I ON C.oid = I.indrelid
          LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
@@ -802,7 +805,8 @@ CREATE VIEW pg_stat_all_indexes AS
             pg_stat_get_numscans(I.oid) AS idx_scan,
             pg_stat_get_lastscan(I.oid) AS last_idx_scan,
             pg_stat_get_tuples_returned(I.oid) AS idx_tup_read,
-            pg_stat_get_tuples_fetched(I.oid) AS idx_tup_fetch
+            pg_stat_get_tuples_fetched(I.oid) AS idx_tup_fetch,
+            pg_stat_get_tuples_checked(I.oid) AS idx_tup_check
     FROM pg_class C JOIN
             pg_index X ON C.oid = X.indrelid JOIN
             pg_class I ON I.oid = X.indexrelid
@@ -1084,7 +1088,8 @@ CREATE VIEW pg_stat_database AS
             pg_stat_get_db_sessions_killed(D.oid) AS sessions_killed,
             pg_stat_get_db_parallel_workers_to_launch(D.oid) as 
parallel_workers_to_launch,
             pg_stat_get_db_parallel_workers_launched(D.oid) as 
parallel_workers_launched,
-            pg_stat_get_db_stat_reset_time(D.oid) AS stats_reset
+            pg_stat_get_db_stat_reset_time(D.oid) AS stats_reset,
+            pg_stat_get_db_tuples_checked(D.oid) AS tup_checked
     FROM (
         SELECT 0 AS oid, NULL::name AS datname
         UNION ALL
diff --git a/src/backend/executor/nodeIndexonlyscan.c 
b/src/backend/executor/nodeIndexonlyscan.c
index f464cca9507..321da2bcda3 100644
--- a/src/backend/executor/nodeIndexonlyscan.c
+++ b/src/backend/executor/nodeIndexonlyscan.c
@@ -29,6 +29,7 @@
  *             ExecIndexOnlyScanInitializeWorker attach to DSM info in 
parallel worker
  */
 #include "postgres.h"
+#include "pgstat.h"
 
 #include "access/genam.h"
 #include "access/relscan.h"
@@ -166,6 +167,8 @@ IndexOnlyNext(IndexOnlyScanState *node)
                         * Rats, we have to visit the heap to check visibility.
                         */
                        InstrCountTuples2(node, 1);
+                       pgstat_count_heap_check(scandesc->heapRelation);
+                       pgstat_count_heap_check(scandesc->indexRelation);
                        if (!index_fetch_heap(scandesc, node->ioss_TableSlot))
                                continue;               /* no visible tuple, 
try next index entry */
 
diff --git a/src/backend/postmaster/autovacuum.c 
b/src/backend/postmaster/autovacuum.c
index 2513a8ef8a6..05b92bf330b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -125,6 +125,8 @@ int                 autovacuum_vac_max_thresh;
 double         autovacuum_vac_scale;
 int                    autovacuum_vac_ins_thresh;
 double         autovacuum_vac_ins_scale;
+int                    autovacuum_vac_check_thresh;
+double         autovacuum_vac_check_scale;
 int                    autovacuum_anl_thresh;
 double         autovacuum_anl_scale;
 int                    autovacuum_freeze_max_age;
@@ -2945,19 +2947,23 @@ relation_needs_vacanalyze(Oid relid,
        int                     vac_base_thresh,
                                vac_max_thresh,
                                vac_ins_base_thresh,
+                               vac_check_base_thresh,
                                anl_base_thresh;
        float4          vac_scale_factor,
                                vac_ins_scale_factor,
+                               vac_check_scale_factor,
                                anl_scale_factor;
 
        /* thresholds calculated from above constants */
        float4          vacthresh,
                                vacinsthresh,
+                               vaccheckthresh,
                                anlthresh;
 
        /* number of vacuum (resp. analyze) tuples at this time */
        float4          vactuples,
                                instuples,
+                               checktuples,
                                anltuples;
 
        /* freeze parameters */
@@ -2999,6 +3005,15 @@ relation_needs_vacanalyze(Oid relid,
                ? relopts->vacuum_ins_threshold
                : autovacuum_vac_ins_thresh;
 
+       vac_check_scale_factor = (relopts && relopts->vacuum_check_scale_factor 
>= 0)
+               ? relopts->vacuum_check_scale_factor
+               : autovacuum_vac_check_scale;
+
+       /* -1 is used to disable check vacuums */
+       vac_check_base_thresh = (relopts && relopts->vacuum_check_threshold >= 
-1)
+               ? relopts->vacuum_check_threshold
+               : autovacuum_vac_check_thresh;
+
        anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
                ? relopts->analyze_scale_factor
                : autovacuum_anl_scale;
@@ -3032,7 +3047,7 @@ relation_needs_vacanalyze(Oid relid,
                if (multiForceLimit < FirstMultiXactId)
                        multiForceLimit -= FirstMultiXactId;
                force_vacuum = MultiXactIdIsValid(relminmxid) &&
-                       MultiXactIdPrecedes(relminmxid, multiForceLimit);
+               MultiXactIdPrecedes(relminmxid, multiForceLimit);
        }
        *wraparound = force_vacuum;
 
@@ -3061,6 +3076,7 @@ relation_needs_vacanalyze(Oid relid,
                vactuples = tabentry->dead_tuples;
                instuples = tabentry->ins_since_vacuum;
                anltuples = tabentry->mod_since_analyze;
+               checktuples = tabentry->check_since_vacuum;
 
                /* If the table hasn't yet been vacuumed, take reltuples as 
zero */
                if (reltuples < 0)
@@ -3089,6 +3105,7 @@ relation_needs_vacanalyze(Oid relid,
 
                vacinsthresh = (float4) vac_ins_base_thresh +
                        vac_ins_scale_factor * reltuples * pcnt_unfrozen;
+               vaccheckthresh = (float4) vac_check_base_thresh + 
vac_check_scale_factor * reltuples;
                anlthresh = (float4) anl_base_thresh + anl_scale_factor * 
reltuples;
 
                /*
@@ -3096,18 +3113,27 @@ relation_needs_vacanalyze(Oid relid,
                 * reset, because if that happens, the last vacuum and analyze 
counts
                 * will be reset too.
                 */
-               if (vac_ins_base_thresh >= 0)
-                       elog(DEBUG3, "%s: vac: %.0f (threshold %.0f), ins: %.0f 
(threshold %.0f), anl: %.0f (threshold %.0f)",
+               if (vac_ins_base_thresh >= 0 && vac_check_base_thresh >= 0)
+                       elog(DEBUG3, "%s: vac: %.0f (threshold %.0f), ins: %.0f 
(threshold %.0f), anl: %.0f (threshold %.0f), check: %.0f (threshold %.0f)",
+                                NameStr(classForm->relname),
+                                vactuples, vacthresh, instuples, vacinsthresh, 
anltuples, anlthresh, checktuples, vaccheckthresh);
+               else if (vac_ins_base_thresh >= 0)
+                       elog(DEBUG3, "%s: vac: %.0f (threshold %.0f), ins: %.0f 
(threshold %.0f), anl: %.0f (threshold %.0f), check: (disabled)",
                                 NameStr(classForm->relname),
                                 vactuples, vacthresh, instuples, vacinsthresh, 
anltuples, anlthresh);
+               else if (vac_check_base_thresh >= 0)
+                       elog(DEBUG3, "%s: vac: %.0f (threshold %.0f), ins: 
disabled), anl: %.0f (threshold %.0f), check: %.0f (threshold %.0f)",
+                                NameStr(classForm->relname),
+                                vactuples, vacthresh, anltuples, anlthresh, 
checktuples, vaccheckthresh);
                else
-                       elog(DEBUG3, "%s: vac: %.0f (threshold %.0f), ins: 
(disabled), anl: %.0f (threshold %.0f)",
+                       elog(DEBUG3, "%s: vac: %.0f (threshold %.0f), ins: 
(disabled), anl: %.0f (threshold %.0f), check: (disabled)",
                                 NameStr(classForm->relname),
                                 vactuples, vacthresh, anltuples, anlthresh);
 
                /* Determine if this table needs vacuum or analyze. */
                *dovacuum = force_vacuum || (vactuples > vacthresh) ||
-                       (vac_ins_base_thresh >= 0 && instuples > vacinsthresh);
+                       (vac_ins_base_thresh >= 0 && instuples > vacinsthresh) 
||
+                       (vac_check_base_thresh >= 0 && checktuples > 
vaccheckthresh);
                *doanalyze = (anltuples > anlthresh);
        }
        else
diff --git a/src/backend/utils/activity/pgstat_database.c 
b/src/backend/utils/activity/pgstat_database.c
index fbaf8364117..9adfbf4f87d 100644
--- a/src/backend/utils/activity/pgstat_database.c
+++ b/src/backend/utils/activity/pgstat_database.c
@@ -449,6 +449,7 @@ pgstat_database_flush_cb(PgStat_EntryRef *entry_ref, bool 
nowait)
        PGSTAT_ACCUM_DBCOUNT(tuples_inserted);
        PGSTAT_ACCUM_DBCOUNT(tuples_updated);
        PGSTAT_ACCUM_DBCOUNT(tuples_deleted);
+       PGSTAT_ACCUM_DBCOUNT(tuples_checked);
 
        /* last_autovac_time is reported immediately */
        Assert(pendingent->last_autovac_time == 0);
diff --git a/src/backend/utils/activity/pgstat_relation.c 
b/src/backend/utils/activity/pgstat_relation.c
index d64595a165c..1cca4e8c4f6 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -246,6 +246,7 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
         * stragglers.
         */
        tabentry->ins_since_vacuum = 0;
+       tabentry->check_since_vacuum = 0;
 
        if (AmAutoVacuumWorkerProcess())
        {
@@ -852,6 +853,7 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool 
nowait)
        tabentry->tuples_inserted += lstats->counts.tuples_inserted;
        tabentry->tuples_updated += lstats->counts.tuples_updated;
        tabentry->tuples_deleted += lstats->counts.tuples_deleted;
+       tabentry->tuples_checked += lstats->counts.tuples_checked;
        tabentry->tuples_hot_updated += lstats->counts.tuples_hot_updated;
        tabentry->tuples_newpage_updated += 
lstats->counts.tuples_newpage_updated;
 
@@ -863,12 +865,14 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool 
nowait)
                tabentry->live_tuples = 0;
                tabentry->dead_tuples = 0;
                tabentry->ins_since_vacuum = 0;
+               tabentry->check_since_vacuum = 0;
        }
 
        tabentry->live_tuples += lstats->counts.delta_live_tuples;
        tabentry->dead_tuples += lstats->counts.delta_dead_tuples;
        tabentry->mod_since_analyze += lstats->counts.changed_tuples;
        tabentry->ins_since_vacuum += lstats->counts.tuples_inserted;
+       tabentry->check_since_vacuum += lstats->counts.tuples_checked;
        tabentry->blocks_fetched += lstats->counts.blocks_fetched;
        tabentry->blocks_hit += lstats->counts.blocks_hit;
 
@@ -886,6 +890,7 @@ pgstat_relation_flush_cb(PgStat_EntryRef *entry_ref, bool 
nowait)
        dbentry->tuples_inserted += lstats->counts.tuples_inserted;
        dbentry->tuples_updated += lstats->counts.tuples_updated;
        dbentry->tuples_deleted += lstats->counts.tuples_deleted;
+       dbentry->tuples_checked += lstats->counts.tuples_checked;
        dbentry->blocks_fetched += lstats->counts.blocks_fetched;
        dbentry->blocks_hit += lstats->counts.blocks_hit;
 
diff --git a/src/backend/utils/adt/pgstatfuncs.c 
b/src/backend/utils/adt/pgstatfuncs.c
index 97af7c6554f..d5ae28ccdb6 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -73,6 +73,9 @@ PG_STAT_GET_RELENTRY_INT64(dead_tuples)
 /* pg_stat_get_ins_since_vacuum */
 PG_STAT_GET_RELENTRY_INT64(ins_since_vacuum)
 
+/* pg_stat_get_check_since_vacuum */
+PG_STAT_GET_RELENTRY_INT64(check_since_vacuum)
+
 /* pg_stat_get_live_tuples */
 PG_STAT_GET_RELENTRY_INT64(live_tuples)
 
@@ -103,6 +106,9 @@ PG_STAT_GET_RELENTRY_INT64(tuples_returned)
 /* pg_stat_get_tuples_updated */
 PG_STAT_GET_RELENTRY_INT64(tuples_updated)
 
+/* pg_stat_get_tuples_checked */
+PG_STAT_GET_RELENTRY_INT64(tuples_checked)
+
 /* pg_stat_get_vacuum_count */
 PG_STAT_GET_RELENTRY_INT64(vacuum_count)
 
@@ -1091,6 +1097,9 @@ PG_STAT_GET_DBENTRY_INT64(tuples_returned)
 /* pg_stat_get_db_tuples_updated */
 PG_STAT_GET_DBENTRY_INT64(tuples_updated)
 
+/* pg_stat_get_db_tuples_checked */
+PG_STAT_GET_DBENTRY_INT64(tuples_checked)
+
 /* pg_stat_get_db_xact_commit */
 PG_STAT_GET_DBENTRY_INT64(xact_commit)
 
@@ -1774,6 +1783,9 @@ PG_STAT_GET_XACT_RELENTRY_INT64(tuples_hot_updated)
 /* pg_stat_get_xact_tuples_newpage_updated */
 PG_STAT_GET_XACT_RELENTRY_INT64(tuples_newpage_updated)
 
+/* pg_stat_get_xact_tuples_checked */
+PG_STAT_GET_XACT_RELENTRY_INT64(tuples_checked)
+
 /* pg_stat_get_xact_blocks_fetched */
 PG_STAT_GET_XACT_RELENTRY_INT64(blocks_fetched)
 
diff --git a/src/backend/utils/misc/guc_tables.c 
b/src/backend/utils/misc/guc_tables.c
index 4eaeca89f2c..567dc2c6fa9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3514,6 +3514,15 @@ struct config_int ConfigureNamesInt[] =
                60, 1, INT_MAX / 1000,
                NULL, NULL, NULL
        },
+       {
+               {"autovacuum_vacuum_check_threshold", PGC_SIGHUP, 
VACUUM_AUTOVACUUM,
+                       gettext_noop("Minimum number of heap tuple checks by 
index-only scan prior to vacuum, or -1 to disable check vacuums."),
+                       NULL
+               },
+               &autovacuum_vac_check_thresh,
+               1000, -1, INT_MAX,
+               NULL, NULL, NULL
+       },
        {
                {"autovacuum_vacuum_threshold", PGC_SIGHUP, VACUUM_AUTOVACUUM,
                        gettext_noop("Minimum number of tuple updates or 
deletes prior to vacuum."),
@@ -4057,6 +4066,16 @@ struct config_real ConfigureNamesReal[] =
                NULL, NULL, NULL
        },
 
+       {
+               {"autovacuum_vacuum_check_scale_factor", PGC_SIGHUP, 
VACUUM_AUTOVACUUM,
+                       gettext_noop("Number of heap tuple checks by index-only 
scan prior to vacuum as a fraction of reltuples."),
+                       NULL
+               },
+               &autovacuum_vac_check_scale,
+               0.2, 0.0, 100.0,
+               NULL, NULL, NULL
+       },
+
        {
                {"autovacuum_vacuum_cost_delay", PGC_SIGHUP, VACUUM_AUTOVACUUM,
                        gettext_noop("Vacuum cost delay in milliseconds, for 
autovacuum."),
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8b68b16d79d..6123321b6ee 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5511,6 +5511,10 @@
   proname => 'pg_stat_get_tuples_deleted', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_tuples_deleted' },
+{ oid => '4551', descr => 'statistics: number of heap tuples checked by 
index-only scan',
+  proname => 'pg_stat_get_tuples_checked', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_tuples_checked' },
 { oid => '1972', descr => 'statistics: number of tuples hot updated',
   proname => 'pg_stat_get_tuples_hot_updated', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
@@ -5538,6 +5542,11 @@
   proname => 'pg_stat_get_ins_since_vacuum', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_ins_since_vacuum' },
+{ oid => '4554',
+  descr => 'statistics: number of heap tuples checked by index-only scan since 
last vacuum',
+  proname => 'pg_stat_get_check_since_vacuum', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_check_since_vacuum' },
 { oid => '1934', descr => 'statistics: number of blocks fetched',
   proname => 'pg_stat_get_blocks_fetched', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
@@ -5766,6 +5775,10 @@
   proname => 'pg_stat_get_db_tuples_deleted', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_db_tuples_deleted' },
+{ oid => '4552', descr => 'statistics: heap tuples fetched by index-only scan 
for database',
+  proname => 'pg_stat_get_db_tuples_checked', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_db_tuples_checked' },
 { oid => '3065',
   descr => 'statistics: recovery conflicts in database caused by drop 
tablespace',
   proname => 'pg_stat_get_db_conflict_tablespace', provolatile => 's',
@@ -6062,6 +6075,11 @@
   proname => 'pg_stat_get_xact_tuples_newpage_updated', provolatile => 'v',
   proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
   prosrc => 'pg_stat_get_xact_tuples_newpage_updated' },
+{ oid => '4553',
+  descr => 'statistics: number of tuples checked by index-only scan in current 
transaction',
+  proname => 'pg_stat_get_xact_tuples_checked', provolatile => 'v',
+  proparallel => 'r', prorettype => 'int8', proargtypes => 'oid',
+  prosrc => 'pg_stat_get_xact_tuples_checked' },
 { oid => '3044',
   descr => 'statistics: number of blocks fetched in current transaction',
   proname => 'pg_stat_get_xact_blocks_fetched', provolatile => 'v',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 378f2f2c2ba..7c11360bb6e 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -139,6 +139,7 @@ typedef struct PgStat_TableCounts
 
        PgStat_Counter tuples_returned;
        PgStat_Counter tuples_fetched;
+       PgStat_Counter tuples_checked;
 
        PgStat_Counter tuples_inserted;
        PgStat_Counter tuples_updated;
@@ -350,6 +351,7 @@ typedef struct PgStat_StatDBEntry
        PgStat_Counter tuples_inserted;
        PgStat_Counter tuples_updated;
        PgStat_Counter tuples_deleted;
+       PgStat_Counter tuples_checked;
        TimestampTz last_autovac_time;
        PgStat_Counter conflict_tablespace;
        PgStat_Counter conflict_lock;
@@ -425,6 +427,7 @@ typedef struct PgStat_StatTabEntry
 
        PgStat_Counter tuples_returned;
        PgStat_Counter tuples_fetched;
+       PgStat_Counter tuples_checked;
 
        PgStat_Counter tuples_inserted;
        PgStat_Counter tuples_updated;
@@ -436,6 +439,7 @@ typedef struct PgStat_StatTabEntry
        PgStat_Counter dead_tuples;
        PgStat_Counter mod_since_analyze;
        PgStat_Counter ins_since_vacuum;
+       PgStat_Counter check_since_vacuum;
 
        PgStat_Counter blocks_fetched;
        PgStat_Counter blocks_hit;
@@ -691,6 +695,11 @@ extern void pgstat_report_analyze(Relation rel,
                if (pgstat_should_count_relation(rel))                          
                \
                        (rel)->pgstat_info->counts.tuples_fetched++;            
        \
        } while (0)
+#define pgstat_count_heap_check(rel)                                           
                \
+       do {                                                                    
                                                \
+               if (pgstat_should_count_relation(rel))                          
                \
+                       (rel)->pgstat_info->counts.tuples_checked++;            
        \
+       } while (0)
 #define pgstat_count_index_scan(rel)                                           
                \
        do {                                                                    
                                                \
                if (pgstat_should_count_relation(rel))                          
                \
diff --git a/src/include/postmaster/autovacuum.h 
b/src/include/postmaster/autovacuum.h
index e8135f41a1c..76af3b5bce7 100644
--- a/src/include/postmaster/autovacuum.h
+++ b/src/include/postmaster/autovacuum.h
@@ -37,6 +37,8 @@ extern PGDLLIMPORT int autovacuum_vac_max_thresh;
 extern PGDLLIMPORT double autovacuum_vac_scale;
 extern PGDLLIMPORT int autovacuum_vac_ins_thresh;
 extern PGDLLIMPORT double autovacuum_vac_ins_scale;
+extern PGDLLIMPORT int autovacuum_vac_check_thresh;
+extern PGDLLIMPORT double autovacuum_vac_check_scale;
 extern PGDLLIMPORT int autovacuum_anl_thresh;
 extern PGDLLIMPORT double autovacuum_anl_scale;
 extern PGDLLIMPORT int autovacuum_freeze_max_age;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index d94fddd7cef..d750aa0fb91 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -311,6 +311,7 @@ typedef struct AutoVacOpts
        int                     vacuum_threshold;
        int                     vacuum_max_threshold;
        int                     vacuum_ins_threshold;
+       int                     vacuum_check_threshold;
        int                     analyze_threshold;
        int                     vacuum_cost_limit;
        int                     freeze_min_age;
@@ -323,6 +324,7 @@ typedef struct AutoVacOpts
        float8          vacuum_cost_delay;
        float8          vacuum_scale_factor;
        float8          vacuum_ins_scale_factor;
+       float8          vacuum_check_scale_factor;
        float8          analyze_scale_factor;
 } AutoVacOpts;
 
diff --git a/src/test/regress/expected/check_autovacuum.out 
b/src/test/regress/expected/check_autovacuum.out
index 429f80a9a08..aa0ba5b355d 100644
--- a/src/test/regress/expected/check_autovacuum.out
+++ b/src/test/regress/expected/check_autovacuum.out
@@ -26,6 +26,14 @@ begin
        end loop;
 end;
 $$ language plpgsql;
+-- Make autovacuum performed more frequently
+alter system set autovacuum_naptime=2;
+select pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
 -- Force index-only scan
 set enable_bitmapscan=off;
 set enable_seqscan=off;
@@ -35,15 +43,15 @@ begin;
 -- Just force assignment XID to transaction
 create table t2(x integer);
 prepare transaction 'pt1';
-insert into t values (generate_series(1,1000000), 0);
+insert into t values (generate_series(1,100000), 0);
 -- vacuum should't mark pages as all visible because of prepared xact
 vacuum t;
 commit prepared 'pt1';
--- With default autovacuum tuning we need to check more than 2k of tuples to 
trigger autovacuum
-select explain_to_json('select sum(pk) from t where pk between 100000 and 
400000')  #>> '{0,Plan,Plans,0,"Heap Fetches"}' as heap_fetches;
+-- With default autovacuum tuning we need to check more than 20k of tuples to 
trigger autovacuum
+select explain_to_json('select sum(pk) from t where pk between 10000 and 
40000')  #>> '{0,Plan,Plans,0,"Heap Fetches"}' as heap_fetches;
  heap_fetches 
 --------------
- 300001
+ 30001
 (1 row)
 
 select pg_sleep(1); -- let statistic be updated
@@ -53,13 +61,7 @@ select pg_sleep(1); -- let statistic be updated
 (1 row)
 
 call wait_autovacuum_completion('t');
-select autovacuum_count,last_autovacuum,n_check_since_vacuum,n_tup_check from 
pg_stat_all_tables where relname='t';
- autovacuum_count |           last_autovacuum           | n_check_since_vacuum 
| n_tup_check 
-------------------+-------------------------------------+----------------------+-------------
-                1 | Tue Apr 01 08:47:51.957169 2025 PDT |                    0 
|      300001
-(1 row)
-
-select explain_to_json('select sum(pk) from t where pk between 100000 and 
400000')  #>> '{0,Plan,Plans,0,"Heap Fetches"}' as heap_fetches;
+select explain_to_json('select sum(pk) from t where pk between 10000 and 
40000')  #>> '{0,Plan,Plans,0,"Heap Fetches"}' as heap_fetches;
  heap_fetches 
 --------------
  0
diff --git a/src/test/regress/expected/rules.out 
b/src/test/regress/expected/rules.out
index 47478969135..88910eb724e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1773,7 +1773,8 @@ pg_stat_all_indexes| SELECT c.oid AS relid,
     pg_stat_get_numscans(i.oid) AS idx_scan,
     pg_stat_get_lastscan(i.oid) AS last_idx_scan,
     pg_stat_get_tuples_returned(i.oid) AS idx_tup_read,
-    pg_stat_get_tuples_fetched(i.oid) AS idx_tup_fetch
+    pg_stat_get_tuples_fetched(i.oid) AS idx_tup_fetch,
+    pg_stat_get_tuples_checked(i.oid) AS idx_tup_check
    FROM (((pg_class c
      JOIN pg_index x ON ((c.oid = x.indrelid)))
      JOIN pg_class i ON ((i.oid = x.indexrelid)))
@@ -1808,7 +1809,9 @@ pg_stat_all_tables| SELECT c.oid AS relid,
     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,
-    pg_stat_get_total_autoanalyze_time(c.oid) AS total_autoanalyze_time
+    pg_stat_get_total_autoanalyze_time(c.oid) AS total_autoanalyze_time,
+    pg_stat_get_check_since_vacuum(c.oid) AS n_check_since_vacuum,
+    pg_stat_get_tuples_checked(c.oid) AS n_tup_check
    FROM ((pg_class c
      LEFT JOIN pg_index i ON ((c.oid = i.indrelid)))
      LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
@@ -1869,7 +1872,8 @@ pg_stat_database| SELECT oid AS datid,
     pg_stat_get_db_sessions_killed(oid) AS sessions_killed,
     pg_stat_get_db_parallel_workers_to_launch(oid) AS 
parallel_workers_to_launch,
     pg_stat_get_db_parallel_workers_launched(oid) AS parallel_workers_launched,
-    pg_stat_get_db_stat_reset_time(oid) AS stats_reset
+    pg_stat_get_db_stat_reset_time(oid) AS stats_reset,
+    pg_stat_get_db_tuples_checked(oid) AS tup_checked
    FROM ( SELECT 0 AS oid,
             NULL::name AS datname
         UNION ALL
@@ -2169,7 +2173,8 @@ pg_stat_sys_indexes| SELECT relid,
     idx_scan,
     last_idx_scan,
     idx_tup_read,
-    idx_tup_fetch
+    idx_tup_fetch,
+    idx_tup_check
    FROM pg_stat_all_indexes
   WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 
'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text));
 pg_stat_sys_tables| SELECT relid,
@@ -2201,7 +2206,9 @@ pg_stat_sys_tables| SELECT relid,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-    total_autoanalyze_time
+    total_autoanalyze_time,
+    n_check_since_vacuum,
+    n_tup_check
    FROM pg_stat_all_tables
   WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 
'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text));
 pg_stat_user_functions| SELECT p.oid AS funcid,
@@ -2221,7 +2228,8 @@ pg_stat_user_indexes| SELECT relid,
     idx_scan,
     last_idx_scan,
     idx_tup_read,
-    idx_tup_fetch
+    idx_tup_fetch,
+    idx_tup_check
    FROM pg_stat_all_indexes
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 
'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
 pg_stat_user_tables| SELECT relid,
@@ -2253,7 +2261,9 @@ pg_stat_user_tables| SELECT relid,
     total_vacuum_time,
     total_autovacuum_time,
     total_analyze_time,
-    total_autoanalyze_time
+    total_autoanalyze_time,
+    n_check_since_vacuum,
+    n_tup_check
    FROM pg_stat_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 
'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
 pg_stat_wal| SELECT wal_records,
@@ -2290,7 +2300,8 @@ pg_stat_xact_all_tables| SELECT c.oid AS relid,
     pg_stat_get_xact_tuples_updated(c.oid) AS n_tup_upd,
     pg_stat_get_xact_tuples_deleted(c.oid) AS n_tup_del,
     pg_stat_get_xact_tuples_hot_updated(c.oid) AS n_tup_hot_upd,
-    pg_stat_get_xact_tuples_newpage_updated(c.oid) AS n_tup_newpage_upd
+    pg_stat_get_xact_tuples_newpage_updated(c.oid) AS n_tup_newpage_upd,
+    pg_stat_get_xact_tuples_checked(c.oid) AS n_tup_check
    FROM ((pg_class c
      LEFT JOIN pg_index i ON ((c.oid = i.indrelid)))
      LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
@@ -2307,7 +2318,8 @@ pg_stat_xact_sys_tables| SELECT relid,
     n_tup_upd,
     n_tup_del,
     n_tup_hot_upd,
-    n_tup_newpage_upd
+    n_tup_newpage_upd,
+    n_tup_check
    FROM pg_stat_xact_all_tables
   WHERE ((schemaname = ANY (ARRAY['pg_catalog'::name, 
'information_schema'::name])) OR (schemaname ~ '^pg_toast'::text));
 pg_stat_xact_user_functions| SELECT p.oid AS funcid,
@@ -2330,7 +2342,8 @@ pg_stat_xact_user_tables| SELECT relid,
     n_tup_upd,
     n_tup_del,
     n_tup_hot_upd,
-    n_tup_newpage_upd
+    n_tup_newpage_upd,
+    n_tup_check
    FROM pg_stat_xact_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 
'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
 pg_statio_all_indexes| SELECT c.oid AS relid,
diff --git a/src/test/regress/parallel_schedule 
b/src/test/regress/parallel_schedule
index 0a35f2f8f6a..b9f61655d66 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -133,6 +133,9 @@ test: event_trigger_login
 # this test also uses event triggers, so likewise run it by itself
 test: fast_default
 
+# this test checks behaviour of autovacuum so run it standalone
+test: check_autovacuum
+
 # run tablespace test at the end because it drops the tablespace created during
 # setup that other tests may use.
 test: tablespace
diff --git a/src/test/regress/sql/check_autovacuum.sql 
b/src/test/regress/sql/check_autovacuum.sql
index 37dc74208db..8c83081ea89 100644
--- a/src/test/regress/sql/check_autovacuum.sql
+++ b/src/test/regress/sql/check_autovacuum.sql
@@ -1,21 +1,37 @@
-create function wait_autovacuum_completion(table_name text) returns void AS $$
+create function explain_to_json(text) returns jsonb language plpgsql as
+$$
 declare
-    autovacuum_counter_before integer;
-    autovacuum_counter_after integer;
-       n_heap_checkes_since_last_autovacuum integer;
+    js text;
+       heap_fetches integer;
+begin
+    for js in execute 'EXPLAIN (ANALYZE, TIMING OFF, COSTS OFF, SUMMARY OFF, 
BUFFERS ON, FORMAT JSON)' || $1 loop
+               return js::jsonb;
+       end loop;
+end;
+$$;
+
+create or replace procedure wait_autovacuum_completion(table_name text)  AS $$
+declare
+       checks_since_last_autovacuum integer;
+       total_checks integer;
 begin
-    select autovacuum_count from pg_stat_all_tables where relname=table_name 
into autovacuum_counter_before;
        -- default autovacuum naptime is 1 minute
-    for i in 1..60 loop
-        select autovacuum_count,n_check_since_vacuum from pg_stat_all_tables 
where relname=table_name into 
autovacuum_counter_after,n_heap_checkes_since_last_autovacuum;
-               if autovacuum_counter_before != autovacuum_counter_after OR 
n_heap_checkes_since_last_autovacuum = 0 then
+    loop
+        select n_check_since_vacuum,n_tup_check from pg_stat_all_tables where 
relname=table_name
+                   into checks_since_last_autovacuum,total_checks;
+               if checks_since_last_autovacuum = 0 and total_checks != 0 then
                    exit;
                end if;
-           perform pg_sleep_for('1 seconds');
+           perform pg_sleep_for('1 second');
+               rollback;
        end loop;
 end;
 $$ language plpgsql;
 
+-- Make autovacuum performed more frequently
+alter system set autovacuum_naptime=2;
+select pg_reload_conf();
+
 -- Force index-only scan
 set enable_bitmapscan=off;
 set enable_seqscan=off;
@@ -28,17 +44,16 @@ begin;
 create table t2(x integer);
 prepare transaction 'pt1';
 
-insert into t values (generate_series(1,1000000), 0);
+insert into t values (generate_series(1,100000), 0);
 -- vacuum should't mark pages as all visible because of prepared xact
 vacuum t;
 
 commit prepared 'pt1';
 
--- With default autovacuum tuning we need to check more than 2k of tuples to 
trigger autovacuum
-select sum(pk) from t where pk between 100000 and 400000;
-select n_tup_check from pg_stat_all_tables where relname='t';
+-- With default autovacuum tuning we need to check more than 20k of tuples to 
trigger autovacuum
+select explain_to_json('select sum(pk) from t where pk between 10000 and 
40000')  #>> '{0,Plan,Plans,0,"Heap Fetches"}' as heap_fetches;
 
-select wait_autovacuum_completion('t');
+select pg_sleep(1); -- let statistic be updated
+call wait_autovacuum_completion('t');
 
-select sum(pk) from t where pk between 100000 and 400000;
-select n_tup_check from pg_stat_all_tables where relname='t';
+select explain_to_json('select sum(pk) from t where pk between 10000 and 
40000')  #>> '{0,Plan,Plans,0,"Heap Fetches"}' as heap_fetches;

Reply via email to