Skip to content

Commit e0c2933

Browse files
committed
Use catalog query to discover tables to process in vacuumdb
vacuumdb would use a catalog query only when the command caller does not define a list of tables. Switching to a catalog table represents two advantages: - Relation existence check can happen before running any VACUUM or ANALYZE query. Before this change, if multiple relations are defined using --table, the utility would fail only after processing the firstly-defined ones, which may be a long some depending on the size of the relation. This adds checks for the relation names, and does nothing, at least yet, for the attribute names. - More filtering options can become available for the utility user. These options, which may be introduced later on, are based on the relation size or the relation age, and need to be made available even if the user does not list any specific table with --table. Author: Nathan Bossart Reviewed-by: Michael Paquier, Masahiko Sawada Discussion: https://postgr.es/m/FFE5373C-E26A-495B-B5C8-911EC4A41C5E@amazon.com
1 parent da05eb5 commit e0c2933

File tree

3 files changed

+147
-74
lines changed

3 files changed

+147
-74
lines changed

src/bin/scripts/common.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,9 @@ executeMaintenanceCommand(PGconn *conn, const char *query, bool echo)
265265
* finish using them, pg_free(*table). *columns is a pointer into "spec",
266266
* possibly to its NUL terminator.
267267
*/
268-
static void
269-
split_table_columns_spec(const char *spec, int encoding,
270-
char **table, const char **columns)
268+
void
269+
splitTableColumnsSpec(const char *spec, int encoding,
270+
char **table, const char **columns)
271271
{
272272
bool inquotes = false;
273273
const char *cp = spec;
@@ -318,7 +318,7 @@ appendQualifiedRelation(PQExpBuffer buf, const char *spec,
318318
return;
319319
}
320320

321-
split_table_columns_spec(spec, PQclientEncoding(conn), &table, &columns);
321+
splitTableColumnsSpec(spec, PQclientEncoding(conn), &table, &columns);
322322

323323
/*
324324
* Query must remain ABSOLUTELY devoid of unqualified names. This would

src/bin/scripts/common.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ extern void executeCommand(PGconn *conn, const char *query,
4848
extern bool executeMaintenanceCommand(PGconn *conn, const char *query,
4949
bool echo);
5050

51+
extern void splitTableColumnsSpec(const char *spec, int encoding,
52+
char **table, const char **columns);
53+
5154
extern void appendQualifiedRelation(PQExpBuffer buf, const char *name,
5255
PGconn *conn, const char *progname, bool echo);
5356

src/bin/scripts/vacuumdb.c

Lines changed: 140 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "catalog/pg_class_d.h"
2020

2121
#include "common.h"
22+
#include "fe_utils/connect.h"
2223
#include "fe_utils/simple_list.h"
2324
#include "fe_utils/string_utils.h"
2425

@@ -61,10 +62,8 @@ static void vacuum_all_databases(vacuumingOptions *vacopts,
6162
int concurrentCons,
6263
const char *progname, bool echo, bool quiet);
6364

64-
static void prepare_vacuum_command(PQExpBuffer sql, PGconn *conn,
65-
vacuumingOptions *vacopts, const char *table,
66-
bool table_pre_qualified,
67-
const char *progname, bool echo);
65+
static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
66+
vacuumingOptions *vacopts, const char *table);
6867

6968
static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
7069
const char *table, const char *progname, bool async);
@@ -359,13 +358,18 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
359358
const char *progname, bool echo, bool quiet)
360359
{
361360
PQExpBufferData sql;
361+
PQExpBufferData buf;
362+
PQExpBufferData catalog_query;
363+
PGresult *res;
362364
PGconn *conn;
363365
SimpleStringListCell *cell;
364366
ParallelSlot *slots;
365367
SimpleStringList dbtables = {NULL, NULL};
366368
int i;
369+
int ntups;
367370
bool failed = false;
368371
bool parallel = concurrentCons > 1;
372+
bool tables_listed = false;
369373
const char *stage_commands[] = {
370374
"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
371375
"SET default_statistics_target=10; RESET vacuum_cost_delay;",
@@ -410,53 +414,132 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
410414
fflush(stdout);
411415
}
412416

413-
initPQExpBuffer(&sql);
414-
415417
/*
416-
* If a table list is not provided and we're using multiple connections,
417-
* prepare the list of tables by querying the catalogs.
418+
* Prepare the list of tables to process by querying the catalogs.
419+
*
420+
* Since we execute the constructed query with the default search_path
421+
* (which could be unsafe), everything in this query MUST be fully
422+
* qualified.
423+
*
424+
* First, build a WITH clause for the catalog query if any tables were
425+
* specified, with a set of values made of relation names and their
426+
* optional set of columns. This is used to match any provided column
427+
* lists with the generated qualified identifiers and to filter for the
428+
* tables provided via --table. If a listed table does not exist, the
429+
* catalog query will fail.
418430
*/
419-
if (parallel && (!tables || !tables->head))
431+
initPQExpBuffer(&catalog_query);
432+
for (cell = tables ? tables->head : NULL; cell; cell = cell->next)
420433
{
421-
PQExpBufferData buf;
422-
PGresult *res;
423-
int ntups;
424-
425-
initPQExpBuffer(&buf);
426-
427-
res = executeQuery(conn,
428-
"SELECT c.relname, ns.nspname"
429-
" FROM pg_class c, pg_namespace ns\n"
430-
" WHERE relkind IN ("
431-
CppAsString2(RELKIND_RELATION) ", "
432-
CppAsString2(RELKIND_MATVIEW) ")"
433-
" AND c.relnamespace = ns.oid\n"
434-
" ORDER BY c.relpages DESC;",
435-
progname, echo);
436-
437-
ntups = PQntuples(res);
438-
for (i = 0; i < ntups; i++)
439-
{
440-
appendPQExpBufferStr(&buf,
441-
fmtQualifiedId(PQgetvalue(res, i, 1),
442-
PQgetvalue(res, i, 0)));
434+
char *just_table;
435+
const char *just_columns;
443436

444-
simple_string_list_append(&dbtables, buf.data);
445-
resetPQExpBuffer(&buf);
437+
/*
438+
* Split relation and column names given by the user, this is used to
439+
* feed the CTE with values on which are performed pre-run validity
440+
* checks as well. For now these happen only on the relation name.
441+
*/
442+
splitTableColumnsSpec(cell->val, PQclientEncoding(conn),
443+
&just_table, &just_columns);
444+
445+
if (!tables_listed)
446+
{
447+
appendPQExpBuffer(&catalog_query,
448+
"WITH listed_tables (table_oid, column_list) "
449+
"AS (\n VALUES (");
450+
tables_listed = true;
446451
}
452+
else
453+
appendPQExpBuffer(&catalog_query, ",\n (");
447454

448-
termPQExpBuffer(&buf);
449-
tables = &dbtables;
455+
appendStringLiteralConn(&catalog_query, just_table, conn);
456+
appendPQExpBuffer(&catalog_query, "::pg_catalog.regclass, ");
450457

451-
/*
452-
* If there are more connections than vacuumable relations, we don't
453-
* need to use them all.
454-
*/
458+
if (just_columns && just_columns[0] != '\0')
459+
appendStringLiteralConn(&catalog_query, just_columns, conn);
460+
else
461+
appendPQExpBufferStr(&catalog_query, "NULL");
462+
463+
appendPQExpBufferStr(&catalog_query, "::pg_catalog.text)");
464+
465+
pg_free(just_table);
466+
}
467+
468+
/* Finish formatting the CTE */
469+
if (tables_listed)
470+
appendPQExpBuffer(&catalog_query, "\n)\n");
471+
472+
appendPQExpBuffer(&catalog_query, "SELECT c.relname, ns.nspname");
473+
474+
if (tables_listed)
475+
appendPQExpBuffer(&catalog_query, ", listed_tables.column_list");
476+
477+
appendPQExpBuffer(&catalog_query,
478+
" FROM pg_catalog.pg_class c\n"
479+
" JOIN pg_catalog.pg_namespace ns"
480+
" ON c.relnamespace OPERATOR(pg_catalog.=) ns.oid\n");
481+
482+
/* Used to match the tables listed by the user */
483+
if (tables_listed)
484+
appendPQExpBuffer(&catalog_query, " JOIN listed_tables"
485+
" ON listed_tables.table_oid OPERATOR(pg_catalog.=) c.oid\n");
486+
487+
appendPQExpBuffer(&catalog_query, " WHERE c.relkind OPERATOR(pg_catalog.=) ANY (array["
488+
CppAsString2(RELKIND_RELATION) ", "
489+
CppAsString2(RELKIND_MATVIEW) "])\n");
490+
491+
/*
492+
* Execute the catalog query. We use the default search_path for this
493+
* query for consistency with table lookups done elsewhere by the user.
494+
*/
495+
appendPQExpBuffer(&catalog_query, " ORDER BY c.relpages DESC;");
496+
executeCommand(conn, "RESET search_path;", progname, echo);
497+
res = executeQuery(conn, catalog_query.data, progname, echo);
498+
termPQExpBuffer(&catalog_query);
499+
PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL,
500+
progname, echo));
501+
502+
/*
503+
* If no rows are returned, there are no matching tables, so we are done.
504+
*/
505+
ntups = PQntuples(res);
506+
if (ntups == 0)
507+
{
508+
PQclear(res);
509+
PQfinish(conn);
510+
return;
511+
}
512+
513+
/*
514+
* Build qualified identifiers for each table, including the column list
515+
* if given.
516+
*/
517+
initPQExpBuffer(&buf);
518+
for (i = 0; i < ntups; i++)
519+
{
520+
appendPQExpBufferStr(&buf,
521+
fmtQualifiedId(PQgetvalue(res, i, 1),
522+
PQgetvalue(res, i, 0)));
523+
524+
if (tables_listed && !PQgetisnull(res, i, 2))
525+
appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
526+
527+
simple_string_list_append(&dbtables, buf.data);
528+
resetPQExpBuffer(&buf);
529+
}
530+
termPQExpBuffer(&buf);
531+
PQclear(res);
532+
533+
/*
534+
* If there are more connections than vacuumable relations, we don't need
535+
* to use them all.
536+
*/
537+
if (parallel)
538+
{
455539
if (concurrentCons > ntups)
456540
concurrentCons = ntups;
457541
if (concurrentCons <= 1)
458542
parallel = false;
459-
PQclear(res);
460543
}
461544

462545
/*
@@ -493,10 +576,12 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
493576
stage_commands[stage], progname, echo);
494577
}
495578

496-
cell = tables ? tables->head : NULL;
579+
initPQExpBuffer(&sql);
580+
581+
cell = dbtables.head;
497582
do
498583
{
499-
const char *tabname = cell ? cell->val : NULL;
584+
const char *tabname = cell->val;
500585
ParallelSlot *free_slot;
501586

502587
if (CancelRequested)
@@ -529,12 +614,8 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
529614
else
530615
free_slot = slots;
531616

532-
/*
533-
* Prepare the vacuum command. Note that in some cases this requires
534-
* query execution, so be sure to use the free connection.
535-
*/
536-
prepare_vacuum_command(&sql, free_slot->connection, vacopts, tabname,
537-
tables == &dbtables, progname, echo);
617+
prepare_vacuum_command(&sql, PQserverVersion(free_slot->connection),
618+
vacopts, tabname);
538619

539620
/*
540621
* Execute the vacuum. If not in parallel mode, this terminates the
@@ -544,8 +625,7 @@ vacuum_one_database(const char *dbname, vacuumingOptions *vacopts,
544625
run_vacuum_command(free_slot->connection, sql.data,
545626
echo, tabname, progname, parallel);
546627

547-
if (cell)
548-
cell = cell->next;
628+
cell = cell->next;
549629
} while (cell != NULL);
550630

551631
if (parallel)
@@ -653,14 +733,12 @@ vacuum_all_databases(vacuumingOptions *vacopts,
653733
* Construct a vacuum/analyze command to run based on the given options, in the
654734
* given string buffer, which may contain previous garbage.
655735
*
656-
* An optional table name can be passed; this must be already be properly
657-
* quoted. The command is semicolon-terminated.
736+
* The table name used must be already properly quoted. The command generated
737+
* depends on the server version involved and it is semicolon-terminated.
658738
*/
659739
static void
660-
prepare_vacuum_command(PQExpBuffer sql, PGconn *conn,
661-
vacuumingOptions *vacopts, const char *table,
662-
bool table_pre_qualified,
663-
const char *progname, bool echo)
740+
prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
741+
vacuumingOptions *vacopts, const char *table)
664742
{
665743
const char *paren = " (";
666744
const char *comma = ", ";
@@ -673,12 +751,12 @@ prepare_vacuum_command(PQExpBuffer sql, PGconn *conn,
673751
appendPQExpBufferStr(sql, "ANALYZE");
674752

675753
/* parenthesized grammar of ANALYZE is supported since v11 */
676-
if (PQserverVersion(conn) >= 110000)
754+
if (serverVersion >= 110000)
677755
{
678756
if (vacopts->skip_locked)
679757
{
680758
/* SKIP_LOCKED is supported since v12 */
681-
Assert(PQserverVersion(conn) >= 120000);
759+
Assert(serverVersion >= 120000);
682760
appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep);
683761
sep = comma;
684762
}
@@ -701,19 +779,19 @@ prepare_vacuum_command(PQExpBuffer sql, PGconn *conn,
701779
appendPQExpBufferStr(sql, "VACUUM");
702780

703781
/* parenthesized grammar of VACUUM is supported since v9.0 */
704-
if (PQserverVersion(conn) >= 90000)
782+
if (serverVersion >= 90000)
705783
{
706784
if (vacopts->disable_page_skipping)
707785
{
708786
/* DISABLE_PAGE_SKIPPING is supported since v9.6 */
709-
Assert(PQserverVersion(conn) >= 90600);
787+
Assert(serverVersion >= 90600);
710788
appendPQExpBuffer(sql, "%sDISABLE_PAGE_SKIPPING", sep);
711789
sep = comma;
712790
}
713791
if (vacopts->skip_locked)
714792
{
715793
/* SKIP_LOCKED is supported since v12 */
716-
Assert(PQserverVersion(conn) >= 120000);
794+
Assert(serverVersion >= 120000);
717795
appendPQExpBuffer(sql, "%sSKIP_LOCKED", sep);
718796
sep = comma;
719797
}
@@ -753,15 +831,7 @@ prepare_vacuum_command(PQExpBuffer sql, PGconn *conn,
753831
}
754832
}
755833

756-
if (table)
757-
{
758-
appendPQExpBufferChar(sql, ' ');
759-
if (table_pre_qualified)
760-
appendPQExpBufferStr(sql, table);
761-
else
762-
appendQualifiedRelation(sql, table, conn, progname, echo);
763-
}
764-
appendPQExpBufferChar(sql, ';');
834+
appendPQExpBuffer(sql, " %s;", table);
765835
}
766836

767837
/*

0 commit comments

Comments
 (0)