diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml
index 45dee10..9843658 100644
--- a/doc/src/sgml/ref/analyze.sgml
+++ b/doc/src/sgml/ref/analyze.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-ANALYZE [ VERBOSE ] [ <replaceable class="PARAMETER">table_name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ] ]
+ANALYZE [ VERBOSE ] [ <replaceable class="PARAMETER">table_name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ] ] [, ...]
 </synopsis>
  </refsynopsisdiv>
 
@@ -38,10 +38,11 @@ ANALYZE [ VERBOSE ] [ <replaceable class="PARAMETER">table_name</replaceable> [
   </para>
 
   <para>
-   With no parameter, <command>ANALYZE</command> examines every table in the
-   current database.  With a parameter, <command>ANALYZE</command> examines
-   only that table.  It is further possible to give a list of column names,
-   in which case only the statistics for those columns are collected.
+   With no parameters, <command>ANALYZE</command> examines every table in the
+   current database.  With parameters, <command>ANALYZE</command> examines
+   only the tables specified.  It is further possible to give a list of
+   column names for each table, in which case only the statistics for those
+   columns are collected.
   </para>
  </refsect1>
 
diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index 421c18d..d4d9eaa 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -21,9 +21,9 @@ PostgreSQL documentation
 
  <refsynopsisdiv>
 <synopsis>
-VACUUM [ ( { FULL | FREEZE | VERBOSE | ANALYZE | DISABLE_PAGE_SKIPPING } [, ...] ) ] [ <replaceable class="PARAMETER">table_name</replaceable> [ (<replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ] ]
-VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ <replaceable class="PARAMETER">table_name</replaceable> ]
-VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] ANALYZE [ <replaceable class="PARAMETER">table_name</replaceable> [ (<replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ] ]
+VACUUM [ ( { FULL | FREEZE | VERBOSE | ANALYZE | DISABLE_PAGE_SKIPPING } [, ...] ) ] [ <replaceable class="PARAMETER">table_name</replaceable> [ (<replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ] ] [, ...]
+VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ <replaceable class="PARAMETER">table_name</replaceable> ] [, ...]
+VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] ANALYZE [ <replaceable class="PARAMETER">table_name</replaceable> [ (<replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ] ] [, ...]
 </synopsis>
  </refsynopsisdiv>
 
@@ -40,9 +40,10 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] ANALYZE [ <replaceable class="PARAMETER">
   </para>
 
   <para>
-   With no parameter, <command>VACUUM</command> processes every table in the
+   With no parameters, <command>VACUUM</command> processes every table in the
    current database that the current user has permission to vacuum.
-   With a parameter, <command>VACUUM</command> processes only that table.
+   With parameters, <command>VACUUM</command> processes only the tables
+   specified.
   </para>
 
   <para>
@@ -165,7 +166,8 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] ANALYZE [ <replaceable class="PARAMETER">
     <listitem>
      <para>
       The name of a specific column to analyze. Defaults to all columns.
-      If a column list is specified, <literal>ANALYZE</> is implied.
+      If a column list is specified, <literal>ANALYZE</> must also be
+      specified.
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 2b63827..d9605af 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -152,7 +152,19 @@ analyze_rel(Oid relid, RangeVar *relation, int options,
 							relation->relname)));
 	}
 	if (!onerel)
+	{
+		/*
+		 * If one of the relations specified by the user has disappeared
+		 * since we last looked it up, let them know so that they do not
+		 * think it was actually analyzed.
+		 */
+		if (!IsAutoVacuumWorkerProcess() && relation)
+			ereport(WARNING,
+				  (errmsg("skipping \"%s\" --- relation no longer exists",
+						  relation->relname)));
+
 		return;
+	}
 
 	/*
 	 * Check permissions --- this should match vacuum's check!
@@ -383,12 +395,22 @@ do_analyze_rel(Relation onerel, int options, VacuumParams *params,
 		{
 			char	   *col = strVal(lfirst(le));
 
+			/*
+			 * We have already checked the column list in vacuum(...),
+			 * but the columns may have disappeared since then.  If
+			 * this happens, emit a nice warning message and skip the
+			 * undefined column.
+			 */
 			i = attnameAttNum(onerel, col, false);
 			if (i == InvalidAttrNumber)
-				ereport(ERROR,
+			{
+				ereport(WARNING,
 						(errcode(ERRCODE_UNDEFINED_COLUMN),
-						 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						 errmsg("skipping column \"%s\" of relation \"%s\" because the column no longer exists",
 								col, RelationGetRelationName(onerel))));
+				continue;
+			}
+
 			vacattrstats[tcnt] = examine_attribute(onerel, i, NULL);
 			if (vacattrstats[tcnt] != NULL)
 				tcnt++;
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index faa1812..16d585d 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -37,6 +37,7 @@
 #include "commands/cluster.h"
 #include "commands/vacuum.h"
 #include "miscadmin.h"
+#include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "postmaster/autovacuum.h"
 #include "storage/bufmgr.h"
@@ -67,7 +68,9 @@ static BufferAccessStrategy vac_strategy;
 
 
 /* non-export function prototypes */
-static List *get_rel_oids(Oid relid, const RangeVar *vacrel);
+static int get_rel_oids(List **vacrels);
+static void check_columns_exist(RelationAndColumns *relation);
+static List *dedupe_relations(List *relations);
 static void vac_truncate_clog(TransactionId frozenXID,
 				  MultiXactId minMulti,
 				  TransactionId lastSaneFrozenXid,
@@ -90,7 +93,6 @@ ExecVacuum(VacuumStmt *vacstmt, bool isTopLevel)
 	Assert(vacstmt->options & (VACOPT_VACUUM | VACOPT_ANALYZE));
 	Assert((vacstmt->options & VACOPT_VACUUM) ||
 		   !(vacstmt->options & (VACOPT_FULL | VACOPT_FREEZE)));
-	Assert((vacstmt->options & VACOPT_ANALYZE) || vacstmt->va_cols == NIL);
 	Assert(!(vacstmt->options & VACOPT_SKIPTOAST));
 
 	/*
@@ -119,8 +121,7 @@ ExecVacuum(VacuumStmt *vacstmt, bool isTopLevel)
 	params.log_min_duration = -1;
 
 	/* Now go through the common routine */
-	vacuum(vacstmt->options, vacstmt->relation, InvalidOid, &params,
-		   vacstmt->va_cols, NULL, isTopLevel);
+	vacuum(vacstmt->options, vacstmt->rels, &params, NULL, isTopLevel);
 }
 
 /*
@@ -128,15 +129,12 @@ ExecVacuum(VacuumStmt *vacstmt, bool isTopLevel)
  *
  * options is a bitmask of VacuumOption flags, indicating what to do.
  *
- * relid, if not InvalidOid, indicate the relation to process; otherwise,
- * the RangeVar is used.  (The latter must always be passed, because it's
- * used for error messages.)
+ * If we intend to process all relations, the 'relations' argument may be
+ * NIL.
  *
  * params contains a set of parameters that can be used to customize the
  * behavior.
  *
- * va_cols is a list of columns to analyze, or NIL to process them all.
- *
  * bstrategy is normally given as NULL, but in autovacuum it can be passed
  * in to use the same buffer strategy object across multiple vacuum() calls.
  *
@@ -146,14 +144,17 @@ ExecVacuum(VacuumStmt *vacstmt, bool isTopLevel)
  * memory context that will not disappear at transaction commit.
  */
 void
-vacuum(int options, RangeVar *relation, Oid relid, VacuumParams *params,
-	   List *va_cols, BufferAccessStrategy bstrategy, bool isTopLevel)
+vacuum(int options, List *relations, VacuumParams *params,
+	   BufferAccessStrategy bstrategy, bool isTopLevel)
 {
-	const char *stmttype;
-	volatile bool in_outer_xact,
-				use_own_xacts;
-	List	   *relations;
-	static bool in_vacuum = false;
+	const char	*stmttype;
+	volatile bool	 in_outer_xact,
+			 use_own_xacts;
+	static bool	 in_vacuum = false;
+	MemoryContext	 oldcontext;
+	List		*temp_relations = NIL;  /* for copying the relations list */
+	ListCell	*relation;
+	int		 oid_count;
 
 	Assert(params != NULL);
 
@@ -196,6 +197,18 @@ vacuum(int options, RangeVar *relation, Oid relid, VacuumParams *params,
 				 errmsg("VACUUM option DISABLE_PAGE_SKIPPING cannot be used with FULL")));
 
 	/*
+	 * Make sure VACOPT_ANALYZE is specified if any column lists are present.
+	 */
+	foreach(relation, relations)
+	{
+		if (((RelationAndColumns *) lfirst(relation))->va_cols != NIL &&
+			!(options & VACOPT_ANALYZE))
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("ANALYZE option must be specified when a column list is provided")));
+	}
+
+	/*
 	 * Send info about dead objects to the statistics collector, unless we are
 	 * in autovacuum --- autovacuum.c does this for itself.
 	 */
@@ -226,10 +239,39 @@ vacuum(int options, RangeVar *relation, Oid relid, VacuumParams *params,
 	vac_strategy = bstrategy;
 
 	/*
+	 * Move our relation list to our special memory context so that we do
+	 * not lose it among our per-relation transactions.
+	 */
+	oldcontext = MemoryContextSwitchTo(vac_context);
+	foreach(relation, relations)
+		temp_relations = lappend(temp_relations, copyObject(lfirst(relation)));
+	MemoryContextSwitchTo(oldcontext);
+	relations = temp_relations;
+
+	/*
 	 * Build list of relations to process, unless caller gave us one. (If we
 	 * build one, we put it in vac_context for safekeeping.)
 	 */
-	relations = get_rel_oids(relid, relation);
+	oid_count = get_rel_oids(&relations);
+
+	/*
+	 * Check that all specified columns exist so that we can fast-fail
+	 * commands with multiple tables.  If the column disappears before we
+	 * actually process it, we will emit a WARNING and skip it later on.
+	 */
+	foreach(relation, relations)
+		check_columns_exist(lfirst(relation));
+
+	/*
+	 * Now dedupe the list to avoid any redundant work (e.g. user specifies
+	 * the same relation twice).  We also take care of combining any
+	 * separate column lists for duplicate relations.
+	 *
+	 * We do this after resolving the OIDs so that we do not miss entries
+	 * that have unequal RangeVars but resolve to the same set of OIDs.
+	 * For example, "foo" and "public.foo" could be the same relation.
+	 */
+	relations = dedupe_relations(relations);
 
 	/*
 	 * Decide whether we need to start/commit our own transactions.
@@ -254,7 +296,7 @@ vacuum(int options, RangeVar *relation, Oid relid, VacuumParams *params,
 			use_own_xacts = true;
 		else if (in_outer_xact)
 			use_own_xacts = false;
-		else if (list_length(relations) > 1)
+		else if (oid_count > 1)
 			use_own_xacts = true;
 		else
 			use_own_xacts = false;
@@ -283,8 +325,6 @@ vacuum(int options, RangeVar *relation, Oid relid, VacuumParams *params,
 	/* Turn vacuum cost accounting on or off */
 	PG_TRY();
 	{
-		ListCell   *cur;
-
 		in_vacuum = true;
 		VacuumCostActive = (VacuumCostDelay > 0);
 		VacuumCostBalance = 0;
@@ -295,36 +335,40 @@ vacuum(int options, RangeVar *relation, Oid relid, VacuumParams *params,
 		/*
 		 * Loop to process each selected relation.
 		 */
-		foreach(cur, relations)
+		foreach(relation, relations)
 		{
-			Oid			relid = lfirst_oid(cur);
-
-			if (options & VACOPT_VACUUM)
-			{
-				if (!vacuum_rel(relid, relation, options, params))
-					continue;
-			}
+			RelationAndColumns *relinfo = (RelationAndColumns *) lfirst(relation);
+			ListCell *oid;
 
-			if (options & VACOPT_ANALYZE)
+			foreach(oid, relinfo->oids)
 			{
-				/*
-				 * If using separate xacts, start one for analyze. Otherwise,
-				 * we can use the outer transaction.
-				 */
-				if (use_own_xacts)
+				if (options & VACOPT_VACUUM)
 				{
-					StartTransactionCommand();
-					/* functions in indexes may want a snapshot set */
-					PushActiveSnapshot(GetTransactionSnapshot());
+					if (!vacuum_rel(lfirst_oid(oid), relinfo->relation, options, params))
+						continue;
 				}
 
-				analyze_rel(relid, relation, options, params,
-							va_cols, in_outer_xact, vac_strategy);
-
-				if (use_own_xacts)
+				if (options & VACOPT_ANALYZE)
 				{
-					PopActiveSnapshot();
-					CommitTransactionCommand();
+					/*
+					 * If using separate xacts, start one for analyze. Otherwise,
+					 * we can use the outer transaction.
+					 */
+					if (use_own_xacts)
+					{
+						StartTransactionCommand();
+						/* functions in indexes may want a snapshot set */
+						PushActiveSnapshot(GetTransactionSnapshot());
+					}
+
+					analyze_rel(lfirst_oid(oid), relinfo->relation, options, params,
+								relinfo->va_cols, in_outer_xact, vac_strategy);
+
+					if (use_own_xacts)
+					{
+						PopActiveSnapshot();
+						CommitTransactionCommand();
+					}
 				}
 			}
 		}
@@ -378,62 +422,71 @@ vacuum(int options, RangeVar *relation, Oid relid, VacuumParams *params,
  * The list is built in vac_context so that it will survive across our
  * per-relation transactions.
  */
-static List *
-get_rel_oids(Oid relid, const RangeVar *vacrel)
+static int
+get_rel_oids(List **vacrels)
 {
-	List	   *oid_list = NIL;
+	RelationAndColumns *relinfo;
 	MemoryContext oldcontext;
+	int oid_count = 0;
 
-	/* OID supplied by VACUUM's caller? */
-	if (OidIsValid(relid))
+	if (*vacrels != NIL)
 	{
-		oldcontext = MemoryContextSwitchTo(vac_context);
-		oid_list = lappend_oid(oid_list, relid);
-		MemoryContextSwitchTo(oldcontext);
-	}
-	else if (vacrel)
-	{
-		/* Process a specific relation */
-		Oid			relid;
-		HeapTuple	tuple;
-		Form_pg_class classForm;
-		bool		include_parts;
+		/* Process specific relation(s) */
+		ListCell *lc;
 
-		/*
-		 * Since we don't take a lock here, the relation might be gone, or the
-		 * RangeVar might no longer refer to the OID we look up here.  In the
-		 * former case, VACUUM will do nothing; in the latter case, it will
-		 * process the OID we looked up here, rather than the new one. Neither
-		 * is ideal, but there's little practical alternative, since we're
-		 * going to commit this transaction and begin a new one between now
-		 * and then.
-		 */
-		relid = RangeVarGetRelid(vacrel, NoLock, false);
+		foreach(lc, *vacrels)
+		{
+			Oid			 relid;
+			HeapTuple		 tuple;
+			Form_pg_class		 classForm;
+			bool			 include_parts;
 
-		/*
-		 * To check whether the relation is a partitioned table, fetch its
-		 * syscache entry.
-		 */
-		tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
-		if (!HeapTupleIsValid(tuple))
-			elog(ERROR, "cache lookup failed for relation %u", relid);
-		classForm = (Form_pg_class) GETSTRUCT(tuple);
-		include_parts = (classForm->relkind == RELKIND_PARTITIONED_TABLE);
-		ReleaseSysCache(tuple);
+			relinfo = (RelationAndColumns *) lfirst(lc);
 
-		/*
-		 * Make relation list entries for this guy and its partitions, if any.
-		 * Note that the list returned by find_all_inheritors() include the
-		 * passed-in OID at its head.  Also note that we did not request a
-		 * lock to be taken to match what would be done otherwise.
-		 */
-		oldcontext = MemoryContextSwitchTo(vac_context);
-		if (include_parts)
-			oid_list = list_concat(oid_list,
-								   find_all_inheritors(relid, NoLock, NULL));
-		else
-			oid_list = lappend_oid(oid_list, relid);
-		MemoryContextSwitchTo(oldcontext);
+			/* OIDs supplied by VACUUM's caller? */
+			if (relinfo->oids != NIL)
+			{
+				oid_count += list_length(relinfo->oids);
+				continue;
+			}
+
+			/*
+			 * Since we don't take a lock here, the relation might be gone, or the
+			 * RangeVar might no longer refer to the OID we look up here.  In the
+			 * former case, VACUUM will do nothing; in the latter case, it will
+			 * process the OID we looked up here, rather than the new one. Neither
+			 * is ideal, but there's little practical alternative, since we're
+			 * going to commit this transaction and begin a new one between now
+			 * and then.
+			 */
+			relid = RangeVarGetRelid(relinfo->relation, NoLock, false);
+
+			/*
+			 * To check whether the relation is a partitioned table, fetch its
+			 * syscache entry.
+			 */
+			tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+			if (!HeapTupleIsValid(tuple))
+				elog(ERROR, "cache lookup failed for relation %u", relid);
+			classForm = (Form_pg_class) GETSTRUCT(tuple);
+			include_parts = (classForm->relkind == RELKIND_PARTITIONED_TABLE);
+			ReleaseSysCache(tuple);
+
+			/*
+			 * Make relation list entries for this guy and its partitions, if any.
+			 * Note that the list returned by find_all_inheritors() include the
+			 * passed-in OID at its head.  Also note that we did not request a
+			 * lock to be taken to match what would be done otherwise.
+			 */
+			oldcontext = MemoryContextSwitchTo(vac_context);
+			if (include_parts)
+				relinfo->oids = find_all_inheritors(relid, NoLock, NULL);
+			else
+				relinfo->oids = list_make1_oid(relid);
+			MemoryContextSwitchTo(oldcontext);
+
+			oid_count += list_length(relinfo->oids);
+		}
 	}
 	else
 	{
@@ -465,7 +518,19 @@ get_rel_oids(Oid relid, const RangeVar *vacrel)
 
 			/* Make a relation list entry for this guy */
 			oldcontext = MemoryContextSwitchTo(vac_context);
-			oid_list = lappend_oid(oid_list, HeapTupleGetOid(tuple));
+
+			/*
+			 * It is safe to leave everything except the OID empty here.
+			 * Since no tables were specified in the VacuumStmt, we know
+			 * we don't have any columns to keep track of.  Also, we do
+			 * not need the RangeVar, because it is only used for error
+			 * messaging when specific relations are chosen.
+			 */
+			relinfo = makeNode(RelationAndColumns);
+			relinfo->oids = list_make1_oid(HeapTupleGetOid(tuple));
+			*vacrels = lappend(*vacrels, relinfo);
+			++oid_count;
+
 			MemoryContextSwitchTo(oldcontext);
 		}
 
@@ -473,7 +538,113 @@ get_rel_oids(Oid relid, const RangeVar *vacrel)
 		heap_close(pgclass, AccessShareLock);
 	}
 
-	return oid_list;
+	return oid_count;
+}
+
+/*
+ * Check that all specified columns for the relation exist.
+ *
+ * This function emits an ERROR if the relation or the columns specified for
+ * that relation cannot be found.  This is used to pre-validate any column lists
+ * specified in VACUUM/ANALYZE commands.  If any columns disappear between
+ * calling this function and when we actually get to processing them, we will
+ * emit a WARNING and skip it at that time.
+ */
+static void
+check_columns_exist(RelationAndColumns *relation)
+{
+	ListCell *oid;
+
+	if (relation->va_cols == NIL)  /* nothing to check */
+		return;
+
+	foreach(oid, relation->oids)
+	{
+		Relation rel;
+		ListCell *lc;
+
+		rel = try_relation_open(lfirst_oid(oid), NoLock);
+		if (!rel)
+		{
+			if (relation->relation->schemaname)
+				ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_TABLE),
+					 errmsg("relation \"%s.%s\" does not exist",
+						relation->relation->schemaname,
+						relation->relation->relname)));
+			else
+				ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_TABLE),
+					 errmsg("relation \"%s\" does not exist",
+						relation->relation->relname)));
+		}
+
+		foreach(lc, relation->va_cols)
+		{
+			char *col = strVal(lfirst(lc));
+			int i = attnameAttNum(rel, col, false);
+
+			if (i == InvalidAttrNumber)
+				ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						col, RelationGetRelationName(rel))));
+		}
+
+		relation_close(rel, NoLock);
+	}
+}
+
+/*
+ * De-duplicate all relations and columns specified.  The OIDs for each relation
+ * must already be determined before calling this function.
+ */
+static List *
+dedupe_relations(List *relations)
+{
+	int i = 0;
+	List *duplicates = NIL;  /* indexes of entries to remove */
+	ListCell *lc;
+	List *temp_relations = NIL;
+
+	if (relations == NIL)  /* nothing to do */
+		return relations;
+
+	/* mark any duplicates and combine the column lists into the first match */
+	foreach(lc, relations)
+	{
+		RelationAndColumns *relation = lfirst(lc);
+		int j;
+
+		for (j = ++i; j < list_length(relations); ++j)
+		{
+			RelationAndColumns *nth_rel = list_nth_node(RelationAndColumns, relations, j);
+
+			/* if already processed or not equal, skip */
+			if (list_member_int(duplicates, j) || !equal(relation->oids, nth_rel->oids))
+				continue;
+
+			/* combine the column lists */
+			relation->va_cols = list_concat_unique(relation->va_cols, nth_rel->va_cols);
+			duplicates = lappend_int(duplicates, j);
+		}
+	}
+
+	/* nothing else needed if we did not find any duplicates */
+	if (duplicates == NIL)
+		return relations;
+
+	for (i = 0; i < list_length(relations); ++i)
+	{
+		/* if we are a duplicate entry, skip */
+		if (list_member_int(duplicates, i))
+			continue;
+
+		/* add non-duplicate entry to the final list */
+		temp_relations = lappend(temp_relations, list_nth_node(RelationAndColumns, relations, i));
+	}
+
+	return temp_relations;
 }
 
 /*
@@ -1309,6 +1480,16 @@ vacuum_rel(Oid relid, RangeVar *relation, int options, VacuumParams *params)
 
 	if (!onerel)
 	{
+		/*
+		 * If one of the relations specified by the user has disappeared
+		 * since we last looked it up, let them know so that they do not
+		 * think it was actually vacuumed.
+		 */
+		if (!IsAutoVacuumWorkerProcess() && relation)
+			ereport(WARNING,
+				  (errmsg("skipping \"%s\" --- relation no longer exists",
+						  relation->relname)));
+
 		PopActiveSnapshot();
 		CommitTransactionCommand();
 		return false;
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 45a04b0..25eaa9d 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3762,8 +3762,19 @@ _copyVacuumStmt(const VacuumStmt *from)
 	VacuumStmt *newnode = makeNode(VacuumStmt);
 
 	COPY_SCALAR_FIELD(options);
+	COPY_NODE_FIELD(rels);
+
+	return newnode;
+}
+
+static RelationAndColumns *
+_copyRelationAndColumns(const RelationAndColumns *from)
+{
+	RelationAndColumns *newnode = makeNode(RelationAndColumns);
+
 	COPY_NODE_FIELD(relation);
 	COPY_NODE_FIELD(va_cols);
+	COPY_NODE_FIELD(oids);
 
 	return newnode;
 }
@@ -5210,6 +5221,9 @@ copyObjectImpl(const void *from)
 		case T_VacuumStmt:
 			retval = _copyVacuumStmt(from);
 			break;
+		case T_RelationAndColumns:
+			retval = _copyRelationAndColumns(from);
+			break;
 		case T_ExplainStmt:
 			retval = _copyExplainStmt(from);
 			break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 8d92c03..3a120f3 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -1663,8 +1663,17 @@ static bool
 _equalVacuumStmt(const VacuumStmt *a, const VacuumStmt *b)
 {
 	COMPARE_SCALAR_FIELD(options);
+	COMPARE_NODE_FIELD(rels);
+
+	return true;
+}
+
+static bool
+_equalRelationAndColumns(const RelationAndColumns *a, const RelationAndColumns *b)
+{
 	COMPARE_NODE_FIELD(relation);
 	COMPARE_NODE_FIELD(va_cols);
+	COMPARE_NODE_FIELD(oids);
 
 	return true;
 }
@@ -3360,6 +3369,9 @@ equal(const void *a, const void *b)
 		case T_VacuumStmt:
 			retval = _equalVacuumStmt(a, b);
 			break;
+		case T_RelationAndColumns:
+			retval = _equalRelationAndColumns(a, b);
+			break;
 		case T_ExplainStmt:
 			retval = _equalExplainStmt(a, b);
 			break;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7d0de99..f32218e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -366,6 +366,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <ival>	import_qualification_type
 %type <importqual> import_qualification
 
+%type <node>	relation_and_columns
+
 %type <list>	stmtblock stmtmulti
 				OptTableElementList TableElementList OptInherit definition
 				OptTypedTableElementList TypedTableElementList
@@ -395,7 +397,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				relation_expr_list dostmt_opt_list
 				transform_element_list transform_type_list
 				TriggerTransitions TriggerReferencing
-				publication_name_list
+				publication_name_list relation_and_columns_list
 
 %type <list>	group_by_list
 %type <node>	group_by_item empty_grouping_set rollup_clause cube_clause
@@ -10128,11 +10130,10 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 						n->options |= VACOPT_FREEZE;
 					if ($4)
 						n->options |= VACOPT_VERBOSE;
-					n->relation = NULL;
-					n->va_cols = NIL;
-					$$ = (Node *)n;
+					n->rels = NIL;
+					$$ = (Node *) n;
 				}
-			| VACUUM opt_full opt_freeze opt_verbose qualified_name
+			| VACUUM opt_full opt_freeze opt_verbose relation_and_columns_list
 				{
 					VacuumStmt *n = makeNode(VacuumStmt);
 					n->options = VACOPT_VACUUM;
@@ -10142,9 +10143,8 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 						n->options |= VACOPT_FREEZE;
 					if ($4)
 						n->options |= VACOPT_VERBOSE;
-					n->relation = $5;
-					n->va_cols = NIL;
-					$$ = (Node *)n;
+					n->rels = $5;
+					$$ = (Node *) n;
 				}
 			| VACUUM opt_full opt_freeze opt_verbose AnalyzeStmt
 				{
@@ -10156,24 +10156,20 @@ VacuumStmt: VACUUM opt_full opt_freeze opt_verbose
 						n->options |= VACOPT_FREEZE;
 					if ($4)
 						n->options |= VACOPT_VERBOSE;
-					$$ = (Node *)n;
+					$$ = (Node *) n;
 				}
 			| VACUUM '(' vacuum_option_list ')'
 				{
 					VacuumStmt *n = makeNode(VacuumStmt);
 					n->options = VACOPT_VACUUM | $3;
-					n->relation = NULL;
-					n->va_cols = NIL;
+					n->rels = NIL;
 					$$ = (Node *) n;
 				}
-			| VACUUM '(' vacuum_option_list ')' qualified_name opt_name_list
+			| VACUUM '(' vacuum_option_list ')' relation_and_columns_list
 				{
 					VacuumStmt *n = makeNode(VacuumStmt);
 					n->options = VACOPT_VACUUM | $3;
-					n->relation = $5;
-					n->va_cols = $6;
-					if (n->va_cols != NIL)	/* implies analyze */
-						n->options |= VACOPT_ANALYZE;
+					n->rels = $5;
 					$$ = (Node *) n;
 				}
 		;
@@ -10207,19 +10203,17 @@ AnalyzeStmt:
 					n->options = VACOPT_ANALYZE;
 					if ($2)
 						n->options |= VACOPT_VERBOSE;
-					n->relation = NULL;
-					n->va_cols = NIL;
-					$$ = (Node *)n;
+					n->rels = NIL;
+					$$ = (Node *) n;
 				}
-			| analyze_keyword opt_verbose qualified_name opt_name_list
+			| analyze_keyword opt_verbose relation_and_columns_list
 				{
 					VacuumStmt *n = makeNode(VacuumStmt);
 					n->options = VACOPT_ANALYZE;
 					if ($2)
 						n->options |= VACOPT_VERBOSE;
-					n->relation = $3;
-					n->va_cols = $4;
-					$$ = (Node *)n;
+					n->rels = $3;
+					$$ = (Node *) n;
 				}
 		;
 
@@ -10246,6 +10240,21 @@ opt_name_list:
 			| /*EMPTY*/								{ $$ = NIL; }
 		;
 
+relation_and_columns:
+			qualified_name opt_name_list
+				{
+					RelationAndColumns *n = makeNode(RelationAndColumns);
+					n->relation = $1;
+					n->va_cols = $2;
+					$$ = (Node *) n;
+				}
+		;
+
+relation_and_columns_list:
+			relation_and_columns					{ $$ = list_make1($1); }
+			| relation_and_columns_list ',' relation_and_columns	{ $$ = lappend($1, $3); }
+		;
+
 
 /*****************************************************************************
  *
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 00b1e82..ea5460b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3115,6 +3115,7 @@ static void
 autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
 {
 	RangeVar	rangevar;
+	RelationAndColumns *rel;
 
 	/* Set up command parameters --- use local variables instead of palloc */
 	MemSet(&rangevar, 0, sizeof(rangevar));
@@ -3126,8 +3127,12 @@ autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy)
 	/* Let pgstat know what we're doing */
 	autovac_report_activity(tab);
 
-	vacuum(tab->at_vacoptions, &rangevar, tab->at_relid, &tab->at_params, NIL,
-		   bstrategy, true);
+	rel = makeNode(RelationAndColumns);
+	rel->relation = &rangevar;
+	if (OidIsValid(tab->at_relid))
+		rel->oids = list_make1_oid(tab->at_relid);
+
+	vacuum(tab->at_vacoptions, list_make1(rel), &tab->at_params, bstrategy, true);
 }
 
 /*
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index a903511..7a7b793 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -157,8 +157,7 @@ extern int	vacuum_multixact_freeze_table_age;
 
 /* in commands/vacuum.c */
 extern void ExecVacuum(VacuumStmt *vacstmt, bool isTopLevel);
-extern void vacuum(int options, RangeVar *relation, Oid relid,
-	   VacuumParams *params, List *va_cols,
+extern void vacuum(int options, List *relations, VacuumParams *params,
 	   BufferAccessStrategy bstrategy, bool isTopLevel);
 extern void vac_open_indexes(Relation relation, LOCKMODE lockmode,
 				 int *nindexes, Relation **Irel);
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 27bd4f3..620a8db 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -468,6 +468,7 @@ typedef enum NodeTag
 	T_PartitionBoundSpec,
 	T_PartitionRangeDatum,
 	T_PartitionCmd,
+	T_RelationAndColumns,
 
 	/*
 	 * TAGS FOR REPLICATION GRAMMAR PARSE NODES (replnodes.h)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 5f2a4a7..ad2b4ee 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3090,12 +3090,23 @@ typedef enum VacuumOption
 	VACOPT_DISABLE_PAGE_SKIPPING = 1 << 7	/* don't skip any pages */
 } VacuumOption;
 
+/*
+ * This is used to keep track of a relation and an optional list of
+ * column names, as may be specified in VACUUM and ANALYZE.
+ */
+typedef struct RelationAndColumns
+{
+	NodeTag		 type;
+	RangeVar	*relation;	/* single table to process */
+	List		*va_cols;	/* list of column names, or NIL for all */
+	List		*oids;		/* corresponding OIDs (filled in by [auto]vacuum.c) */
+} RelationAndColumns;
+
 typedef struct VacuumStmt
 {
-	NodeTag		type;
-	int			options;		/* OR of VacuumOption flags */
-	RangeVar   *relation;		/* single table to process, or NULL */
-	List	   *va_cols;		/* list of column names, or NIL for all */
+	NodeTag		 type;
+	int		 options;	/* OR of VacuumOption flags */
+	List		*rels;		/* list of tables/columns to process, or NIL for all */
 } VacuumStmt;
 
 /* ----------------------
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index 6f68663..8a95fd0 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -80,8 +80,6 @@ CONTEXT:  SQL function "do_analyze" statement 1
 SQL function "wrap_do_analyze" statement 1
 VACUUM FULL vactst;
 VACUUM (DISABLE_PAGE_SKIPPING) vaccluster;
-DROP TABLE vaccluster;
-DROP TABLE vactst;
 -- partitioned table
 CREATE TABLE vacparted (a int, b char) PARTITION BY LIST (a);
 CREATE TABLE vacparted1 PARTITION OF vacparted FOR VALUES IN (1);
@@ -90,4 +88,34 @@ UPDATE vacparted SET b = 'b';
 VACUUM (ANALYZE) vacparted;
 VACUUM (FULL) vacparted;
 VACUUM (FREEZE) vacparted;
+-- multiple tables specified
+VACUUM FREEZE vaccluster, vactst;
+VACUUM FREEZE vacparted, does_not_exist;
+ERROR:  relation "does_not_exist" does not exist
+VACUUM (FREEZE) vacparted, vaccluster, vactst;
+VACUUM (FREEZE) vaccluster, does_not_exist;
+ERROR:  relation "does_not_exist" does not exist
+VACUUM (FREEZE, ANALYZE) vactst (i);
+VACUUM (FREEZE, ANALYZE) vactst, vacparted (a);
+VACUUM (FREEZE, ANALYZE) vacparted, vactst (i), vacparted;
+VACUUM (FREEZE, ANALYZE) vactst (i), vacparted (a, b), vaccluster (i);
+VACUUM (VERBOSE, ANALYZE) vactst (i), vacparted (does_not_exist);
+ERROR:  column "does_not_exist" of relation "vacparted" does not exist
+VACUUM FREEZE vactst, vactst, vactst;
+VACUUM (FREEZE, ANALYZE) vacparted (a), vacparted (b), vacparted;
+VACUUM vactst (i);
+ERROR:  ANALYZE option must be specified when a column list is provided
+VACUUM (VERBOSE) vactst, vacparted, vaccluster (i);
+ERROR:  ANALYZE option must be specified when a column list is provided
+VACUUM (VERBOSE) vactst (i), vacparted (a, b), vaccluster (i);
+ERROR:  ANALYZE option must be specified when a column list is provided
+VACUUM;
+ANALYZE vactst, vacparted;
+ANALYZE vacparted (b), vactst;
+ANALYZE VERBOSE vactst (i), vacparted (does_not_exist);
+ERROR:  column "does_not_exist" of relation "vacparted" does not exist
+ANALYZE vactst, vactst, vactst;
+ANALYZE vacparted (a), vacparted (b), vacparted;
+DROP TABLE vaccluster;
+DROP TABLE vactst;
 DROP TABLE vacparted;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 7c5fb04..558f60a 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -62,9 +62,6 @@ VACUUM FULL vactst;
 
 VACUUM (DISABLE_PAGE_SKIPPING) vaccluster;
 
-DROP TABLE vaccluster;
-DROP TABLE vactst;
-
 -- partitioned table
 CREATE TABLE vacparted (a int, b char) PARTITION BY LIST (a);
 CREATE TABLE vacparted1 PARTITION OF vacparted FOR VALUES IN (1);
@@ -73,4 +70,29 @@ UPDATE vacparted SET b = 'b';
 VACUUM (ANALYZE) vacparted;
 VACUUM (FULL) vacparted;
 VACUUM (FREEZE) vacparted;
+
+-- multiple tables specified
+VACUUM FREEZE vaccluster, vactst;
+VACUUM FREEZE vacparted, does_not_exist;
+VACUUM (FREEZE) vacparted, vaccluster, vactst;
+VACUUM (FREEZE) vaccluster, does_not_exist;
+VACUUM (FREEZE, ANALYZE) vactst (i);
+VACUUM (FREEZE, ANALYZE) vactst, vacparted (a);
+VACUUM (FREEZE, ANALYZE) vacparted, vactst (i), vacparted;
+VACUUM (FREEZE, ANALYZE) vactst (i), vacparted (a, b), vaccluster (i);
+VACUUM (VERBOSE, ANALYZE) vactst (i), vacparted (does_not_exist);
+VACUUM FREEZE vactst, vactst, vactst;
+VACUUM (FREEZE, ANALYZE) vacparted (a), vacparted (b), vacparted;
+VACUUM vactst (i);
+VACUUM (VERBOSE) vactst, vacparted, vaccluster (i);
+VACUUM (VERBOSE) vactst (i), vacparted (a, b), vaccluster (i);
+VACUUM;
+ANALYZE vactst, vacparted;
+ANALYZE vacparted (b), vactst;
+ANALYZE VERBOSE vactst (i), vacparted (does_not_exist);
+ANALYZE vactst, vactst, vactst;
+ANALYZE vacparted (a), vacparted (b), vacparted;
+
+DROP TABLE vaccluster;
+DROP TABLE vactst;
 DROP TABLE vacparted;
