From 4acb49d16e43f7f19b7c2cdd38be6001d43debf4 Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Sun, 13 Sep 2026 05:19:26 +0000 Subject: [PATCH v7 1/2] Report per-index vacuum progress in pg_stat_progress_vacuum. Previously, pg_stat_progress_vacuum showed the total and processed index counts, but neither which index a backend was working on nor how far it had gotten through it. On a table with many indexes of different access methods, that made it hard to tell which index a slow or stuck vacuum was spending its time on, and for a large B-tree, at the scale of hundreds of GBs to TBs, there was no way to estimate when the index phase, and together with heap_blks_*, the whole vacuum would finish. This commit adds three columns. current_index_relid reports the OID of the index a backend is currently vacuuming or cleaning up. index_blks_total and index_blks_done report block progress within that index. All three are set before an index is processed and reset once that index is done, so they do not report a stale value after the phase moves on. During parallel index vacuum the leader also participates, and each participant processes a different set of indexes. That per-index progress cannot be collapsed into a single leader row without losing the detail that matters, so each participant, the leader and the workers alike, now reports its own row and shows the index it is processing and how far along it is. A worker row carries the columns that come from its own backend state, pid, datid, datname, relid, phase, current_index_relid, index_blks_total and index_blks_done; the remaining columns track command-level progress that the leader maintains and read as zero on worker rows, or NULL for mode and started_by. Because a table can be vacuumed by only one VACUUM at a time, the rows sharing a datid and relid make up a single vacuum, so they can be grouped that way to see the leader together with all of its workers. If the exact leader-to-worker mapping is wanted, it can be recovered from leader_pid in pg_stat_activity. The block counters are the ones CREATE INDEX progress reporting added in commit ab0dfc961b6a, PROGRESS_SCAN_BLOCKS_TOTAL and PROGRESS_SCAN_BLOCKS_DONE. B-tree's index scan already knows how to report them, so all that is needed here is turning that reporting on in the serial and parallel index vacuum paths; other access methods report nothing and leave the two columns at zero. They are deliberately kept separate from heap_blks_*, which must be retained across a multi-pass index vacuum, the case where the dead-TID store fills, and which in the serial case belong to the same backend that is doing the index vacuuming, so reusing them for index blocks would destroy heap progress that is still needed. No caller sets IndexVacuumInfo.report_progress to false anymore; the next commit removes the field. XXX: Bump catalog version. Author: Bharath Rupireddy Reviewed-by: Michael Paquier Reviewed-by: Sami Imseih Reviewed-by: Masahiko Sawada Discussion: https://postgr.es/m/CALj2ACUgwSchK6jQ2CdKLBWUADTOE_zKdTff2Zg3E6hOuXKv-w@mail.gmail.com --- doc/src/sgml/monitoring.sgml | 95 ++++++++++++++++++++++++++- src/backend/access/heap/vacuumlazy.c | 36 +++++++++- src/backend/catalog/system_views.sql | 5 +- src/backend/commands/vacuumparallel.c | 37 ++++++++++- src/include/commands/progress.h | 2 + src/test/regress/expected/rules.out | 5 +- 6 files changed, 172 insertions(+), 8 deletions(-) diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 62dadf3e86c..4c49aac7003 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -7670,8 +7670,11 @@ FROM pg_stat_get_backend_idset() AS backendid; Whenever VACUUM is running, the pg_stat_progress_vacuum view will contain - one row for each backend (including autovacuum worker processes) that is - currently vacuuming. The tables below describe the information + one row for each backend (including autovacuum worker processes and + parallel workers launched for + parallel vacuum) that is currently + vacuuming; see the note following the view for the columns reported on + parallel worker rows. The tables below describe the information that will be reported and provide information about how to interpret it. Progress for VACUUM FULL commands is reported via pg_stat_progress_repack, and is also visible via @@ -7929,10 +7932,98 @@ FROM pg_stat_get_backend_idset() AS backendid; + + + + current_index_relid oid + + + If VACUUM is currently processing an index, this + column shows the OID of the index being vacuumed. The value is set + when the phase is vacuuming indexes or + cleaning up indexes, and is reset to 0 once that + index has been processed, so it does not show a stale index while the + backend is between indexes or has moved on to another phase. During + parallel index vacuum, each parallel worker row shows the index that + particular worker is processing. + + + + + + index_blks_total bigint + + + Total number of blocks in the index identified by + current_index_relid. This is reported only + for B-tree indexes, and only while the index is being scanned; it is 0 + for other index access methods, for a B-tree whose scan + VACUUM was able to skip during cleanup, and once + the index has been processed. + + + + + + index_blks_done bigint + + + Number of blocks of the index identified by + current_index_relid scanned so far. This is + reported only for B-tree indexes, and only while the index is being + scanned; it is 0 for other index access methods, for a B-tree whose + scan VACUUM was able to skip during cleanup, and + once the index has been processed. Together with + index_blks_total this gives per-index vacuum + progress, which is useful for estimating completion of large indexes. + The index metapage is counted in + index_blks_total but is never scanned, so + this column stops one block short of it. + + + + + + During a parallel vacuum, each participating backend (the leader and each + parallel worker) reports its own row. On a worker row the meaningful + columns are pid, datid, + datname, relid, + phase, current_index_relid, + index_blks_total and + index_blks_done. The remaining columns track + command-level heap progress that only the leader maintains; they read as + zero on worker rows, except that mode and + started_by appear as NULL. + A participant row showing + vacuuming indexes or + cleaning up indexes with a zero + current_index_relid is between indexes, or has + finished its share of the indexes while other participants are still + working. A worker row whose phase is + initializing is a worker that has been launched but has + not started on an index yet, which also happens when every index was + claimed by another participant before this worker got to it. Because a + table can be vacuumed by only one + VACUUM at a time, the rows sharing a given + datid and relid + together make up a single vacuum, so they can be grouped that way to see + the leader and all of its workers. The leader and worker process IDs can be + correlated with leader_pid in + pg_stat_activity + if needed. Because the worker rows are separate backends, a role that does + not have privileges of the pg_read_all_stats role and + does not own those backends sees only their pid, + datid and datname, + with the remaining columns NULL, so such a role can tell + that a parallel vacuum is running in a database but not which table or + index it is working on. + + + VACUUM Phases diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 8e1f660bc2f..a23af936b7f 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -3038,16 +3038,26 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat, { IndexVacuumInfo ivinfo; LVSavedErrInfo saved_err_info; + const int reset_index[] = { + PROGRESS_VACUUM_CURRENT_INDEX_RELID, + PROGRESS_SCAN_BLOCKS_TOTAL, + PROGRESS_SCAN_BLOCKS_DONE + }; + const int64 reset_val[] = {(int64) InvalidOid, 0, 0}; ivinfo.index = indrel; ivinfo.heaprel = vacrel->rel; ivinfo.analyze_only = false; - ivinfo.report_progress = false; + ivinfo.report_progress = true; ivinfo.estimated_count = true; ivinfo.message_level = DEBUG2; ivinfo.num_heap_tuples = reltuples; ivinfo.strategy = vacrel->bstrategy; + /* Report which index we're currently processing */ + pgstat_progress_update_param(PROGRESS_VACUUM_CURRENT_INDEX_RELID, + (int64) RelationGetRelid(indrel)); + /* * Update error traceback information. * @@ -3069,6 +3079,12 @@ lazy_vacuum_one_index(Relation indrel, IndexBulkDeleteResult *istat, pfree(vacrel->indname); vacrel->indname = NULL; + /* + * Reset the current index progress parameters to avoid reporting stale + * values. + */ + pgstat_progress_update_multi_param(3, reset_index, reset_val); + return istat; } @@ -3088,17 +3104,27 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat, { IndexVacuumInfo ivinfo; LVSavedErrInfo saved_err_info; + const int reset_index[] = { + PROGRESS_VACUUM_CURRENT_INDEX_RELID, + PROGRESS_SCAN_BLOCKS_TOTAL, + PROGRESS_SCAN_BLOCKS_DONE + }; + const int64 reset_val[] = {(int64) InvalidOid, 0, 0}; ivinfo.index = indrel; ivinfo.heaprel = vacrel->rel; ivinfo.analyze_only = false; - ivinfo.report_progress = false; + ivinfo.report_progress = true; ivinfo.estimated_count = estimated_count; ivinfo.message_level = DEBUG2; ivinfo.num_heap_tuples = reltuples; ivinfo.strategy = vacrel->bstrategy; + /* Report which index we're currently processing */ + pgstat_progress_update_param(PROGRESS_VACUUM_CURRENT_INDEX_RELID, + (int64) RelationGetRelid(indrel)); + /* * Update error traceback information. * @@ -3118,6 +3144,12 @@ lazy_cleanup_one_index(Relation indrel, IndexBulkDeleteResult *istat, pfree(vacrel->indname); vacrel->indname = NULL; + /* + * Reset the current index progress parameters to avoid reporting stale + * values. + */ + pgstat_progress_update_multi_param(3, reset_index, reset_val); + return istat; } diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index ad340887f54..809b9c0f1e4 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1353,7 +1353,10 @@ CREATE VIEW pg_stat_progress_vacuum AS CASE S.param13 WHEN 1 THEN 'manual' WHEN 2 THEN 'autovacuum' WHEN 3 THEN 'autovacuum_wraparound' - ELSE NULL END AS started_by + ELSE NULL END AS started_by, + CAST(S.param14 AS oid) AS current_index_relid, + S.param16 AS index_blks_total, + S.param17 AS index_blks_done FROM pg_stat_get_progress_info('VACUUM') AS S LEFT JOIN pg_database D ON S.datid = D.oid; diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c index 767d162e578..607c57c8eba 100644 --- a/src/backend/commands/vacuumparallel.c +++ b/src/backend/commands/vacuumparallel.c @@ -1076,6 +1076,17 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, IndexBulkDeleteResult *istat = NULL; IndexBulkDeleteResult *istat_res; IndexVacuumInfo ivinfo; + const int progress_index[] = { + PROGRESS_VACUUM_PHASE, + PROGRESS_VACUUM_CURRENT_INDEX_RELID + }; + int64 progress_val[2]; + const int reset_index[] = { + PROGRESS_VACUUM_CURRENT_INDEX_RELID, + PROGRESS_SCAN_BLOCKS_TOTAL, + PROGRESS_SCAN_BLOCKS_DONE + }; + const int64 reset_val[] = {(int64) InvalidOid, 0, 0}; /* * Update the pointer to the corresponding bulk-deletion result if someone @@ -1087,7 +1098,7 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, ivinfo.index = indrel; ivinfo.heaprel = pvs->heaprel; ivinfo.analyze_only = false; - ivinfo.report_progress = false; + ivinfo.report_progress = true; ivinfo.message_level = DEBUG2; ivinfo.estimated_count = pvs->shared->estimated_count; ivinfo.num_heap_tuples = pvs->shared->reltuples; @@ -1097,6 +1108,16 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, pvs->indname = pstrdup(RelationGetRelationName(indrel)); pvs->status = indstats->status; + /* + * Report the phase and the index we're about to process before we start, + * so that it is visible for the whole duration of the index scan. + */ + progress_val[0] = (indstats->status == PARALLEL_INDVAC_STATUS_NEED_BULKDELETE) + ? PROGRESS_VACUUM_PHASE_VACUUM_INDEX + : PROGRESS_VACUUM_PHASE_INDEX_CLEANUP; + progress_val[1] = (int64) RelationGetRelid(indrel); + pgstat_progress_update_multi_param(2, progress_index, progress_val); + switch (indstats->status) { case PARALLEL_INDVAC_STATUS_NEED_BULKDELETE: @@ -1112,6 +1133,12 @@ parallel_vacuum_process_one_index(ParallelVacuumState *pvs, Relation indrel, RelationGetRelationName(indrel)); } + /* + * Reset the current index progress parameters to avoid reporting stale + * values. + */ + pgstat_progress_update_multi_param(3, reset_index, reset_val); + /* * Copy the index bulk-deletion result returned from ambulkdelete and * amvacuumcleanup to the DSM segment if it's the first cycle because they @@ -1191,7 +1218,7 @@ parallel_vacuum_index_is_parallel_safe(Relation indrel, int num_index_scans, * Perform work within a launched parallel process. * * Since parallel vacuum workers perform only index vacuum or index cleanup, - * we don't need to report progress information. + * they report progress for the index they are processing. */ void parallel_vacuum_main(dsm_segment *seg, shm_toc *toc) @@ -1315,6 +1342,9 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc) /* Prepare to track buffer usage during parallel execution */ InstrStartParallelQuery(); + /* Register this worker for vacuum progress reporting */ + pgstat_progress_start_command(PROGRESS_COMMAND_VACUUM, shared->relid); + /* Process indexes to perform vacuum/cleanup */ parallel_vacuum_process_safe_indexes(&pvs); @@ -1334,6 +1364,9 @@ parallel_vacuum_main(dsm_segment *seg, shm_toc *toc) /* Pop the error context stack */ error_context_stack = errcallback.previous; + /* Unregister this worker from vacuum progress reporting */ + pgstat_progress_end_command(); + vac_close_indexes(nindexes, indrels, RowExclusiveLock); table_close(rel, ShareUpdateExclusiveLock); FreeAccessStrategy(pvs.bstrategy); diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h index 2a12920c75f..bf91455eff9 100644 --- a/src/include/commands/progress.h +++ b/src/include/commands/progress.h @@ -31,6 +31,8 @@ #define PROGRESS_VACUUM_DELAY_TIME 10 #define PROGRESS_VACUUM_MODE 11 #define PROGRESS_VACUUM_STARTED_BY 12 +#define PROGRESS_VACUUM_CURRENT_INDEX_RELID 13 +/* 15 and 16 reserved for "block number" metrics */ /* Phases of vacuum (as advertised via PROGRESS_VACUUM_PHASE) */ #define PROGRESS_VACUUM_PHASE_SCAN_HEAP 1 diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 4a8cc759d7b..0addd043e68 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -2214,7 +2214,10 @@ pg_stat_progress_vacuum| SELECT s.pid, WHEN 2 THEN 'autovacuum'::text WHEN 3 THEN 'autovacuum_wraparound'::text ELSE NULL::text - END AS started_by + END AS started_by, + (s.param14)::oid AS current_index_relid, + s.param16 AS index_blks_total, + s.param17 AS index_blks_done FROM (pg_stat_get_progress_info('VACUUM'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20) LEFT JOIN pg_database d ON ((s.datid = d.oid))); pg_stat_recovery| SELECT promote_triggered, -- 2.47.3