Commit 2f48ede0 authored by Tom Lane's avatar Tom Lane

Avoid using a cursor in plpgsql's RETURN QUERY statement.

plpgsql has always executed the query given in a RETURN QUERY command
by opening it as a cursor and then fetching a few rows at a time,
which it turns around and dumps into the function's result tuplestore.
The point of this was to keep from blowing out memory with an oversized
SPITupleTable result (note that while a tuplestore can spill tuples
to disk, SPITupleTable cannot).  However, it's rather inefficient, both
because of extra data copying and because of executor entry/exit
overhead.  In recent versions, a new performance problem has emerged:
use of a cursor prevents use of a parallel plan for the executed query.

We can improve matters by skipping use of a cursor and having the
executor push result tuples directly into the function's result
tuplestore.  However, a moderate amount of new infrastructure is needed
to make that idea work:

* We can use the existing tstoreReceiver.c DestReceiver code to funnel
executor output to the tuplestore, but it has to be extended to support
plpgsql's requirement for possibly applying a tuple conversion map.

* SPI needs to be extended to allow use of a caller-supplied
DestReceiver instead of its usual receiver that puts tuples into
a SPITupleTable.  Two new API calls are needed to handle both the
RETURN QUERY and RETURN QUERY EXECUTE cases.

I also felt that I didn't want these new API calls to use the legacy
method of specifying query parameter values with "char" null flags
(the old ' '/'n' convention); rather they should accept ParamListInfo
objects containing the parameter type and value info.  This required
a bit of additional new infrastructure since we didn't yet have any
parse analysis callback that would interpret $N parameter symbols
according to type data supplied in a ParamListInfo.  There seems to be
no harm in letting makeParamList install that callback by default,
rather than leaving a new ParamListInfo's parserSetup hook as NULL.
(Indeed, as of HEAD, I couldn't find anyplace that was using the
parserSetup field at all; plpgsql was using parserSetupArg for its
own purposes, but parserSetup seemed to be write-only.)

We can actually get plpgsql out of the business of using legacy null
flags altogether, and using ParamListInfo instead of its ad-hoc
PreparedParamsData structure; but this requires inventing one more
SPI API call that can replace SPI_cursor_open_with_args.  That seems
worth doing, though.

SPI_execute_with_args and SPI_cursor_open_with_args are now unused
anywhere in the core PG distribution.  Perhaps someday we could
deprecate/remove them.  But cleaning up the crufty bits of the SPI
API is a task for a different patch.

Per bug #16040 from Jeremy Smith.  This is unfortunately too invasive to
consider back-patching.  Patch by me; thanks to Hamid Akhtar for review.

Discussion: https://postgr.es/m/16040-eaacad11fecfb198@postgresql.org
parent aaf8c990
This diff is collapsed.
......@@ -383,7 +383,9 @@ PersistHoldablePortal(Portal portal)
SetTuplestoreDestReceiverParams(queryDesc->dest,
portal->holdStore,
portal->holdContext,
true);
true,
NULL,
NULL);
/* Fetch the result set into the tuplestore */
ExecutorRun(queryDesc, ForwardScanDirection, 0L, false);
......
......@@ -60,7 +60,8 @@ 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,
bool read_only, bool fire_triggers, uint64 tcount);
bool read_only, bool fire_triggers, uint64 tcount,
DestReceiver *caller_dest);
static ParamListInfo _SPI_convert_params(int nargs, Oid *argtypes,
Datum *Values, const char *Nulls);
......@@ -513,7 +514,7 @@ SPI_execute(const char *src, bool read_only, long tcount)
res = _SPI_execute_plan(&plan, NULL,
InvalidSnapshot, InvalidSnapshot,
read_only, true, tcount);
read_only, true, tcount, NULL);
_SPI_end_call(true);
return res;
......@@ -547,7 +548,7 @@ SPI_execute_plan(SPIPlanPtr plan, Datum *Values, const char *Nulls,
_SPI_convert_params(plan->nargs, plan->argtypes,
Values, Nulls),
InvalidSnapshot, InvalidSnapshot,
read_only, true, tcount);
read_only, true, tcount, NULL);
_SPI_end_call(true);
return res;
......@@ -576,7 +577,36 @@ SPI_execute_plan_with_paramlist(SPIPlanPtr plan, ParamListInfo params,
res = _SPI_execute_plan(plan, params,
InvalidSnapshot, InvalidSnapshot,
read_only, true, tcount);
read_only, true, tcount, NULL);
_SPI_end_call(true);
return res;
}
/*
* Execute a previously prepared plan. If dest isn't NULL, we send result
* tuples to the caller-supplied DestReceiver rather than through the usual
* SPI output arrangements. If dest is NULL this is equivalent to
* SPI_execute_plan_with_paramlist.
*/
int
SPI_execute_plan_with_receiver(SPIPlanPtr plan,
ParamListInfo params,
bool read_only, long tcount,
DestReceiver *dest)
{
int res;
if (plan == NULL || plan->magic != _SPI_PLAN_MAGIC || tcount < 0)
return SPI_ERROR_ARGUMENT;
res = _SPI_begin_call(true);
if (res < 0)
return res;
res = _SPI_execute_plan(plan, params,
InvalidSnapshot, InvalidSnapshot,
read_only, true, tcount, dest);
_SPI_end_call(true);
return res;
......@@ -617,7 +647,7 @@ SPI_execute_snapshot(SPIPlanPtr plan,
_SPI_convert_params(plan->nargs, plan->argtypes,
Values, Nulls),
snapshot, crosscheck_snapshot,
read_only, fire_triggers, tcount);
read_only, fire_triggers, tcount, NULL);
_SPI_end_call(true);
return res;
......@@ -664,7 +694,50 @@ SPI_execute_with_args(const char *src,
res = _SPI_execute_plan(&plan, paramLI,
InvalidSnapshot, InvalidSnapshot,
read_only, true, tcount);
read_only, true, tcount, NULL);
_SPI_end_call(true);
return res;
}
/*
* SPI_execute_with_receiver -- plan and execute a query with arguments
*
* This is the same as SPI_execute_with_args except that parameters are
* supplied through a ParamListInfo, and (if dest isn't NULL) we send
* result tuples to the caller-supplied DestReceiver rather than through
* the usual SPI output arrangements.
*/
int
SPI_execute_with_receiver(const char *src,
ParamListInfo params,
bool read_only, long tcount,
DestReceiver *dest)
{
int res;
_SPI_plan plan;
if (src == NULL || tcount < 0)
return SPI_ERROR_ARGUMENT;
res = _SPI_begin_call(true);
if (res < 0)
return res;
memset(&plan, 0, sizeof(_SPI_plan));
plan.magic = _SPI_PLAN_MAGIC;
plan.cursor_options = CURSOR_OPT_PARALLEL_OK;
if (params)
{
plan.parserSetup = params->parserSetup;
plan.parserSetupArg = params->parserSetupArg;
}
_SPI_prepare_oneshot_plan(src, &plan);
res = _SPI_execute_plan(&plan, params,
InvalidSnapshot, InvalidSnapshot,
read_only, true, tcount, dest);
_SPI_end_call(true);
return res;
......@@ -1303,6 +1376,49 @@ SPI_cursor_open_with_paramlist(const char *name, SPIPlanPtr plan,
return SPI_cursor_open_internal(name, plan, params, read_only);
}
/*
* SPI_cursor_parse_open_with_paramlist()
*
* Same as SPI_cursor_open_with_args except that parameters (if any) are passed
* as a ParamListInfo, which supports dynamic parameter set determination
*/
Portal
SPI_cursor_parse_open_with_paramlist(const char *name,
const char *src,
ParamListInfo params,
bool read_only, int cursorOptions)
{
Portal result;
_SPI_plan plan;
if (src == NULL)
elog(ERROR, "SPI_cursor_parse_open_with_paramlist called with invalid arguments");
SPI_result = _SPI_begin_call(true);
if (SPI_result < 0)
elog(ERROR, "SPI_cursor_parse_open_with_paramlist called while not connected");
memset(&plan, 0, sizeof(_SPI_plan));
plan.magic = _SPI_PLAN_MAGIC;
plan.cursor_options = cursorOptions;
if (params)
{
plan.parserSetup = params->parserSetup;
plan.parserSetupArg = params->parserSetupArg;
}
_SPI_prepare_plan(src, &plan);
/* We needn't copy the plan; SPI_cursor_open_internal will do so */
result = SPI_cursor_open_internal(name, &plan, params, read_only);
/* And clean up */
_SPI_end_call(true);
return result;
}
/*
* SPI_cursor_open_internal()
......@@ -2090,11 +2206,13 @@ _SPI_prepare_oneshot_plan(const char *src, SPIPlanPtr plan)
* fire_triggers: true to fire AFTER triggers at end of query (normal case);
* false means any AFTER triggers are postponed to end of outer query
* tcount: execution tuple-count limit, or 0 for none
* caller_dest: DestReceiver to receive output, or NULL for normal SPI output
*/
static int
_SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
Snapshot snapshot, Snapshot crosscheck_snapshot,
bool read_only, bool fire_triggers, uint64 tcount)
bool read_only, bool fire_triggers, uint64 tcount,
DestReceiver *caller_dest)
{
int my_res = 0;
uint64 my_processed = 0;
......@@ -2228,6 +2346,12 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
bool canSetTag = stmt->canSetTag;
DestReceiver *dest;
/*
* Reset output state. (Note that if a non-SPI receiver is used,
* _SPI_current->processed will stay zero, and that's what we'll
* report to the caller. It's the receiver's job to count tuples
* in that case.)
*/
_SPI_current->processed = 0;
_SPI_current->tuptable = NULL;
......@@ -2267,7 +2391,16 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
UpdateActiveSnapshotCommandId();
}
dest = CreateDestReceiver(canSetTag ? DestSPI : DestNone);
/*
* Select appropriate tuple receiver. Output from non-canSetTag
* subqueries always goes to the bit bucket.
*/
if (!canSetTag)
dest = CreateDestReceiver(DestNone);
else if (caller_dest)
dest = caller_dest;
else
dest = CreateDestReceiver(DestSPI);
if (stmt->utilityStmt == NULL)
{
......@@ -2373,7 +2506,13 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI,
SPI_freetuptable(_SPI_current->tuptable);
_SPI_current->tuptable = NULL;
}
/* we know that the receiver doesn't need a destroy call */
/*
* We don't issue a destroy call to the receiver. The SPI and
* None receivers would ignore it anyway, while if the caller
* supplied a receiver, it's not our job to destroy it.
*/
if (res < 0)
{
my_res = res;
......@@ -2465,7 +2604,7 @@ _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount)
switch (operation)
{
case CMD_SELECT:
if (queryDesc->dest->mydest != DestSPI)
if (queryDesc->dest->mydest == DestNone)
{
/* Don't return SPI_OK_SELECT if we're discarding result */
res = SPI_OK_UTILITY;
......
......@@ -8,6 +8,8 @@
* toasted values. This is to support cursors WITH HOLD, which must retain
* data even if the underlying table is dropped.
*
* Also optionally, we can apply a tuple conversion map before storing.
*
*
* Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
......@@ -21,6 +23,7 @@
#include "postgres.h"
#include "access/detoast.h"
#include "access/tupconvert.h"
#include "executor/tstoreReceiver.h"
......@@ -31,14 +34,19 @@ typedef struct
Tuplestorestate *tstore; /* where to put the data */
MemoryContext cxt; /* context containing tstore */
bool detoast; /* were we told to detoast? */
TupleDesc target_tupdesc; /* target tupdesc, or NULL if none */
const char *map_failure_msg; /* tupdesc mapping failure message */
/* workspace: */
Datum *outvalues; /* values array for result tuple */
Datum *tofree; /* temp values to be pfree'd */
TupleConversionMap *tupmap; /* conversion map, if needed */
TupleTableSlot *mapslot; /* slot for mapped tuples */
} TStoreState;
static bool tstoreReceiveSlot_notoast(TupleTableSlot *slot, DestReceiver *self);
static bool tstoreReceiveSlot_detoast(TupleTableSlot *slot, DestReceiver *self);
static bool tstoreReceiveSlot_tupmap(TupleTableSlot *slot, DestReceiver *self);
/*
......@@ -69,27 +77,46 @@ tstoreStartupReceiver(DestReceiver *self, int operation, TupleDesc typeinfo)
}
}
/* Check if tuple conversion is needed */
if (myState->target_tupdesc)
myState->tupmap = convert_tuples_by_position(typeinfo,
myState->target_tupdesc,
myState->map_failure_msg);
else
myState->tupmap = NULL;
/* Set up appropriate callback */
if (needtoast)
{
Assert(!myState->tupmap);
myState->pub.receiveSlot = tstoreReceiveSlot_detoast;
/* Create workspace */
myState->outvalues = (Datum *)
MemoryContextAlloc(myState->cxt, natts * sizeof(Datum));
myState->tofree = (Datum *)
MemoryContextAlloc(myState->cxt, natts * sizeof(Datum));
myState->mapslot = NULL;
}
else if (myState->tupmap)
{
myState->pub.receiveSlot = tstoreReceiveSlot_tupmap;
myState->outvalues = NULL;
myState->tofree = NULL;
myState->mapslot = MakeSingleTupleTableSlot(myState->target_tupdesc,
&TTSOpsVirtual);
}
else
{
myState->pub.receiveSlot = tstoreReceiveSlot_notoast;
myState->outvalues = NULL;
myState->tofree = NULL;
myState->mapslot = NULL;
}
}
/*
* Receive a tuple from the executor and store it in the tuplestore.
* This is for the easy case where we don't have to detoast.
* This is for the easy case where we don't have to detoast nor map anything.
*/
static bool
tstoreReceiveSlot_notoast(TupleTableSlot *slot, DestReceiver *self)
......@@ -157,6 +184,21 @@ tstoreReceiveSlot_detoast(TupleTableSlot *slot, DestReceiver *self)
return true;
}
/*
* Receive a tuple from the executor and store it in the tuplestore.
* This is for the case where we must apply a tuple conversion map.
*/
static bool
tstoreReceiveSlot_tupmap(TupleTableSlot *slot, DestReceiver *self)
{
TStoreState *myState = (TStoreState *) self;
execute_attr_map_slot(myState->tupmap->attrMap, slot, myState->mapslot);
tuplestore_puttupleslot(myState->tstore, myState->mapslot);
return true;
}
/*
* Clean up at end of an executor run
*/
......@@ -172,6 +214,12 @@ tstoreShutdownReceiver(DestReceiver *self)
if (myState->tofree)
pfree(myState->tofree);
myState->tofree = NULL;
if (myState->tupmap)
free_conversion_map(myState->tupmap);
myState->tupmap = NULL;
if (myState->mapslot)
ExecDropSingleTupleTableSlot(myState->mapslot);
myState->mapslot = NULL;
}
/*
......@@ -204,17 +252,32 @@ CreateTuplestoreDestReceiver(void)
/*
* Set parameters for a TuplestoreDestReceiver
*
* tStore: where to store the tuples
* tContext: memory context containing tStore
* detoast: forcibly detoast contained data?
* target_tupdesc: if not NULL, forcibly convert tuples to this rowtype
* map_failure_msg: error message to use if mapping to target_tupdesc fails
*
* We don't currently support both detoast and target_tupdesc at the same
* time, just because no existing caller needs that combination.
*/
void
SetTuplestoreDestReceiverParams(DestReceiver *self,
Tuplestorestate *tStore,
MemoryContext tContext,
bool detoast)
bool detoast,
TupleDesc target_tupdesc,
const char *map_failure_msg)
{
TStoreState *myState = (TStoreState *) self;
Assert(!(detoast && target_tupdesc));
Assert(myState->pub.mydest == DestTuplestore);
myState->tstore = tStore;
myState->cxt = tContext;
myState->detoast = detoast;
myState->target_tupdesc = target_tupdesc;
myState->map_failure_msg = map_failure_msg;
}
......@@ -17,19 +17,27 @@
#include "access/xact.h"
#include "mb/stringinfo_mb.h"
#include "nodes/bitmapset.h"
#include "nodes/params.h"
#include "parser/parse_node.h"
#include "storage/shmem.h"
#include "utils/datum.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
static void paramlist_parser_setup(ParseState *pstate, void *arg);
static Node *paramlist_param_ref(ParseState *pstate, ParamRef *pref);
/*
* Allocate and initialize a new ParamListInfo structure.
*
* To make a new structure for the "dynamic" way (with hooks), pass 0 for
* numParams and set numParams manually.
*
* A default parserSetup function is supplied automatically. Callers may
* override it if they choose. (Note that most use-cases for ParamListInfos
* will never use the parserSetup function anyway.)
*/
ParamListInfo
makeParamList(int numParams)
......@@ -45,8 +53,8 @@ makeParamList(int numParams)
retval->paramFetchArg = NULL;
retval->paramCompile = NULL;
retval->paramCompileArg = NULL;
retval->parserSetup = NULL;
retval->parserSetupArg = NULL;
retval->parserSetup = paramlist_parser_setup;
retval->parserSetupArg = (void *) retval;
retval->paramValuesStr = NULL;
retval->numParams = numParams;
......@@ -102,6 +110,55 @@ copyParamList(ParamListInfo from)
return retval;
}
/*
* Set up to parse a query containing references to parameters
* sourced from a ParamListInfo.
*/
static void
paramlist_parser_setup(ParseState *pstate, void *arg)
{
pstate->p_paramref_hook = paramlist_param_ref;
/* no need to use p_coerce_param_hook */
pstate->p_ref_hook_state = arg;
}
/*
* Transform a ParamRef using parameter type data from a ParamListInfo.
*/
static Node *
paramlist_param_ref(ParseState *pstate, ParamRef *pref)
{
ParamListInfo paramLI = (ParamListInfo) pstate->p_ref_hook_state;
int paramno = pref->number;
ParamExternData *prm;
ParamExternData prmdata;
Param *param;
/* check parameter number is valid */
if (paramno <= 0 || paramno > paramLI->numParams)
return NULL;
/* give hook a chance in case parameter is dynamic */
if (paramLI->paramFetch != NULL)
prm = paramLI->paramFetch(paramLI, paramno, false, &prmdata);
else
prm = &paramLI->params[paramno - 1];
if (!OidIsValid(prm->ptype))
return NULL;
param = makeNode(Param);
param->paramkind = PARAM_EXTERN;
param->paramid = paramno;
param->paramtype = prm->ptype;
param->paramtypmod = -1;
param->paramcollid = get_typcollation(param->paramtype);
param->location = pref->location;
return (Node *) param;
}
/*
* Estimate the amount of space required to serialize a ParamListInfo.
*/
......
......@@ -996,7 +996,9 @@ FillPortalStore(Portal portal, bool isTopLevel)
SetTuplestoreDestReceiverParams(treceiver,
portal->holdStore,
portal->holdContext,
false);
false,
NULL,
NULL);
switch (portal->strategy)
{
......
......@@ -90,6 +90,10 @@ extern int SPI_execute_plan(SPIPlanPtr plan, Datum *Values, const char *Nulls,
extern int SPI_execute_plan_with_paramlist(SPIPlanPtr plan,
ParamListInfo params,
bool read_only, long tcount);
extern int SPI_execute_plan_with_receiver(SPIPlanPtr plan,
ParamListInfo params,
bool read_only, long tcount,
DestReceiver *dest);
extern int SPI_exec(const char *src, long tcount);
extern int SPI_execp(SPIPlanPtr plan, Datum *Values, const char *Nulls,
long tcount);
......@@ -102,6 +106,10 @@ extern int SPI_execute_with_args(const char *src,
int nargs, Oid *argtypes,
Datum *Values, const char *Nulls,
bool read_only, long tcount);
extern int SPI_execute_with_receiver(const char *src,
ParamListInfo params,
bool read_only, long tcount,
DestReceiver *dest);
extern SPIPlanPtr SPI_prepare(const char *src, int nargs, Oid *argtypes);
extern SPIPlanPtr SPI_prepare_cursor(const char *src, int nargs, Oid *argtypes,
int cursorOptions);
......@@ -150,6 +158,11 @@ extern Portal SPI_cursor_open_with_args(const char *name,
bool read_only, int cursorOptions);
extern Portal SPI_cursor_open_with_paramlist(const char *name, SPIPlanPtr plan,
ParamListInfo params, bool read_only);
extern Portal SPI_cursor_parse_open_with_paramlist(const char *name,
const char *src,
ParamListInfo params,
bool read_only,
int cursorOptions);
extern Portal SPI_cursor_find(const char *name);
extern void SPI_cursor_fetch(Portal portal, bool forward, long count);
extern void SPI_cursor_move(Portal portal, bool forward, long count);
......
......@@ -24,6 +24,8 @@ extern DestReceiver *CreateTuplestoreDestReceiver(void);
extern void SetTuplestoreDestReceiverParams(DestReceiver *self,
Tuplestorestate *tStore,
MemoryContext tContext,
bool detoast);
bool detoast,
TupleDesc target_tupdesc,
const char *map_failure_msg);
#endif /* TSTORE_RECEIVER_H */
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment