Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit a46a701

Browse files
committed
Add options to control whether VACUUM runs vac_update_datfrozenxid.
VACUUM normally ends by running vac_update_datfrozenxid(), which requires a scan of pg_class. Therefore, if one attempts to vacuum a database one table at a time --- as vacuumdb has done since v12 --- we will spend O(N^2) time in vac_update_datfrozenxid(). That causes serious performance problems in databases with tens of thousands of tables, and indeed the effect is measurable with only a few hundred. To add insult to injury, only one process can run vac_update_datfrozenxid at the same time per DB, so this behavior largely defeats vacuumdb's -j option. Hence, invent options SKIP_DATABASE_STATS and ONLY_DATABASE_STATS to allow applications to postpone vac_update_datfrozenxid() until the end of a series of VACUUM requests, and teach vacuumdb to use them. Per bug #17717 from Gunnar L. Sadly, this answer doesn't seem like something we'd consider back-patching, so the performance problem will remain in v12-v15. Tom Lane and Nathan Bossart Discussion: https://postgr.es/m/17717-6c50eb1c7d23a886@postgresql.org
1 parent cd4b233 commit a46a701

File tree

10 files changed

+147
-21
lines changed

10 files changed

+147
-21
lines changed

doc/src/sgml/ref/vacuum.sgml

+37
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
3636
PROCESS_TOAST [ <replaceable class="parameter">boolean</replaceable> ]
3737
TRUNCATE [ <replaceable class="parameter">boolean</replaceable> ]
3838
PARALLEL <replaceable class="parameter">integer</replaceable>
39+
SKIP_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
40+
ONLY_DATABASE_STATS [ <replaceable class="parameter">boolean</replaceable> ]
3941

4042
<phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
4143

@@ -295,6 +297,41 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
295297
</listitem>
296298
</varlistentry>
297299

300+
<varlistentry>
301+
<term><literal>SKIP_DATABASE_STATS</literal></term>
302+
<listitem>
303+
<para>
304+
Specifies that <command>VACUUM</command> should skip updating the
305+
database-wide statistics about oldest unfrozen XIDs. Normally
306+
<command>VACUUM</command> will update these statistics once at the
307+
end of the command. However, this can take awhile in a database
308+
with a very large number of tables, and it will accomplish nothing
309+
unless the table that had contained the oldest unfrozen XID was
310+
among those vacuumed. Moreover, if multiple <command>VACUUM</command>
311+
commands are issued in parallel, only one of them can update the
312+
database-wide statistics at a time. Therefore, if an application
313+
intends to issue a series of many <command>VACUUM</command>
314+
commands, it can be helpful to set this option in all but the last
315+
such command; or set it in all the commands and separately
316+
issue <literal>VACUUM (ONLY_DATABASE_STATS)</literal> afterwards.
317+
</para>
318+
</listitem>
319+
</varlistentry>
320+
321+
<varlistentry>
322+
<term><literal>ONLY_DATABASE_STATS</literal></term>
323+
<listitem>
324+
<para>
325+
Specifies that <command>VACUUM</command> should do nothing except
326+
update the database-wide statistics about oldest unfrozen XIDs.
327+
When this option is specified,
328+
the <replaceable class="parameter">table_and_columns</replaceable>
329+
list must be empty, and no other option may be enabled
330+
except <literal>VERBOSE</literal>.
331+
</para>
332+
</listitem>
333+
</varlistentry>
334+
298335
<varlistentry>
299336
<term><replaceable class="parameter">boolean</replaceable></term>
300337
<listitem>

src/backend/commands/vacuum.c

+37-5
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
114114
bool full = false;
115115
bool disable_page_skipping = false;
116116
bool process_toast = true;
117+
bool skip_database_stats = false;
118+
bool only_database_stats = false;
117119
ListCell *lc;
118120

119121
/* index_cleanup and truncate values unspecified for now */
@@ -200,6 +202,10 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
200202
params.nworkers = nworkers;
201203
}
202204
}
205+
else if (strcmp(opt->defname, "skip_database_stats") == 0)
206+
skip_database_stats = defGetBoolean(opt);
207+
else if (strcmp(opt->defname, "only_database_stats") == 0)
208+
only_database_stats = defGetBoolean(opt);
203209
else
204210
ereport(ERROR,
205211
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -216,7 +222,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
216222
(freeze ? VACOPT_FREEZE : 0) |
217223
(full ? VACOPT_FULL : 0) |
218224
(disable_page_skipping ? VACOPT_DISABLE_PAGE_SKIPPING : 0) |
219-
(process_toast ? VACOPT_PROCESS_TOAST : 0);
225+
(process_toast ? VACOPT_PROCESS_TOAST : 0) |
226+
(skip_database_stats ? VACOPT_SKIP_DATABASE_STATS : 0) |
227+
(only_database_stats ? VACOPT_ONLY_DATABASE_STATS : 0);
220228

221229
/* sanity checks on options */
222230
Assert(params.options & (VACOPT_VACUUM | VACOPT_ANALYZE));
@@ -349,6 +357,24 @@ vacuum(List *relations, VacuumParams *params,
349357
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
350358
errmsg("PROCESS_TOAST required with VACUUM FULL")));
351359

360+
/* sanity check for ONLY_DATABASE_STATS */
361+
if (params->options & VACOPT_ONLY_DATABASE_STATS)
362+
{
363+
Assert(params->options & VACOPT_VACUUM);
364+
if (relations != NIL)
365+
ereport(ERROR,
366+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
367+
errmsg("ONLY_DATABASE_STATS cannot be specified with a list of tables")));
368+
/* don't require people to turn off PROCESS_TOAST explicitly */
369+
if (params->options & ~(VACOPT_VACUUM |
370+
VACOPT_VERBOSE |
371+
VACOPT_PROCESS_TOAST |
372+
VACOPT_ONLY_DATABASE_STATS))
373+
ereport(ERROR,
374+
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
375+
errmsg("ONLY_DATABASE_STATS cannot be specified with other VACUUM options")));
376+
}
377+
352378
/*
353379
* Create special memory context for cross-transaction storage.
354380
*
@@ -376,7 +402,12 @@ vacuum(List *relations, VacuumParams *params,
376402
* Build list of relation(s) to process, putting any new data in
377403
* vac_context for safekeeping.
378404
*/
379-
if (relations != NIL)
405+
if (params->options & VACOPT_ONLY_DATABASE_STATS)
406+
{
407+
/* We don't process any tables in this case */
408+
Assert(relations == NIL);
409+
}
410+
else if (relations != NIL)
380411
{
381412
List *newrels = NIL;
382413
ListCell *lc;
@@ -528,11 +559,11 @@ vacuum(List *relations, VacuumParams *params,
528559
StartTransactionCommand();
529560
}
530561

531-
if ((params->options & VACOPT_VACUUM) && !IsAutoVacuumWorkerProcess())
562+
if ((params->options & VACOPT_VACUUM) &&
563+
!(params->options & VACOPT_SKIP_DATABASE_STATS))
532564
{
533565
/*
534566
* Update pg_database.datfrozenxid, and truncate pg_xact if possible.
535-
* (autovacuum.c does this for itself.)
536567
*/
537568
vac_update_datfrozenxid();
538569
}
@@ -560,13 +591,14 @@ vacuum_is_permitted_for_relation(Oid relid, Form_pg_class reltuple,
560591

561592
Assert((options & (VACOPT_VACUUM | VACOPT_ANALYZE)) != 0);
562593

563-
/*
594+
/*----------
564595
* A role has privileges to vacuum or analyze the relation if any of the
565596
* following are true:
566597
* - the role is a superuser
567598
* - the role owns the relation
568599
* - the role owns the current database and the relation is not shared
569600
* - the role has been granted the MAINTAIN privilege on the relation
601+
*----------
570602
*/
571603
if (object_ownercheck(RelationRelationId, relid, GetUserId()) ||
572604
(object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId()) && !reltuple->relisshared) ||

src/backend/postmaster/autovacuum.c

+7-2
Original file line numberDiff line numberDiff line change
@@ -2854,8 +2854,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
28542854
tab->at_relid = relid;
28552855
tab->at_sharedrel = classForm->relisshared;
28562856

2857-
/* Note that this skips toast relations */
2858-
tab->at_params.options = (dovacuum ? VACOPT_VACUUM : 0) |
2857+
/*
2858+
* Select VACUUM options. Note we don't say VACOPT_PROCESS_TOAST, so
2859+
* that vacuum() skips toast relations. Also note we tell vacuum() to
2860+
* skip vac_update_datfrozenxid(); we'll do that separately.
2861+
*/
2862+
tab->at_params.options =
2863+
(dovacuum ? (VACOPT_VACUUM | VACOPT_SKIP_DATABASE_STATS) : 0) |
28592864
(doanalyze ? VACOPT_ANALYZE : 0) |
28602865
(!wraparound ? VACOPT_SKIP_LOCKED : 0);
28612866

src/bin/psql/tab-complete.c

+3-2
Original file line numberDiff line numberDiff line change
@@ -4598,8 +4598,9 @@ psql_completion(const char *text, int start, int end)
45984598
COMPLETE_WITH("FULL", "FREEZE", "ANALYZE", "VERBOSE",
45994599
"DISABLE_PAGE_SKIPPING", "SKIP_LOCKED",
46004600
"INDEX_CLEANUP", "PROCESS_TOAST",
4601-
"TRUNCATE", "PARALLEL");
4602-
else if (TailMatches("FULL|FREEZE|ANALYZE|VERBOSE|DISABLE_PAGE_SKIPPING|SKIP_LOCKED|PROCESS_TOAST|TRUNCATE"))
4601+
"TRUNCATE", "PARALLEL", "SKIP_DATABASE_STATS",
4602+
"ONLY_DATABASE_STATS");
4603+
else if (TailMatches("FULL|FREEZE|ANALYZE|VERBOSE|DISABLE_PAGE_SKIPPING|SKIP_LOCKED|PROCESS_TOAST|TRUNCATE|SKIP_DATABASE_STATS|ONLY_DATABASE_STATS"))
46034604
COMPLETE_WITH("ON", "OFF");
46044605
else if (TailMatches("INDEX_CLEANUP"))
46054606
COMPLETE_WITH("AUTO", "ON", "OFF");

src/bin/scripts/t/100_vacuumdb.pl

+12-12
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,27 @@
2222
'SQL VACUUM run');
2323
$node->issues_sql_like(
2424
[ 'vacuumdb', '-f', 'postgres' ],
25-
qr/statement: VACUUM \(FULL\).*;/,
25+
qr/statement: VACUUM \(SKIP_DATABASE_STATS, FULL\).*;/,
2626
'vacuumdb -f');
2727
$node->issues_sql_like(
2828
[ 'vacuumdb', '-F', 'postgres' ],
29-
qr/statement: VACUUM \(FREEZE\).*;/,
29+
qr/statement: VACUUM \(SKIP_DATABASE_STATS, FREEZE\).*;/,
3030
'vacuumdb -F');
3131
$node->issues_sql_like(
3232
[ 'vacuumdb', '-zj2', 'postgres' ],
33-
qr/statement: VACUUM \(ANALYZE\).*;/,
33+
qr/statement: VACUUM \(SKIP_DATABASE_STATS, ANALYZE\).*;/,
3434
'vacuumdb -zj2');
3535
$node->issues_sql_like(
3636
[ 'vacuumdb', '-Z', 'postgres' ],
3737
qr/statement: ANALYZE.*;/,
3838
'vacuumdb -Z');
3939
$node->issues_sql_like(
4040
[ 'vacuumdb', '--disable-page-skipping', 'postgres' ],
41-
qr/statement: VACUUM \(DISABLE_PAGE_SKIPPING\).*;/,
41+
qr/statement: VACUUM \(DISABLE_PAGE_SKIPPING, SKIP_DATABASE_STATS\).*;/,
4242
'vacuumdb --disable-page-skipping');
4343
$node->issues_sql_like(
4444
[ 'vacuumdb', '--skip-locked', 'postgres' ],
45-
qr/statement: VACUUM \(SKIP_LOCKED\).*;/,
45+
qr/statement: VACUUM \(SKIP_DATABASE_STATS, SKIP_LOCKED\).*;/,
4646
'vacuumdb --skip-locked');
4747
$node->issues_sql_like(
4848
[ 'vacuumdb', '--skip-locked', '--analyze-only', 'postgres' ],
@@ -53,32 +53,32 @@
5353
'--analyze-only and --disable-page-skipping specified together');
5454
$node->issues_sql_like(
5555
[ 'vacuumdb', '--no-index-cleanup', 'postgres' ],
56-
qr/statement: VACUUM \(INDEX_CLEANUP FALSE\).*;/,
56+
qr/statement: VACUUM \(INDEX_CLEANUP FALSE, SKIP_DATABASE_STATS\).*;/,
5757
'vacuumdb --no-index-cleanup');
5858
$node->command_fails(
5959
[ 'vacuumdb', '--analyze-only', '--no-index-cleanup', 'postgres' ],
6060
'--analyze-only and --no-index-cleanup specified together');
6161
$node->issues_sql_like(
6262
[ 'vacuumdb', '--no-truncate', 'postgres' ],
63-
qr/statement: VACUUM \(TRUNCATE FALSE\).*;/,
63+
qr/statement: VACUUM \(TRUNCATE FALSE, SKIP_DATABASE_STATS\).*;/,
6464
'vacuumdb --no-truncate');
6565
$node->command_fails(
6666
[ 'vacuumdb', '--analyze-only', '--no-truncate', 'postgres' ],
6767
'--analyze-only and --no-truncate specified together');
6868
$node->issues_sql_like(
6969
[ 'vacuumdb', '--no-process-toast', 'postgres' ],
70-
qr/statement: VACUUM \(PROCESS_TOAST FALSE\).*;/,
70+
qr/statement: VACUUM \(PROCESS_TOAST FALSE, SKIP_DATABASE_STATS\).*;/,
7171
'vacuumdb --no-process-toast');
7272
$node->command_fails(
7373
[ 'vacuumdb', '--analyze-only', '--no-process-toast', 'postgres' ],
7474
'--analyze-only and --no-process-toast specified together');
7575
$node->issues_sql_like(
7676
[ 'vacuumdb', '-P', 2, 'postgres' ],
77-
qr/statement: VACUUM \(PARALLEL 2\).*;/,
77+
qr/statement: VACUUM \(SKIP_DATABASE_STATS, PARALLEL 2\).*;/,
7878
'vacuumdb -P 2');
7979
$node->issues_sql_like(
8080
[ 'vacuumdb', '-P', 0, 'postgres' ],
81-
qr/statement: VACUUM \(PARALLEL 0\).*;/,
81+
qr/statement: VACUUM \(SKIP_DATABASE_STATS, PARALLEL 0\).*;/,
8282
'vacuumdb -P 0');
8383
$node->command_ok([qw(vacuumdb -Z --table=pg_am dbname=template1)],
8484
'vacuumdb with connection string');
@@ -119,7 +119,7 @@
119119
'negative parallel degree');
120120
$node->issues_sql_like(
121121
[ 'vacuumdb', '--analyze', '--table', 'vactable(a, b)', 'postgres' ],
122-
qr/statement: VACUUM \(ANALYZE\) public.vactable\(a, b\);/,
122+
qr/statement: VACUUM \(SKIP_DATABASE_STATS, ANALYZE\) public.vactable\(a, b\);/,
123123
'vacuumdb --analyze with complete column list');
124124
$node->issues_sql_like(
125125
[ 'vacuumdb', '--analyze-only', '--table', 'vactable(b)', 'postgres' ],
@@ -150,7 +150,7 @@
150150
'vacuumdb --table --min-xid-age');
151151
$node->issues_sql_like(
152152
[ 'vacuumdb', '--schema', '"Foo"', 'postgres' ],
153-
qr/VACUUM "Foo".bar/,
153+
qr/VACUUM \(SKIP_DATABASE_STATS\) "Foo".bar/,
154154
'vacuumdb --schema');
155155
$node->issues_sql_like(
156156
[ 'vacuumdb', '--exclude-schema', '"Foo"', 'postgres' ],

src/bin/scripts/vacuumdb.c

+33
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ typedef struct vacuumingOptions
4444
bool force_index_cleanup;
4545
bool do_truncate;
4646
bool process_toast;
47+
bool skip_database_stats;
4748
} vacuumingOptions;
4849

4950
/* object filter options */
@@ -533,6 +534,9 @@ vacuum_one_database(ConnParams *cparams,
533534
pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
534535
"--parallel", "13");
535536

537+
/* skip_database_stats is used automatically if server supports it */
538+
vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000);
539+
536540
if (!quiet)
537541
{
538542
if (stage != ANALYZE_NO_STAGE)
@@ -790,7 +794,29 @@ vacuum_one_database(ConnParams *cparams,
790794
} while (cell != NULL);
791795

792796
if (!ParallelSlotsWaitCompletion(sa))
797+
{
793798
failed = true;
799+
goto finish;
800+
}
801+
802+
/* If we used SKIP_DATABASE_STATS, mop up with ONLY_DATABASE_STATS */
803+
if (vacopts->skip_database_stats && stage == ANALYZE_NO_STAGE)
804+
{
805+
const char *cmd = "VACUUM (ONLY_DATABASE_STATS);";
806+
ParallelSlot *free_slot = ParallelSlotsGetIdle(sa, NULL);
807+
808+
if (!free_slot)
809+
{
810+
failed = true;
811+
goto finish;
812+
}
813+
814+
ParallelSlotSetHandler(free_slot, TableCommandResultHandler, NULL);
815+
run_vacuum_command(free_slot->connection, cmd, echo, NULL);
816+
817+
if (!ParallelSlotsWaitCompletion(sa))
818+
failed = true;
819+
}
794820

795821
finish:
796822
ParallelSlotsTerminate(sa);
@@ -957,6 +983,13 @@ prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
957983
appendPQExpBuffer(sql, "%sPROCESS_TOAST FALSE", sep);
958984
sep = comma;
959985
}
986+
if (vacopts->skip_database_stats)
987+
{
988+
/* SKIP_DATABASE_STATS is supported since v16 */
989+
Assert(serverVersion >= 160000);
990+
appendPQExpBuffer(sql, "%sSKIP_DATABASE_STATS", sep);
991+
sep = comma;
992+
}
960993
if (vacopts->skip_locked)
961994
{
962995
/* SKIP_LOCKED is supported since v12 */

src/fe_utils/parallel_slot.c

+3
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,9 @@ ParallelSlotsWaitCompletion(ParallelSlotArray *sa)
475475
continue;
476476
if (!consumeQueryResult(&sa->slots[i]))
477477
return false;
478+
/* Mark connection as idle */
479+
sa->slots[i].inUse = false;
480+
ParallelSlotClearHandler(&sa->slots[i]);
478481
}
479482

480483
return true;

src/include/commands/vacuum.h

+2
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ typedef struct VacAttrStats
188188
#define VACOPT_SKIP_LOCKED 0x20 /* skip if cannot get lock */
189189
#define VACOPT_PROCESS_TOAST 0x40 /* process the TOAST table, if any */
190190
#define VACOPT_DISABLE_PAGE_SKIPPING 0x80 /* don't skip any pages */
191+
#define VACOPT_SKIP_DATABASE_STATS 0x100 /* skip vac_update_datfrozenxid() */
192+
#define VACOPT_ONLY_DATABASE_STATS 0x200 /* only vac_update_datfrozenxid() */
191193

192194
/*
193195
* Values used by index_cleanup and truncate params.

src/test/regress/expected/vacuum.out

+6
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ ALTER TABLE vactst ALTER COLUMN t SET STORAGE EXTERNAL;
282282
VACUUM (PROCESS_TOAST FALSE) vactst;
283283
VACUUM (PROCESS_TOAST FALSE, FULL) vactst;
284284
ERROR: PROCESS_TOAST required with VACUUM FULL
285+
-- SKIP_DATABASE_STATS option
286+
VACUUM (SKIP_DATABASE_STATS) vactst;
287+
-- ONLY_DATABASE_STATS option
288+
VACUUM (ONLY_DATABASE_STATS);
289+
VACUUM (ONLY_DATABASE_STATS) vactst; -- error
290+
ERROR: ONLY_DATABASE_STATS cannot be specified with a list of tables
285291
DROP TABLE vaccluster;
286292
DROP TABLE vactst;
287293
DROP TABLE vacparted;

src/test/regress/sql/vacuum.sql

+7
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ ALTER TABLE vactst ALTER COLUMN t SET STORAGE EXTERNAL;
237237
VACUUM (PROCESS_TOAST FALSE) vactst;
238238
VACUUM (PROCESS_TOAST FALSE, FULL) vactst;
239239

240+
-- SKIP_DATABASE_STATS option
241+
VACUUM (SKIP_DATABASE_STATS) vactst;
242+
243+
-- ONLY_DATABASE_STATS option
244+
VACUUM (ONLY_DATABASE_STATS);
245+
VACUUM (ONLY_DATABASE_STATS) vactst; -- error
246+
240247
DROP TABLE vaccluster;
241248
DROP TABLE vactst;
242249
DROP TABLE vacparted;

0 commit comments

Comments
 (0)