diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index c582021d29..3ca300b859 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -366,7 +366,13 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
    </para>
 
    <para>
-    <command>VACUUM</command> cannot be executed inside a transaction block.
+    <command>VACUUM</command> cannot be executed inside a transaction block
+    for temporary tables, unless <literal>FULL</literal> is specified and
+    the command specifies only a single table.
+    For persistent tables, <command>VACUUM</command> of single tables is allowed
+    inside a transaction block, but is interpreted as a request for an autovacuum
+    worker to perform the vacuum task instead. The caller does not wait for the task
+    to complete; should the task execution fail, there is no response to the caller.
    </para>
 
    <para>
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 7e386250ae..dcd309a5d0 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -208,7 +208,8 @@ brininsert(Relation idxRel, Datum *values, bool *nulls,
 
 				recorded = AutoVacuumRequestWork(AVW_BRINSummarizeRange,
 												 RelationGetRelid(idxRel),
-												 lastPageRange);
+												 lastPageRange,
+												 0);
 				if (!recorded)
 					ereport(LOG,
 							(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 3c8ea21475..380291f241 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -269,6 +269,82 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	/* user-invoked vacuum uses VACOPT_VERBOSE instead of log_min_duration */
 	params.log_min_duration = -1;
 
+	/*
+	 * Special case for handling normal VACUUMs in a transaction block.
+	 *
+	 * If we're in a transaction block or subtransaction then we will fail
+	 * later during vacuum(), so instead, interpret VACUUM as a request to
+	 * initiate an avw vacuum task, if this is a single persistent table.
+	 */
+	if ((params.options & VACOPT_VACUUM) != 0 &&
+		(params.options & VACOPT_SKIP_LOCKED) == 0 &&
+		(params.options & VACOPT_FULL) == 0 &&
+		vacstmt->rels != NIL &&
+		list_length(vacstmt->rels) == 1 &&
+		(IsTransactionBlock() ||
+		 IsSubTransaction()))
+	{
+		foreach(lc, vacstmt->rels)
+		{
+			VacuumRelation *vrel = lfirst_node(VacuumRelation, lc);
+
+			/*
+			 * We need a lock to allow us to check permissions and to see
+			 * if the relation is persistent. We can't easily handle SKIP LOCKED here.
+			 * We release the lock almost immediately to avoid lock upgrade hazard
+			 * and to ensure we don't interfere with AV.
+			 */
+			Relation rel = relation_openrv(vrel->relation, AccessShareLock);
+
+			if (!RelationUsesLocalBuffers(rel))
+			{
+				bool requested;
+
+				/*
+				 * Check permissions, using identical check to vacuum_rel().
+				 *
+				 * Check if relation needs to be skipped based on ownership.  This check
+				 * happens also when building the relation list to vacuum for a manual
+				 * operation, and needs to be done additionally here as VACUUM could
+				 * happen across multiple transactions where relation ownership could have
+				 * changed in-between.  Make sure to only generate logs for VACUUM in this
+				 * case.
+				 */
+				if (!vacuum_is_relation_owner(RelationGetRelid(rel),
+											  rel->rd_rel,
+											  params.options & VACOPT_VACUUM))
+				{
+					relation_close(rel, AccessShareLock);
+					return;
+				}
+
+				/*
+				 * Request an autovacuum worker does the work for us,
+				 * and skip the actual execution in the current backend.
+				 * Note that we do not wait for the AV worker to complete
+				 * the task, nor do we check whether it succeeded or not.
+				 */
+				requested = AutoVacuumRequestWork(AVW_VacuumImmediate,
+													RelationGetRelid(rel),
+													InvalidBlockNumber,
+													params.options);
+				if (requested)
+					ereport(NOTICE,
+							(errmsg("autovacuum of \"%s\" was requested, using the options specified",
+									RelationGetRelationName(rel))));
+				else
+					ereport(LOG,
+							(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+							 errmsg("request for immediate autovacuum of \"%s\" was not recorded",
+									RelationGetRelationName(rel))));
+				relation_close(rel, AccessShareLock);
+				return;
+			}
+			else
+				relation_close(rel, AccessShareLock);
+		}
+	}
+
 	/* Now go through the common routine */
 	vacuum(vacstmt->rels, &params, NULL, isTopLevel);
 }
@@ -297,6 +373,7 @@ vacuum(List *relations, VacuumParams *params,
 	   BufferAccessStrategy bstrategy, bool isTopLevel)
 {
 	static bool in_vacuum = false;
+	bool single_table_ok = false;
 
 	const char *stmttype;
 	volatile bool in_outer_xact,
@@ -306,15 +383,22 @@ vacuum(List *relations, VacuumParams *params,
 
 	stmttype = (params->options & VACOPT_VACUUM) ? "VACUUM" : "ANALYZE";
 
+	if ((params->options & VACOPT_FULL) != 0 &&
+		relations != NIL &&
+		list_length(relations) == 1)
+		single_table_ok = true;
+
 	/*
-	 * We cannot run VACUUM inside a user transaction block; if we were inside
-	 * a transaction, then our commit- and start-transaction-command calls
+	 * Single-table VACUUM can run inside a user transaction block, but
+	 * we cannot run multiple VACUUMs inside a user transaction block; if we were
+	 * inside a transaction, then our commit- and start-transaction-command calls
 	 * would not have the intended effect!	There are numerous other subtle
-	 * dependencies on this, too.
+	 * dependencies on this, too, so when running in a transaction block, vacuum
+	 * will skip some of its normal actions, see later for details.
 	 *
 	 * ANALYZE (without VACUUM) can run either way.
 	 */
-	if (params->options & VACOPT_VACUUM)
+	if (params->options & VACOPT_VACUUM && !single_table_ok)
 	{
 		PreventInTransactionBlock(isTopLevel, stmttype);
 		in_outer_xact = false;
@@ -401,9 +485,8 @@ vacuum(List *relations, VacuumParams *params,
 	 * Decide whether we need to start/commit our own transactions.
 	 *
 	 * For VACUUM (with or without ANALYZE): always do so, so that we can
-	 * release locks as soon as possible.  (We could possibly use the outer
-	 * transaction for a one-table VACUUM, but handling TOAST tables would be
-	 * problematic.)
+	 * release locks as soon as possible, except for a single table VACUUM
+	 * when it is executed inside a transaction block.
 	 *
 	 * For ANALYZE (no VACUUM): if inside a transaction block, we cannot
 	 * start/commit our own transactions.  Also, there's no need to do so if
@@ -412,7 +495,7 @@ vacuum(List *relations, VacuumParams *params,
 	 * transactions so we can release locks sooner.
 	 */
 	if (params->options & VACOPT_VACUUM)
-		use_own_xacts = true;
+		use_own_xacts = !in_outer_xact;
 	else
 	{
 		Assert(params->options & VACOPT_ANALYZE);
@@ -427,6 +510,7 @@ vacuum(List *relations, VacuumParams *params,
 	}
 
 	/*
+	 * Tell vacuum_rel whether it will need to manage its own transaction. If so,
 	 * vacuum_rel expects to be entered with no transaction active; it will
 	 * start and commit its own transaction.  But we are called by an SQL
 	 * command, and so we are executing inside a transaction already. We
@@ -437,6 +521,9 @@ vacuum(List *relations, VacuumParams *params,
 	if (use_own_xacts)
 	{
 		Assert(!in_outer_xact);
+		Assert(!IsInTransactionBlock(isTopLevel));
+
+		params->use_own_xact = true;
 
 		/* ActiveSnapshot is not set by autovacuum */
 		if (ActiveSnapshotSet())
@@ -445,6 +532,8 @@ vacuum(List *relations, VacuumParams *params,
 		/* matches the StartTransaction in PostgresMain() */
 		CommitTransactionCommand();
 	}
+	else
+		params->use_own_xact = false;
 
 	/* Turn vacuum cost accounting on or off, and set/clear in_vacuum */
 	PG_TRY();
@@ -1837,9 +1926,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 	Assert(params != NULL);
 
 	/* Begin a transaction for vacuuming this relation */
-	StartTransactionCommand();
+	if (params->use_own_xact)
+		StartTransactionCommand();
 
-	if (!(params->options & VACOPT_FULL))
+	if (!(params->options & VACOPT_FULL) && (params->use_own_xact))
 	{
 		/*
 		 * In lazy vacuum, we can set the PROC_IN_VACUUM flag, which lets
@@ -1852,6 +1942,11 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 		 * contents of other tables is arguably broken, but we won't break it
 		 * here by violating transaction semantics.)
 		 *
+		 * Don't set PROC_IN_VACUUM if running in a transaction block, since it
+		 * would be very bad for other users to ignore our xact in that case.
+		 * Note that setting the flag is an optional performance tweak, not
+		 * required for correct operation of VACUUM.
+		 *
 		 * We also set the VACUUM_FOR_WRAPAROUND flag, which is passed down by
 		 * autovacuum; it's used to avoid canceling a vacuum that was invoked
 		 * in an emergency.
@@ -1876,7 +1971,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 	 * cutoff xids in local memory wrapping around, and to have updated xmin
 	 * horizons.
 	 */
-	PushActiveSnapshot(GetTransactionSnapshot());
+	if (params->use_own_xact)
+		PushActiveSnapshot(GetTransactionSnapshot());
 
 	/*
 	 * Check for user-requested abort.  Note we want this to be inside a
@@ -1899,8 +1995,11 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 	/* leave if relation could not be opened or locked */
 	if (!rel)
 	{
-		PopActiveSnapshot();
-		CommitTransactionCommand();
+		if (params->use_own_xact)
+		{
+			PopActiveSnapshot();
+			CommitTransactionCommand();
+		}
 		return false;
 	}
 
@@ -1917,8 +2016,11 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 								  params->options & VACOPT_VACUUM))
 	{
 		relation_close(rel, lmode);
-		PopActiveSnapshot();
-		CommitTransactionCommand();
+		if (params->use_own_xact)
+		{
+			PopActiveSnapshot();
+			CommitTransactionCommand();
+		}
 		return false;
 	}
 
@@ -1934,8 +2036,11 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 				(errmsg("skipping \"%s\" --- cannot vacuum non-tables or special system tables",
 						RelationGetRelationName(rel))));
 		relation_close(rel, lmode);
-		PopActiveSnapshot();
-		CommitTransactionCommand();
+		if (params->use_own_xact)
+		{
+			PopActiveSnapshot();
+			CommitTransactionCommand();
+		}
 		return false;
 	}
 
@@ -1949,8 +2054,11 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 	if (RELATION_IS_OTHER_TEMP(rel))
 	{
 		relation_close(rel, lmode);
-		PopActiveSnapshot();
-		CommitTransactionCommand();
+		if (params->use_own_xact)
+		{
+			PopActiveSnapshot();
+			CommitTransactionCommand();
+		}
 		return false;
 	}
 
@@ -1962,8 +2070,11 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 	if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
 	{
 		relation_close(rel, lmode);
-		PopActiveSnapshot();
-		CommitTransactionCommand();
+		if (params->use_own_xact)
+		{
+			PopActiveSnapshot();
+			CommitTransactionCommand();
+		}
 		/* It's OK to proceed with ANALYZE on this table */
 		return true;
 	}
@@ -1977,9 +2088,13 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 	 * NOTE: this cannot block, even if someone else is waiting for access,
 	 * because the lock manager knows that both lock requests are from the
 	 * same process.
+	 *
+	 * If we are in a transaction block, vacuum both main table and toast
+	 * table within the existing transaction, so no session lock required.
 	 */
 	lockrelid = rel->rd_lockInfo.lockRelId;
-	LockRelationIdForSession(&lockrelid, lmode);
+	if (params->use_own_xact)
+		LockRelationIdForSession(&lockrelid, lmode);
 
 	/*
 	 * Set index_cleanup option based on index_cleanup reloption if it wasn't
@@ -2075,8 +2190,11 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 	/*
 	 * Complete the transaction and free all temporary memory used.
 	 */
-	PopActiveSnapshot();
-	CommitTransactionCommand();
+	if (params->use_own_xact)
+	{
+		PopActiveSnapshot();
+		CommitTransactionCommand();
+	}
 
 	/*
 	 * If the relation has a secondary toast rel, vacuum that too while we
@@ -2091,7 +2209,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params)
 	/*
 	 * Now release the session-level lock on the main table.
 	 */
-	UnlockRelationIdForSession(&lockrelid, lmode);
+	if (params->use_own_xact)
+		UnlockRelationIdForSession(&lockrelid, lmode);
 
 	/* Report that we really did it. */
 	return true;
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 601834d4b4..c6a15097b5 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -258,6 +258,7 @@ typedef struct AutoVacuumWorkItem
 	Oid			avw_database;
 	Oid			avw_relation;
 	BlockNumber avw_blockNumber;
+	bits32      avw_vac_options;
 } AutoVacuumWorkItem;
 
 #define NUM_WORKITEMS	256
@@ -2663,6 +2664,41 @@ perform_work_item(AutoVacuumWorkItem *workitem)
 									ObjectIdGetDatum(workitem->avw_relation),
 									Int64GetDatum((int64) workitem->avw_blockNumber));
 				break;
+			case AVW_VacuumImmediate:
+				{
+					BufferAccessStrategy	bstrategy;
+					autovac_table			tab;
+
+					tab.at_relid = workitem->avw_relation;
+
+					/* Set options */
+					tab.at_params.options = workitem->avw_vac_options;
+					tab.at_params.index_cleanup = VACOPTVALUE_ENABLED;
+					tab.at_params.truncate = VACOPTVALUE_DISABLED;
+
+					/* Deliberately avoid using autovacuum parameters, since this is immediate */
+					tab.at_vacuum_cost_delay = VacuumCostDelay;
+					tab.at_vacuum_cost_limit = VacuumCostLimit;
+					tab.at_dobalance = false;
+					tab.at_sharedrel = false;
+
+					/* Set names in case of error */
+					tab.at_relname = pstrdup(get_rel_name(tab.at_relid));
+					tab.at_nspname = pstrdup(get_namespace_name(get_rel_namespace(tab.at_relid)));
+					tab.at_datname = pstrdup(get_database_name(MyDatabaseId));
+
+					/* XXX what other options should we set? */
+
+					/*
+					 * Create a buffer access strategy object for VACUUM to use.  We want to
+					 * use the same one across all the vacuum operations we perform, since the
+					 * point is for VACUUM not to blow out the shared cache.
+					 */
+					bstrategy = GetAccessStrategy(BAS_VACUUM);
+
+					autovacuum_do_vac_analyze(&tab, bstrategy);
+				}
+				break;
 			default:
 				elog(WARNING, "unrecognized work item found: type %d",
 					 workitem->avw_type);
@@ -3210,6 +3246,11 @@ autovac_report_workitem(AutoVacuumWorkItem *workitem,
 			snprintf(activity, MAX_AUTOVAC_ACTIV_LEN,
 					 "autovacuum: BRIN summarize");
 			break;
+
+		case AVW_VacuumImmediate:
+			snprintf(activity, MAX_AUTOVAC_ACTIV_LEN,
+					 "autovacuum: user vacuum");
+			break;
 	}
 
 	/*
@@ -3250,7 +3291,7 @@ AutoVacuumingActive(void)
  */
 bool
 AutoVacuumRequestWork(AutoVacuumWorkItemType type, Oid relationId,
-					  BlockNumber blkno)
+					  BlockNumber blkno, bits32 options)
 {
 	int			i;
 	bool		result = false;
@@ -3273,6 +3314,7 @@ AutoVacuumRequestWork(AutoVacuumWorkItemType type, Oid relationId,
 		workitem->avw_database = MyDatabaseId;
 		workitem->avw_relation = relationId;
 		workitem->avw_blockNumber = blkno;
+		workitem->avw_vac_options = options;
 		result = true;
 
 		/* done */
@@ -3281,6 +3323,8 @@ AutoVacuumRequestWork(AutoVacuumWorkItemType type, Oid relationId,
 
 	LWLockRelease(AutovacuumLock);
 
+	/* XXX Should this wake up the AVL? */
+
 	return result;
 }
 
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 5d816ba7f4..35310c588b 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -227,6 +227,8 @@ typedef struct VacuumParams
 	VacOptValue index_cleanup;	/* Do index vacuum and cleanup */
 	VacOptValue truncate;		/* Truncate empty pages at the end */
 
+	bool		use_own_xact;	/* Use own xact in vacuum_rel? */
+
 	/*
 	 * The number of parallel vacuum workers.  0 by default which means choose
 	 * based on the number of indexes.  -1 indicates parallel vacuum is
diff --git a/src/include/postmaster/autovacuum.h b/src/include/postmaster/autovacuum.h
index 9d40fd6d54..f8b91f111b 100644
--- a/src/include/postmaster/autovacuum.h
+++ b/src/include/postmaster/autovacuum.h
@@ -22,7 +22,8 @@
  */
 typedef enum
 {
-	AVW_BRINSummarizeRange
+	AVW_BRINSummarizeRange,
+	AVW_VacuumImmediate
 } AutoVacuumWorkItemType;
 
 
@@ -74,7 +75,7 @@ extern void AutovacuumLauncherIAm(void);
 #endif
 
 extern bool AutoVacuumRequestWork(AutoVacuumWorkItemType type,
-								  Oid relationId, BlockNumber blkno);
+								  Oid relationId, BlockNumber blkno, bits32 options);
 
 /* shared memory stuff */
 extern Size AutoVacuumShmemSize(void);
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index c63a157e5f..a3209477a1 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -282,6 +282,19 @@ ALTER TABLE vactst ALTER COLUMN t SET STORAGE EXTERNAL;
 VACUUM (PROCESS_TOAST FALSE) vactst;
 VACUUM (PROCESS_TOAST FALSE, FULL) vactst;
 ERROR:  PROCESS_TOAST required with VACUUM FULL
+-- Single table inside transaction block
+CREATE TEMPORARY TABLE vactst_temp (LIKE vactst);
+BEGIN;
+VACUUM FULL vactst_temp;
+COMMIT;
+BEGIN;
+VACUUM vactst_temp;
+ERROR:  VACUUM cannot run inside a transaction block
+COMMIT;
+BEGIN;
+VACUUM (ANALYZE) vactst;
+NOTICE:  autovacuum of "vactst" was requested, using the options specified
+COMMIT;
 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 9faa8a34a6..6e489a2e40 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -237,6 +237,18 @@ ALTER TABLE vactst ALTER COLUMN t SET STORAGE EXTERNAL;
 VACUUM (PROCESS_TOAST FALSE) vactst;
 VACUUM (PROCESS_TOAST FALSE, FULL) vactst;
 
+-- Single table inside transaction block
+CREATE TEMPORARY TABLE vactst_temp (LIKE vactst);
+BEGIN;
+VACUUM FULL vactst_temp;
+COMMIT;
+BEGIN;
+VACUUM vactst_temp;
+COMMIT;
+BEGIN;
+VACUUM (ANALYZE) vactst;
+COMMIT;
+
 DROP TABLE vaccluster;
 DROP TABLE vactst;
 DROP TABLE vacparted;
