This is about another feature which will most likely show up in PostgreSQL 18 later this year. The statistic system is something which gets more and more details with almost every release of PostgreSQL, and PostgreSQL 18 will be no exception to that.
When you take a look at pg_stat_all_tables (or pg_stat_user_tables) in PostgreSQL 17, it looks like this:
postgres=# select version();
version
-----------------------------------------------------------------------------------------------------------------------------
PostgreSQL 17.2 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 14.2.1 20250110 (Red Hat 14.2.1-7), 64-bit
(1 row)
postgres=# \d pg_stat_all_tables
View "pg_catalog.pg_stat_all_tables"
Column | Type | Collation | Nullable | Default
---------------------+--------------------------+-----------+----------+---------
relid | oid | | |
schemaname | name | | |
relname | name | | |
seq_scan | bigint | | |
last_seq_scan | timestamp with time zone | | |
seq_tup_read | bigint | | |
idx_scan | bigint | | |
last_idx_scan | timestamp with time zone | | |
idx_tup_fetch | bigint | | |
n_tup_ins | bigint | | |
n_tup_upd | bigint | | |
n_tup_del | bigint | | |
n_tup_hot_upd | bigint | | |
n_tup_newpage_upd | bigint | | |
n_live_tup | bigint | | |
n_dead_tup | bigint | | |
n_mod_since_analyze | bigint | | |
n_ins_since_vacuum | bigint | | |
last_vacuum | timestamp with time zone | | |
last_autovacuum | timestamp with time zone | | |
last_analyze | timestamp with time zone | | |
last_autoanalyze | timestamp with time zone | | |
vacuum_count | bigint | | |
autovacuum_count | bigint | | |
analyze_count | bigint | | |
autoanalyze_count | bigint | | |
There already are statistics for [auto]vacuum and [auto]analyze but there is no information about how much time the system spend in total for vacuum and analyze operations. This is now available:
postgres=# select version();
version
--------------------------------------------------------------------
PostgreSQL 18devel on x86_64-linux, compiled by gcc-14.2.1, 64-bit
(1 row)
postgres=# \d pg_stat_all_tables
View "pg_catalog.pg_stat_all_tables"
Column | Type | Collation | Nullable | Default
------------------------+--------------------------+-----------+----------+---------
relid | oid | | |
schemaname | name | | |
relname | name | | |
seq_scan | bigint | | |
last_seq_scan | timestamp with time zone | | |
seq_tup_read | bigint | | |
idx_scan | bigint | | |
last_idx_scan | timestamp with time zone | | |
idx_tup_fetch | bigint | | |
n_tup_ins | bigint | | |
n_tup_upd | bigint | | |
n_tup_del | bigint | | |
n_tup_hot_upd | bigint | | |
n_tup_newpage_upd | bigint | | |
n_live_tup | bigint | | |
n_dead_tup | bigint | | |
n_mod_since_analyze | bigint | | |
n_ins_since_vacuum | bigint | | |
last_vacuum | timestamp with time zone | | |
last_autovacuum | timestamp with time zone | | |
last_analyze | timestamp with time zone | | |
last_autoanalyze | timestamp with time zone | | |
vacuum_count | bigint | | |
autovacuum_count | bigint | | |
analyze_count | bigint | | |
autoanalyze_count | bigint | | |
total_vacuum_time | double precision | | |
total_autovacuum_time | double precision | | |
total_analyze_time | double precision | | |
total_autoanalyze_time | double precision | | |
To see that in action, lets create a small table and populate it:
postgres=# create table t ( a int, b text);
CREATE TABLE
postgres=# insert into t select i, i::text from generate_series(1,1000000) i;
INSERT 0 1000000
This triggers autovacuum (you’ll have to wait up to a minute before you see something, because of autovacuum_naptime) and you get the total time in ms spend for auto vacuum and auto analyze:
postgres=# select last_autovacuum
, last_autoanalyze
, total_autovacuum_time
, total_autoanalyze_time
from pg_stat_all_tables
where relname = 't';
last_autovacuum | last_autoanalyze | total_autovacuum_time | total_autoanalyze_time
-------------------------------+-------------------------------+-----------------------+------------------------
2025-01-31 11:12:09.809252+01 | 2025-01-31 11:12:09.942748+01 | 187 | 134
(1 row)
The same happens if you manually trigger either vacuum or analyze:
postgres=# select last_vacuum
, last_analyze
, total_vacuum_time
, total_analyze_time
from pg_stat_all_tables
where relname = 't';
last_vacuum | last_analyze | total_vacuum_time | total_analyze_time
-------------+--------------+-------------------+--------------------
| | 0 | 0
(1 row)
postgres=# analyze t;
ANALYZE
postgres=# select last_vacuum
, last_analyze
, total_vacuum_time
, total_analyze_time
from pg_stat_all_tables
where relname = 't';
last_vacuum | last_analyze | total_vacuum_time | total_analyze_time
-------------+-------------------------------+-------------------+--------------------
| 2025-01-31 11:23:07.102182+01 | 0 | 52
(1 row)
postgres=# vacuum t;
VACUUM
postgres=# select last_vacuum
, last_analyze
, total_vacuum_time
, total_analyze_time
from pg_stat_all_tables
where relname = 't';
last_vacuum | last_analyze | total_vacuum_time | total_analyze_time
-------------------------------+-------------------------------+-------------------+--------------------
2025-01-31 11:23:12.286613+01 | 2025-01-31 11:23:07.102182+01 | 1 | 52
Nice, this really helps in identifying the relations where [auto]vacuum and [auto]anaylze have spent most of the time on. Thanks to all involved.