diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index c582021d29..d2cb40a80e 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,
+    unless a single table is specified and <literal>FULL</literal> is not
+    specified.  When executing inside a transaction block the vacuum scan will
+    hold back the xmin horizon and does not update the database datfrozenxid,
+    as a result this usage is not useful for database maintenance, but is provided
+    to allow vacuuming in special circumstances, such as temporary or private
+    work tables.
    </para>
 
    <para>
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 7ccde07de9..ad7c4480e6 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -85,6 +85,7 @@ pg_atomic_uint32 *VacuumActiveNWorkers = NULL;
 int			VacuumCostBalanceLocal = 0;
 
 /* non-export function prototypes */
+static void vacuum_xact_block_callback(void *arg);
 static List *expand_vacuum_rel(VacuumRelation *vrel, int options);
 static List *get_all_vacuum_rels(int options);
 static void vac_truncate_clog(TransactionId frozenXID,
@@ -273,6 +274,20 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 	vacuum(vacstmt->rels, &params, NULL, isTopLevel);
 }
 
+/*
+ * Error context callback while checking PreventInTransactionBlock().
+ */
+static void
+vacuum_xact_block_callback(void *arg)
+{
+	bool *multi = (bool *) arg;
+
+	if (*multi)
+		errcontext("while attempting VACUUM of multiple relations");
+	else
+		errcontext("while attempting VACUUM FULL");
+}
+
 /*
  * Internal entry point for VACUUM and ANALYZE commands.
  *
@@ -297,6 +312,7 @@ vacuum(List *relations, VacuumParams *params,
 	   BufferAccessStrategy bstrategy, bool isTopLevel)
 {
 	static bool in_vacuum = false;
+	bool single_table = false;
 
 	const char *stmttype;
 	volatile bool in_outer_xact,
@@ -306,17 +322,39 @@ vacuum(List *relations, VacuumParams *params,
 
 	stmttype = (params->options & VACOPT_VACUUM) ? "VACUUM" : "ANALYZE";
 
+	if (relations != NIL && list_length(relations) == 1)
+		single_table = 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 ||
+		(params->options & VACOPT_FULL) != 0))
 	{
+		ErrorContextCallback errcallback;
+		bool	multi = !single_table;
+
+		/*
+		 * Setup errcontext to explain reason for disallowing vacuum, if any
+		 */
+		errcallback.callback = vacuum_xact_block_callback;
+		errcallback.previous = error_context_stack;
+		error_context_stack = &errcallback;
+		errcallback.arg = &multi;
+
 		PreventInTransactionBlock(isTopLevel, stmttype);
+
+		/* Pop error context stack back to how it was */
+		error_context_stack = errcallback.previous;
+
 		in_outer_xact = false;
 	}
 	else
@@ -401,9 +439,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 +449,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 +464,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 +475,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 +486,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();
@@ -528,11 +571,13 @@ vacuum(List *relations, VacuumParams *params,
 		StartTransactionCommand();
 	}
 
-	if ((params->options & VACOPT_VACUUM) && !IsAutoVacuumWorkerProcess())
+	if ((params->options & VACOPT_VACUUM) && !IsAutoVacuumWorkerProcess() && use_own_xacts)
 	{
 		/*
 		 * Update pg_database.datfrozenxid, and truncate pg_xact if possible.
-		 * (autovacuum.c does this for itself.)
+		 * (autovacuum.c does this for itself.) unless we are in a transaction
+		 * block, since this might take a while and we're not sure whether it
+		 * is safe to allow this.
 		 */
 		vac_update_datfrozenxid();
 	}
@@ -1837,9 +1882,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 +1898,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 +1927,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 +1951,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 +1972,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 +1992,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 +2010,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 +2026,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 +2044,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 +2146,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 +2165,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/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/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 2b2cff7d91..88d5043cc6 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -1033,6 +1033,7 @@ SELECT 1\; VACUUM;
 (1 row)
 
 ERROR:  VACUUM cannot run inside a transaction block
+CONTEXT:  while attempting VACUUM of multiple relations
 SELECT 1\; COMMIT\; VACUUM;
 WARNING:  there is no transaction in progress
  ?column? 
@@ -1041,6 +1042,7 @@ WARNING:  there is no transaction in progress
 (1 row)
 
 ERROR:  VACUUM cannot run inside a transaction block
+CONTEXT:  while attempting VACUUM of multiple relations
 -- we disallow savepoint-related commands in implicit-transaction state
 SELECT 1\; SAVEPOINT sp;
  ?column? 
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index c63a157e5f..f252620d93 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
+BEGIN;
+VACUUM (PROCESS_TOAST FALSE) vactst;
+COMMIT;
+BEGIN;
+VACUUM vactst;
+COMMIT;
+BEGIN;
+VACUUM (ANALYZE) vactst;
+COMMIT;
+BEGIN; VACUUM FULL vactst; COMMIT;
+ERROR:  VACUUM cannot run inside a transaction block
+CONTEXT:  while attempting VACUUM FULL
 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..b9f6c0182d 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
+BEGIN;
+VACUUM (PROCESS_TOAST FALSE) vactst;
+COMMIT;
+BEGIN;
+VACUUM vactst;
+COMMIT;
+BEGIN;
+VACUUM (ANALYZE) vactst;
+COMMIT;
+BEGIN; VACUUM FULL vactst; COMMIT;
+
 DROP TABLE vaccluster;
 DROP TABLE vactst;
 DROP TABLE vacparted;
