diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
index 7162fdb7aa343ccb4490715ea4f4065f8148b43b..ea6bfc2a3f53b082e47de3c55ea3309c3799b378 100644
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -326,9 +326,7 @@ SPI_execute("INSERT INTO foo SELECT * FROM bar", false, 5);
   </para>
 
   <para>
-   You can pass multiple commands in one string, but later commands cannot
-   depend on the creation of objects earlier in the string, because the
-   whole string will be parsed and planned before execution begins.
+   You can pass multiple commands in one string;
    <function>SPI_execute</function> returns the
    result for the command executed last.  The <parameter>count</parameter>
    limit applies to each command separately, but it is not applied to
@@ -392,7 +390,8 @@ typedef struct
     TupleDesc   tupdesc;        /* row descriptor */
     HeapTuple  *vals;           /* rows */
 } SPITupleTable;
-</programlisting><structfield>vals</> is an array of pointers to rows.  (The number
+</programlisting>
+   <structfield>vals</> is an array of pointers to rows.  (The number
    of valid entries is given by <varname>SPI_processed</varname>.)
    <structfield>tupdesc</> is a row descriptor which you can pass to
    SPI functions dealing with rows.  <structfield>tuptabcxt</>,
@@ -432,7 +431,8 @@ typedef struct
     <term><literal>long <parameter>count</parameter></literal></term>
     <listitem>
      <para>
-      maximum number of rows to process or return
+      maximum number of rows to process or return,
+      or <literal>0</> for no limit
      </para>
     </listitem>
    </varlistentry>
@@ -671,7 +671,8 @@ int SPI_exec(const char * <parameter>command</parameter>, long <parameter>count<
     <term><literal>long <parameter>count</parameter></literal></term>
     <listitem>
      <para>
-      maximum number of rows to process or return
+      maximum number of rows to process or return,
+      or <literal>0</> for no limit
      </para>
     </listitem>
    </varlistentry>
@@ -809,7 +810,8 @@ int SPI_execute_with_args(const char *<parameter>command</parameter>,
     <term><literal>long <parameter>count</parameter></literal></term>
     <listitem>
      <para>
-      maximum number of rows to process or return
+      maximum number of rows to process or return,
+      or <literal>0</> for no limit
      </para>
     </listitem>
    </varlistentry>
@@ -1452,7 +1454,8 @@ int SPI_execute_plan(SPIPlanPtr <parameter>plan</parameter>, Datum * <parameter>
     <term><literal>long <parameter>count</parameter></literal></term>
     <listitem>
      <para>
-      maximum number of rows to process or return
+      maximum number of rows to process or return,
+      or <literal>0</> for no limit
      </para>
     </listitem>
    </varlistentry>
@@ -1569,7 +1572,8 @@ int SPI_execute_plan_with_paramlist(SPIPlanPtr <parameter>plan</parameter>,
     <term><literal>long <parameter>count</parameter></literal></term>
     <listitem>
      <para>
-      maximum number of rows to process or return
+      maximum number of rows to process or return,
+      or <literal>0</> for no limit
      </para>
     </listitem>
    </varlistentry>
@@ -1669,7 +1673,8 @@ int SPI_execp(SPIPlanPtr <parameter>plan</parameter>, Datum * <parameter>values<
     <term><literal>long <parameter>count</parameter></literal></term>
     <listitem>
      <para>
-      maximum number of rows to process or return
+      maximum number of rows to process or return,
+      or <literal>0</> for no limit
      </para>
     </listitem>
    </varlistentry>
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 5a11c6f7392296adeb41a276fc698cb1ca246bf4..da40f8207c19f98c80730c7d48b1c4daf6a164da 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -48,8 +48,9 @@ static int	_SPI_curid = -1;
 static Portal SPI_cursor_open_internal(const char *name, SPIPlanPtr plan,
 						 ParamListInfo paramLI, bool read_only);
 
-static void _SPI_prepare_plan(const char *src, SPIPlanPtr plan,
-				  ParamListInfo boundParams);
+static void _SPI_prepare_plan(const char *src, SPIPlanPtr plan);
+
+static void _SPI_prepare_oneshot_plan(const char *src, SPIPlanPtr plan);
 
 static int _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 				  Snapshot snapshot, Snapshot crosscheck_snapshot,
@@ -354,7 +355,7 @@ SPI_execute(const char *src, bool read_only, long tcount)
 	plan.magic = _SPI_PLAN_MAGIC;
 	plan.cursor_options = 0;
 
-	_SPI_prepare_plan(src, &plan, NULL);
+	_SPI_prepare_oneshot_plan(src, &plan);
 
 	res = _SPI_execute_plan(&plan, NULL,
 							InvalidSnapshot, InvalidSnapshot,
@@ -505,7 +506,7 @@ SPI_execute_with_args(const char *src,
 	paramLI = _SPI_convert_params(nargs, argtypes,
 								  Values, Nulls);
 
-	_SPI_prepare_plan(src, &plan, paramLI);
+	_SPI_prepare_oneshot_plan(src, &plan);
 
 	res = _SPI_execute_plan(&plan, paramLI,
 							InvalidSnapshot, InvalidSnapshot,
@@ -546,7 +547,7 @@ SPI_prepare_cursor(const char *src, int nargs, Oid *argtypes,
 	plan.parserSetup = NULL;
 	plan.parserSetupArg = NULL;
 
-	_SPI_prepare_plan(src, &plan, NULL);
+	_SPI_prepare_plan(src, &plan);
 
 	/* copy plan to procedure context */
 	result = _SPI_make_plan_non_temp(&plan);
@@ -583,7 +584,7 @@ SPI_prepare_params(const char *src,
 	plan.parserSetup = parserSetup;
 	plan.parserSetupArg = parserSetupArg;
 
-	_SPI_prepare_plan(src, &plan, NULL);
+	_SPI_prepare_plan(src, &plan);
 
 	/* copy plan to procedure context */
 	result = _SPI_make_plan_non_temp(&plan);
@@ -598,7 +599,8 @@ SPI_keepplan(SPIPlanPtr plan)
 {
 	ListCell   *lc;
 
-	if (plan == NULL || plan->magic != _SPI_PLAN_MAGIC || plan->saved)
+	if (plan == NULL || plan->magic != _SPI_PLAN_MAGIC ||
+		plan->saved || plan->oneshot)
 		return SPI_ERROR_ARGUMENT;
 
 	/*
@@ -1082,7 +1084,7 @@ SPI_cursor_open_with_args(const char *name,
 	paramLI = _SPI_convert_params(nargs, argtypes,
 								  Values, Nulls);
 
-	_SPI_prepare_plan(src, &plan, paramLI);
+	_SPI_prepare_plan(src, &plan);
 
 	/* We needn't copy the plan; SPI_cursor_open_internal will do so */
 
@@ -1644,10 +1646,6 @@ spi_printtup(TupleTableSlot *slot, DestReceiver *self)
  *
  * At entry, plan->argtypes and plan->nargs (or alternatively plan->parserSetup
  * and plan->parserSetupArg) must be valid, as must plan->cursor_options.
- * If boundParams isn't NULL then it represents parameter values that are made
- * available to the planner (as either estimates or hard values depending on
- * their PARAM_FLAG_CONST marking).  The boundParams had better match the
- * param type information embedded in the plan!
  *
  * Results are stored into *plan (specifically, plan->plancache_list).
  * Note that the result data is all in CurrentMemoryContext or child contexts
@@ -1656,13 +1654,12 @@ spi_printtup(TupleTableSlot *slot, DestReceiver *self)
  * parsing is also left in CurrentMemoryContext.
  */
 static void
-_SPI_prepare_plan(const char *src, SPIPlanPtr plan, ParamListInfo boundParams)
+_SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 {
 	List	   *raw_parsetree_list;
 	List	   *plancache_list;
 	ListCell   *list_item;
 	ErrorContextCallback spierrcontext;
-	int			cursor_options = plan->cursor_options;
 
 	/*
 	 * Setup error traceback support for ereport()
@@ -1725,13 +1722,80 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan, ParamListInfo boundParams)
 						   plan->nargs,
 						   plan->parserSetup,
 						   plan->parserSetupArg,
-						   cursor_options,
+						   plan->cursor_options,
 						   false);		/* not fixed result */
 
 		plancache_list = lappend(plancache_list, plansource);
 	}
 
 	plan->plancache_list = plancache_list;
+	plan->oneshot = false;
+
+	/*
+	 * Pop the error context stack
+	 */
+	error_context_stack = spierrcontext.previous;
+}
+
+/*
+ * Parse, but don't analyze, a querystring.
+ *
+ * This is a stripped-down version of _SPI_prepare_plan that only does the
+ * initial raw parsing.  It creates "one shot" CachedPlanSources
+ * that still require parse analysis before execution is possible.
+ *
+ * The advantage of using the "one shot" form of CachedPlanSource is that
+ * we eliminate data copying and invalidation overhead.  Postponing parse
+ * analysis also prevents issues if some of the raw parsetrees are DDL
+ * commands that affect validity of later parsetrees.  Both of these
+ * attributes are good things for SPI_execute() and similar cases.
+ *
+ * Results are stored into *plan (specifically, plan->plancache_list).
+ * Note that the result data is all in CurrentMemoryContext or child contexts
+ * thereof; in practice this means it is in the SPI executor context, and
+ * what we are creating is a "temporary" SPIPlan.  Cruft generated during
+ * parsing is also left in CurrentMemoryContext.
+ */
+static void
+_SPI_prepare_oneshot_plan(const char *src, SPIPlanPtr plan)
+{
+	List	   *raw_parsetree_list;
+	List	   *plancache_list;
+	ListCell   *list_item;
+	ErrorContextCallback spierrcontext;
+
+	/*
+	 * Setup error traceback support for ereport()
+	 */
+	spierrcontext.callback = _SPI_error_callback;
+	spierrcontext.arg = (void *) src;
+	spierrcontext.previous = error_context_stack;
+	error_context_stack = &spierrcontext;
+
+	/*
+	 * Parse the request string into a list of raw parse trees.
+	 */
+	raw_parsetree_list = pg_parse_query(src);
+
+	/*
+	 * Construct plancache entries, but don't do parse analysis yet.
+	 */
+	plancache_list = NIL;
+
+	foreach(list_item, raw_parsetree_list)
+	{
+		Node	   *parsetree = (Node *) lfirst(list_item);
+		CachedPlanSource *plansource;
+
+		plansource = CreateOneShotCachedPlan(parsetree,
+											 src,
+											 CreateCommandTag(parsetree));
+
+		plancache_list = lappend(plancache_list, plansource);
+	}
+
+	plan->plancache_list = plancache_list;
+	plan->oneshot = true;
 
 	/*
 	 * Pop the error context stack
@@ -1769,7 +1833,7 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 	 * Setup error traceback support for ereport()
 	 */
 	spierrcontext.callback = _SPI_error_callback;
-	spierrcontext.arg = NULL;
+	spierrcontext.arg = NULL;	/* we'll fill this below */
 	spierrcontext.previous = error_context_stack;
 	error_context_stack = &spierrcontext;
 
@@ -1815,6 +1879,47 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
 
 		spierrcontext.arg = (void *) plansource->query_string;
 
+		/*
+		 * If this is a one-shot plan, we still need to do parse analysis.
+		 */
+		if (plan->oneshot)
+		{
+			Node	   *parsetree = plansource->raw_parse_tree;
+			const char *src = plansource->query_string;
+			List	   *stmt_list;
+
+			/*
+			 * Parameter datatypes are driven by parserSetup hook if provided,
+			 * otherwise we use the fixed parameter list.
+			 */
+			if (plan->parserSetup != NULL)
+			{
+				Assert(plan->nargs == 0);
+				stmt_list = pg_analyze_and_rewrite_params(parsetree,
+														  src,
+														  plan->parserSetup,
+														  plan->parserSetupArg);
+			}
+			else
+			{
+				stmt_list = pg_analyze_and_rewrite(parsetree,
+												   src,
+												   plan->argtypes,
+												   plan->nargs);
+			}
+
+			/* Finish filling in the CachedPlanSource */
+			CompleteCachedPlan(plansource,
+							   stmt_list,
+							   NULL,
+							   plan->argtypes,
+							   plan->nargs,
+							   plan->parserSetup,
+							   plan->parserSetupArg,
+							   plan->cursor_options,
+							   false);		/* not fixed result */
+		}
+
 		/*
 		 * Replan if needed, and increment plan refcount.  If it's a saved
 		 * plan, the refcount must be backed by the CurrentResourceOwner.
@@ -2306,6 +2411,8 @@ _SPI_make_plan_non_temp(SPIPlanPtr plan)
 	/* Assert the input is a temporary SPIPlan */
 	Assert(plan->magic == _SPI_PLAN_MAGIC);
 	Assert(plan->plancxt == NULL);
+	/* One-shot plans can't be saved */
+	Assert(!plan->oneshot);
 
 	/*
 	 * Create a memory context for the plan, underneath the procedure context.
@@ -2323,6 +2430,7 @@ _SPI_make_plan_non_temp(SPIPlanPtr plan)
 	newplan = (SPIPlanPtr) palloc(sizeof(_SPI_plan));
 	newplan->magic = _SPI_PLAN_MAGIC;
 	newplan->saved = false;
+	newplan->oneshot = false;
 	newplan->plancache_list = NIL;
 	newplan->plancxt = plancxt;
 	newplan->cursor_options = plan->cursor_options;
@@ -2372,6 +2480,9 @@ _SPI_save_plan(SPIPlanPtr plan)
 	MemoryContext oldcxt;
 	ListCell   *lc;
 
+	/* One-shot plans can't be saved */
+	Assert(!plan->oneshot);
+
 	/*
 	 * Create a memory context for the plan.  We don't expect the plan to be
 	 * very large, so use smaller-than-default alloc parameters.  It's a
@@ -2388,6 +2499,7 @@ _SPI_save_plan(SPIPlanPtr plan)
 	newplan = (SPIPlanPtr) palloc(sizeof(_SPI_plan));
 	newplan->magic = _SPI_PLAN_MAGIC;
 	newplan->saved = false;
+	newplan->oneshot = false;
 	newplan->plancache_list = NIL;
 	newplan->plancxt = plancxt;
 	newplan->cursor_options = plan->cursor_options;
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index c42765c25a782a095cf3e81e45582c9cf353e558..c46264f1c61f39ac58d74d9d1034c14a41be41f3 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -180,6 +180,7 @@ CreateCachedPlan(Node *raw_parse_tree,
 	plansource->invalItems = NIL;
 	plansource->query_context = NULL;
 	plansource->gplan = NULL;
+	plansource->is_oneshot = false;
 	plansource->is_complete = false;
 	plansource->is_saved = false;
 	plansource->is_valid = false;
@@ -194,6 +195,69 @@ CreateCachedPlan(Node *raw_parse_tree,
 	return plansource;
 }
 
+/*
+ * CreateOneShotCachedPlan: initially create a one-shot plan cache entry.
+ *
+ * This variant of CreateCachedPlan creates a plan cache entry that is meant
+ * to be used only once.  No data copying occurs: all data structures remain
+ * in the caller's memory context (which typically should get cleared after
+ * completing execution).  The CachedPlanSource struct itself is also created
+ * in that context.
+ *
+ * A one-shot plan cannot be saved or copied, since we make no effort to
+ * preserve the raw parse tree unmodified.  There is also no support for
+ * invalidation, so plan use must be completed in the current transaction,
+ * and DDL that might invalidate the querytree_list must be avoided as well.
+ *
+ * raw_parse_tree: output of raw_parser()
+ * query_string: original query text
+ * commandTag: compile-time-constant tag for query, or NULL if empty query
+ */
+CachedPlanSource *
+CreateOneShotCachedPlan(Node *raw_parse_tree,
+						const char *query_string,
+						const char *commandTag)
+{
+	CachedPlanSource *plansource;
+
+	Assert(query_string != NULL);		/* required as of 8.4 */
+
+	/*
+	 * Create and fill the CachedPlanSource struct within the caller's memory
+	 * context.  Most fields are just left empty for the moment.
+	 */
+	plansource = (CachedPlanSource *) palloc0(sizeof(CachedPlanSource));
+	plansource->magic = CACHEDPLANSOURCE_MAGIC;
+	plansource->raw_parse_tree = raw_parse_tree;
+	plansource->query_string = query_string;
+	plansource->commandTag = commandTag;
+	plansource->param_types = NULL;
+	plansource->num_params = 0;
+	plansource->parserSetup = NULL;
+	plansource->parserSetupArg = NULL;
+	plansource->cursor_options = 0;
+	plansource->fixed_result = false;
+	plansource->resultDesc = NULL;
+	plansource->search_path = NULL;
+	plansource->context = CurrentMemoryContext;
+	plansource->query_list = NIL;
+	plansource->relationOids = NIL;
+	plansource->invalItems = NIL;
+	plansource->query_context = NULL;
+	plansource->gplan = NULL;
+	plansource->is_oneshot = true;
+	plansource->is_complete = false;
+	plansource->is_saved = false;
+	plansource->is_valid = false;
+	plansource->generation = 0;
+	plansource->next_saved = NULL;
+	plansource->generic_cost = -1;
+	plansource->total_custom_cost = 0;
+	plansource->num_custom_plans = 0;
+
+	return plansource;
+}
+
 /*
  * CompleteCachedPlan: second step of creating a plan cache entry.
  *
@@ -221,6 +285,10 @@ CreateCachedPlan(Node *raw_parse_tree,
  * option, it is caller's responsibility that the referenced data remains
  * valid for as long as the CachedPlanSource exists.
  *
+ * If the CachedPlanSource is a "oneshot" plan, then no querytree copying
+ * occurs at all, and querytree_context is ignored; it is caller's
+ * responsibility that the passed querytree_list is sufficiently long-lived.
+ *
  * plansource: structure returned by CreateCachedPlan
  * querytree_list: analyzed-and-rewritten form of query (list of Query nodes)
  * querytree_context: memory context containing querytree_list,
@@ -253,9 +321,15 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 	/*
 	 * If caller supplied a querytree_context, reparent it underneath the
 	 * CachedPlanSource's context; otherwise, create a suitable context and
-	 * copy the querytree_list into it.
+	 * copy the querytree_list into it.  But no data copying should be done
+	 * for one-shot plans; for those, assume the passed querytree_list is
+	 * sufficiently long-lived.
 	 */
-	if (querytree_context != NULL)
+	if (plansource->is_oneshot)
+	{
+		querytree_context = CurrentMemoryContext;
+	}
+	else if (querytree_context != NULL)
 	{
 		MemoryContextSetParent(querytree_context, source_context);
 		MemoryContextSwitchTo(querytree_context);
@@ -278,11 +352,12 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 	/*
 	 * Use the planner machinery to extract dependencies.  Data is saved in
 	 * query_context.  (We assume that not a lot of extra cruft is created by
-	 * this call.)
+	 * this call.)  We can skip this for one-shot plans.
 	 */
-	extract_query_dependencies((Node *) querytree_list,
-							   &plansource->relationOids,
-							   &plansource->invalItems);
+	if (!plansource->is_oneshot)
+		extract_query_dependencies((Node *) querytree_list,
+								   &plansource->relationOids,
+								   &plansource->invalItems);
 
 	/*
 	 * Save the final parameter types (or other parameter specification data)
@@ -325,7 +400,8 @@ CompleteCachedPlan(CachedPlanSource *plansource,
  * it to the list of cached plans that are checked for invalidation when an
  * sinval event occurs.
  *
- * This is guaranteed not to throw error; callers typically depend on that
+ * This is guaranteed not to throw error, except for the caller-error case
+ * of trying to save a one-shot plan.  Callers typically depend on that
  * since this is called just before or just after adding a pointer to the
  * CachedPlanSource to some permanent data structure of their own.	Up until
  * this is done, a CachedPlanSource is just transient data that will go away
@@ -339,6 +415,10 @@ SaveCachedPlan(CachedPlanSource *plansource)
 	Assert(plansource->is_complete);
 	Assert(!plansource->is_saved);
 
+	/* This seems worth a real test, though */
+	if (plansource->is_oneshot)
+		elog(ERROR, "cannot save one-shot cached plan");
+
 	/*
 	 * In typical use, this function would be called before generating any
 	 * plans from the CachedPlanSource.  If there is a generic plan, moving it
@@ -401,11 +481,15 @@ DropCachedPlan(CachedPlanSource *plansource)
 	/* Decrement generic CachePlan's refcount and drop if no longer needed */
 	ReleaseGenericPlan(plansource);
 
+	/* Mark it no longer valid */
+	plansource->magic = 0;
+
 	/*
 	 * Remove the CachedPlanSource and all subsidiary data (including the
-	 * query_context if any).
+	 * query_context if any).  But if it's a one-shot we can't free anything.
 	 */
-	MemoryContextDelete(plansource->context);
+	if (!plansource->is_oneshot)
+		MemoryContextDelete(plansource->context);
 }
 
 /*
@@ -450,6 +534,17 @@ RevalidateCachedQuery(CachedPlanSource *plansource)
 	MemoryContext querytree_context;
 	MemoryContext oldcxt;
 
+	/*
+	 * For one-shot plans, we do not support revalidation checking; it's
+	 * assumed the query is parsed, planned, and executed in one transaction,
+	 * so that no lock re-acquisition is necessary.
+	 */
+	if (plansource->is_oneshot)
+	{
+		Assert(plansource->is_valid);
+		return NIL;
+	}
+
 	/*
 	 * If the query is currently valid, acquire locks on the referenced
 	 * objects; then check again.  We need to do it this way to cover the race
@@ -648,6 +743,8 @@ CheckCachedPlan(CachedPlanSource *plansource)
 		return false;
 
 	Assert(plan->magic == CACHEDPLAN_MAGIC);
+	/* Generic plans are never one-shot */
+	Assert(!plan->is_oneshot);
 
 	/*
 	 * If it appears valid, acquire locks and recheck; this is much the same
@@ -707,7 +804,8 @@ CheckCachedPlan(CachedPlanSource *plansource)
  * hint rather than a hard constant.
  *
  * Planning work is done in the caller's memory context.  The finished plan
- * is in a child memory context, which typically should get reparented.
+ * is in a child memory context, which typically should get reparented
+ * (unless this is a one-shot plan, in which case we don't copy the plan).
  */
 static CachedPlan *
 BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
@@ -718,7 +816,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	bool		snapshot_set;
 	bool		spi_pushed;
 	MemoryContext plan_context;
-	MemoryContext oldcxt;
+	MemoryContext oldcxt = CurrentMemoryContext;
 
 	/*
 	 * Normally the querytree should be valid already, but if it's not,
@@ -738,10 +836,16 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 
 	/*
 	 * If we don't already have a copy of the querytree list that can be
-	 * scribbled on by the planner, make one.
+	 * scribbled on by the planner, make one.  For a one-shot plan, we assume
+	 * it's okay to scribble on the original query_list.
 	 */
 	if (qlist == NIL)
-		qlist = (List *) copyObject(plansource->query_list);
+	{
+		if (!plansource->is_oneshot)
+			qlist = (List *) copyObject(plansource->query_list);
+		else
+			qlist = plansource->query_list;
+	}
 
 	/*
 	 * Restore the search_path that was in use when the plan was made. See
@@ -793,22 +897,29 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 	PopOverrideSearchPath();
 
 	/*
-	 * Make a dedicated memory context for the CachedPlan and its subsidiary
-	 * data.  It's probably not going to be large, but just in case, use the
-	 * default maxsize parameter.  It's transient for the moment.
+	 * Normally we make a dedicated memory context for the CachedPlan and its
+	 * subsidiary data.  (It's probably not going to be large, but just in
+	 * case, use the default maxsize parameter.  It's transient for the
+	 * moment.)  But for a one-shot plan, we just leave it in the caller's
+	 * memory context.
 	 */
-	plan_context = AllocSetContextCreate(CurrentMemoryContext,
-										 "CachedPlan",
-										 ALLOCSET_SMALL_MINSIZE,
-										 ALLOCSET_SMALL_INITSIZE,
-										 ALLOCSET_DEFAULT_MAXSIZE);
+	if (!plansource->is_oneshot)
+	{
+		plan_context = AllocSetContextCreate(CurrentMemoryContext,
+											 "CachedPlan",
+											 ALLOCSET_SMALL_MINSIZE,
+											 ALLOCSET_SMALL_INITSIZE,
+											 ALLOCSET_DEFAULT_MAXSIZE);
 
-	/*
-	 * Copy plan into the new context.
-	 */
-	oldcxt = MemoryContextSwitchTo(plan_context);
+		/*
+		 * Copy plan into the new context.
+		 */
+		MemoryContextSwitchTo(plan_context);
 
-	plist = (List *) copyObject(plist);
+		plist = (List *) copyObject(plist);
+	}
+	else
+		plan_context = CurrentMemoryContext;
 
 	/*
 	 * Create and fill the CachedPlan struct within the new context.
@@ -825,6 +936,7 @@ BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 		plan->saved_xmin = InvalidTransactionId;
 	plan->refcount = 0;
 	plan->context = plan_context;
+	plan->is_oneshot = plansource->is_oneshot;
 	plan->is_saved = false;
 	plan->is_valid = true;
 
@@ -846,7 +958,11 @@ choose_custom_plan(CachedPlanSource *plansource, ParamListInfo boundParams)
 {
 	double		avg_custom_cost;
 
-	/* Never any point in a custom plan if there's no parameters */
+	/* One-shot plans will always be considered custom */
+	if (plansource->is_oneshot)
+		return true;
+
+	/* Otherwise, never any point in a custom plan if there's no parameters */
 	if (boundParams == NULL)
 		return false;
 
@@ -1048,7 +1164,14 @@ ReleaseCachedPlan(CachedPlan *plan, bool useResOwner)
 	Assert(plan->refcount > 0);
 	plan->refcount--;
 	if (plan->refcount == 0)
-		MemoryContextDelete(plan->context);
+	{
+		/* Mark it no longer valid */
+		plan->magic = 0;
+
+		/* One-shot plans do not own their context, so we can't free them */
+		if (!plan->is_oneshot)
+			MemoryContextDelete(plan->context);
+	}
 }
 
 /*
@@ -1065,9 +1188,11 @@ CachedPlanSetParentContext(CachedPlanSource *plansource,
 	Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
 	Assert(plansource->is_complete);
 
-	/* This seems worth a real test, though */
+	/* These seem worth real tests, though */
 	if (plansource->is_saved)
 		elog(ERROR, "cannot move a saved cached plan to another context");
+	if (plansource->is_oneshot)
+		elog(ERROR, "cannot move a one-shot cached plan to another context");
 
 	/* OK, let the caller keep the plan where he wishes */
 	MemoryContextSetParent(plansource->context, newcontext);
@@ -1104,6 +1229,13 @@ CopyCachedPlan(CachedPlanSource *plansource)
 	Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
 	Assert(plansource->is_complete);
 
+	/*
+	 * One-shot plans can't be copied, because we haven't taken care that
+	 * parsing/planning didn't scribble on the raw parse tree or querytrees.
+	 */
+	if (plansource->is_oneshot)
+		elog(ERROR, "cannot copy a one-shot cached plan");
+
 	source_context = AllocSetContextCreate(CurrentMemoryContext,
 										   "CachedPlanSource",
 										   ALLOCSET_SMALL_MINSIZE,
@@ -1151,6 +1283,7 @@ CopyCachedPlan(CachedPlanSource *plansource)
 
 	newsource->gplan = NULL;
 
+	newsource->is_oneshot = false;
 	newsource->is_complete = true;
 	newsource->is_saved = false;
 	newsource->is_valid = plansource->is_valid;
diff --git a/src/include/executor/spi_priv.h b/src/include/executor/spi_priv.h
index 4fbb548af4b93d360b6cd48f8f03993bd383594f..1a828912fcddaa89d02a5faf5b837a6d037f679c 100644
--- a/src/include/executor/spi_priv.h
+++ b/src/include/executor/spi_priv.h
@@ -59,6 +59,12 @@ typedef struct
  * while additional data such as argtypes and list cells is loose in the SPI
  * executor context.  Such plans can be identified by having plancxt == NULL.
  *
+ * We can also have "one-shot" SPI plans (which are typically temporary,
+ * as described above).  These are meant to be executed once and discarded,
+ * and various optimizations are made on the assumption of single use.
+ * Note in particular that the CachedPlanSources within such an SPI plan
+ * are not "complete" until execution.
+ *
  * Note: if the original query string contained only whitespace and comments,
  * the plancache_list will be NIL and so there is no place to store the
  * query string.  We don't care about that, but we do care about the
@@ -68,6 +74,7 @@ typedef struct _SPI_plan
 {
 	int			magic;			/* should equal _SPI_PLAN_MAGIC */
 	bool		saved;			/* saved or unsaved plan? */
+	bool		oneshot;		/* one-shot plan? */
 	List	   *plancache_list; /* one CachedPlanSource per parsetree */
 	MemoryContext plancxt;		/* Context containing _SPI_plan and data */
 	int			cursor_options; /* Cursor options used for planning */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 413e8462a6c6e50241574d6ebcb2f38837b0af25..ccc7e3f053b6d1839215c8241c42b7ffdf7e8dbd 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -60,6 +60,14 @@
  * context that holds the rewritten query tree and associated data.  This
  * allows the query tree to be discarded easily when it is invalidated.
  *
+ * Some callers wish to use the CachedPlan API even with one-shot queries
+ * that have no reason to be saved at all.  We therefore support a "oneshot"
+ * variant that does no data copying or invalidation checking.  In this case
+ * there are no separate memory contexts: the CachedPlanSource struct and
+ * all subsidiary data live in the caller's CurrentMemoryContext, and there
+ * is no way to free memory short of clearing that entire context.  A oneshot
+ * plan is always treated as unsaved.
+ *
  * Note: the string referenced by commandTag is not subsidiary storage;
  * it is assumed to be a compile-time-constant string.	As with portals,
  * commandTag shall be NULL if and only if the original query string (before
@@ -69,7 +77,7 @@ typedef struct CachedPlanSource
 {
 	int			magic;			/* should equal CACHEDPLANSOURCE_MAGIC */
 	Node	   *raw_parse_tree; /* output of raw_parser() */
-	char	   *query_string;	/* source text of query */
+	const char *query_string;	/* source text of query */
 	const char *commandTag;		/* command tag (a constant!), or NULL */
 	Oid		   *param_types;	/* array of parameter type OIDs, or NULL */
 	int			num_params;		/* length of param_types array */
@@ -91,6 +99,7 @@ typedef struct CachedPlanSource
 	bool		is_complete;	/* has CompleteCachedPlan been done? */
 	bool		is_saved;		/* has CachedPlanSource been "saved"? */
 	bool		is_valid;		/* is the query_list currently valid? */
+	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	int			generation;		/* increments each time we create a plan */
 	/* If CachedPlanSource has been saved, it is a member of a global list */
 	struct CachedPlanSource *next_saved;		/* list link, if so */
@@ -106,7 +115,9 @@ typedef struct CachedPlanSource
  * (if any), and any active plan executions, so the plan can be discarded
  * exactly when refcount goes to zero.	Both the struct itself and the
  * subsidiary data live in the context denoted by the context field.
- * This makes it easy to free a no-longer-needed cached plan.
+ * This makes it easy to free a no-longer-needed cached plan.  (However,
+ * if is_oneshot is true, the context does not belong solely to the CachedPlan
+ * so no freeing is possible.)
  */
 typedef struct CachedPlan
 {
@@ -115,6 +126,7 @@ typedef struct CachedPlan
 								 * bare utility statements) */
 	bool		is_saved;		/* is CachedPlan in a long-lived context? */
 	bool		is_valid;		/* is the stmt_list currently valid? */
+	bool		is_oneshot;		/* is it a "oneshot" plan? */
 	TransactionId saved_xmin;	/* if valid, replan when TransactionXmin
 								 * changes from this value */
 	int			generation;		/* parent's generation number for this plan */
@@ -129,6 +141,9 @@ extern void ResetPlanCache(void);
 extern CachedPlanSource *CreateCachedPlan(Node *raw_parse_tree,
 				 const char *query_string,
 				 const char *commandTag);
+extern CachedPlanSource *CreateOneShotCachedPlan(Node *raw_parse_tree,
+				 const char *query_string,
+				 const char *commandTag);
 extern void CompleteCachedPlan(CachedPlanSource *plansource,
 				   List *querytree_list,
 				   MemoryContext querytree_context,