mirror of
https://github.com/postgres/postgres.git
synced 2025-05-01 01:04:50 +03:00
The original syntax made it difficult to add options without making them into reserved words. This change parenthesizes the options to avoid that problem, and makes provision for an explicit (and perhaps non-Boolean) value for each option. The original syntax is still supported, but only for the two original options ANALYZE and VERBOSE. As a test case, add a COSTS option that can suppress the planner cost estimates. This may be useful for including EXPLAIN output in the regression tests, which are otherwise unable to cope with cross-platform variations in cost estimates. Robert Haas
1192 lines
30 KiB
C
1192 lines
30 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* explain.c
|
|
* Explain query execution plans
|
|
*
|
|
* Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group
|
|
* Portions Copyright (c) 1994-5, Regents of the University of California
|
|
*
|
|
* IDENTIFICATION
|
|
* $PostgreSQL: pgsql/src/backend/commands/explain.c,v 1.188 2009/07/26 23:34:17 tgl Exp $
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#include "postgres.h"
|
|
|
|
#include "access/xact.h"
|
|
#include "catalog/pg_constraint.h"
|
|
#include "catalog/pg_type.h"
|
|
#include "commands/defrem.h"
|
|
#include "commands/explain.h"
|
|
#include "commands/prepare.h"
|
|
#include "commands/trigger.h"
|
|
#include "executor/instrument.h"
|
|
#include "optimizer/clauses.h"
|
|
#include "optimizer/planner.h"
|
|
#include "optimizer/var.h"
|
|
#include "parser/parsetree.h"
|
|
#include "rewrite/rewriteHandler.h"
|
|
#include "tcop/tcopprot.h"
|
|
#include "utils/builtins.h"
|
|
#include "utils/guc.h"
|
|
#include "utils/lsyscache.h"
|
|
#include "utils/tuplesort.h"
|
|
#include "utils/snapmgr.h"
|
|
|
|
|
|
/* Hook for plugins to get control in ExplainOneQuery() */
|
|
ExplainOneQuery_hook_type ExplainOneQuery_hook = NULL;
|
|
|
|
/* Hook for plugins to get control in explain_get_index_name() */
|
|
explain_get_index_name_hook_type explain_get_index_name_hook = NULL;
|
|
|
|
|
|
static void ExplainOneQuery(Query *query, ExplainState *es,
|
|
const char *queryString, ParamListInfo params);
|
|
static void report_triggers(ResultRelInfo *rInfo, bool show_relname,
|
|
StringInfo buf);
|
|
static double elapsed_time(instr_time *starttime);
|
|
static void ExplainNode(Plan *plan, PlanState *planstate,
|
|
Plan *outer_plan, int indent, ExplainState *es);
|
|
static void show_plan_tlist(Plan *plan, int indent, ExplainState *es);
|
|
static void show_qual(List *qual, const char *qlabel, Plan *plan,
|
|
Plan *outer_plan, int indent, bool useprefix, ExplainState *es);
|
|
static void show_scan_qual(List *qual, const char *qlabel,
|
|
Plan *scan_plan, Plan *outer_plan,
|
|
int indent, ExplainState *es);
|
|
static void show_upper_qual(List *qual, const char *qlabel, Plan *plan,
|
|
int indent, ExplainState *es);
|
|
static void show_sort_keys(Plan *sortplan, int indent, ExplainState *es);
|
|
static void show_sort_info(SortState *sortstate, int indent, ExplainState *es);
|
|
static const char *explain_get_index_name(Oid indexId);
|
|
static void ExplainScanTarget(Scan *plan, ExplainState *es);
|
|
static void ExplainMemberNodes(List *plans, PlanState **planstate,
|
|
Plan *outer_plan, int indent, ExplainState *es);
|
|
static void ExplainSubPlans(List *plans, int indent, ExplainState *es);
|
|
|
|
|
|
/*
|
|
* ExplainQuery -
|
|
* execute an EXPLAIN command
|
|
*/
|
|
void
|
|
ExplainQuery(ExplainStmt *stmt, const char *queryString,
|
|
ParamListInfo params, DestReceiver *dest)
|
|
{
|
|
ExplainState es;
|
|
Oid *param_types;
|
|
int num_params;
|
|
TupOutputState *tstate;
|
|
List *rewritten;
|
|
ListCell *lc;
|
|
|
|
/* Initialize ExplainState. */
|
|
ExplainInitState(&es);
|
|
|
|
/* Parse options list. */
|
|
foreach(lc, stmt->options)
|
|
{
|
|
DefElem *opt = (DefElem *) lfirst(lc);
|
|
|
|
if (strcmp(opt->defname, "analyze") == 0)
|
|
es.analyze = defGetBoolean(opt);
|
|
else if (strcmp(opt->defname, "verbose") == 0)
|
|
es.verbose = defGetBoolean(opt);
|
|
else if (strcmp(opt->defname, "costs") == 0)
|
|
es.costs = defGetBoolean(opt);
|
|
else
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("unrecognized EXPLAIN option \"%s\"",
|
|
opt->defname)));
|
|
}
|
|
|
|
/* Convert parameter type data to the form parser wants */
|
|
getParamListTypes(params, ¶m_types, &num_params);
|
|
|
|
/*
|
|
* Run parse analysis and rewrite. Note this also acquires sufficient
|
|
* locks on the source table(s).
|
|
*
|
|
* Because the parser and planner tend to scribble on their input, we make
|
|
* a preliminary copy of the source querytree. This prevents problems in
|
|
* the case that the EXPLAIN is in a portal or plpgsql function and is
|
|
* executed repeatedly. (See also the same hack in DECLARE CURSOR and
|
|
* PREPARE.) XXX FIXME someday.
|
|
*/
|
|
rewritten = pg_analyze_and_rewrite((Node *) copyObject(stmt->query),
|
|
queryString, param_types, num_params);
|
|
|
|
if (rewritten == NIL)
|
|
{
|
|
/* In the case of an INSTEAD NOTHING, tell at least that */
|
|
appendStringInfoString(es.str, "Query rewrites to nothing\n");
|
|
}
|
|
else
|
|
{
|
|
ListCell *l;
|
|
|
|
/* Explain every plan */
|
|
foreach(l, rewritten)
|
|
{
|
|
ExplainOneQuery((Query *) lfirst(l), &es, queryString, params);
|
|
/* put a blank line between plans */
|
|
if (lnext(l) != NULL)
|
|
appendStringInfoChar(es.str, '\n');
|
|
}
|
|
}
|
|
|
|
/* output tuples */
|
|
tstate = begin_tup_output_tupdesc(dest, ExplainResultDesc(stmt));
|
|
do_text_output_multiline(tstate, es.str->data);
|
|
end_tup_output(tstate);
|
|
|
|
pfree(es.str->data);
|
|
}
|
|
|
|
/*
|
|
* Initialize ExplainState.
|
|
*/
|
|
void
|
|
ExplainInitState(ExplainState *es)
|
|
{
|
|
/* Set default options. */
|
|
memset(es, 0, sizeof(ExplainState));
|
|
es->costs = true;
|
|
/* Prepare output buffer. */
|
|
es->str = makeStringInfo();
|
|
}
|
|
|
|
/*
|
|
* ExplainResultDesc -
|
|
* construct the result tupledesc for an EXPLAIN
|
|
*/
|
|
TupleDesc
|
|
ExplainResultDesc(ExplainStmt *stmt)
|
|
{
|
|
TupleDesc tupdesc;
|
|
|
|
/* need a tuple descriptor representing a single TEXT column */
|
|
tupdesc = CreateTemplateTupleDesc(1, false);
|
|
TupleDescInitEntry(tupdesc, (AttrNumber) 1, "QUERY PLAN",
|
|
TEXTOID, -1, 0);
|
|
return tupdesc;
|
|
}
|
|
|
|
/*
|
|
* ExplainOneQuery -
|
|
* print out the execution plan for one Query
|
|
*/
|
|
static void
|
|
ExplainOneQuery(Query *query, ExplainState *es,
|
|
const char *queryString, ParamListInfo params)
|
|
{
|
|
/* planner will not cope with utility statements */
|
|
if (query->commandType == CMD_UTILITY)
|
|
{
|
|
ExplainOneUtility(query->utilityStmt, es, queryString, params);
|
|
return;
|
|
}
|
|
|
|
/* if an advisor plugin is present, let it manage things */
|
|
if (ExplainOneQuery_hook)
|
|
(*ExplainOneQuery_hook) (query, es, queryString, params);
|
|
else
|
|
{
|
|
PlannedStmt *plan;
|
|
|
|
/* plan the query */
|
|
plan = pg_plan_query(query, 0, params);
|
|
|
|
/* run it (if needed) and produce output */
|
|
ExplainOnePlan(plan, es, queryString, params);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* ExplainOneUtility -
|
|
* print out the execution plan for one utility statement
|
|
* (In general, utility statements don't have plans, but there are some
|
|
* we treat as special cases)
|
|
*
|
|
* This is exported because it's called back from prepare.c in the
|
|
* EXPLAIN EXECUTE case
|
|
*/
|
|
void
|
|
ExplainOneUtility(Node *utilityStmt, ExplainState *es,
|
|
const char *queryString, ParamListInfo params)
|
|
{
|
|
if (utilityStmt == NULL)
|
|
return;
|
|
|
|
if (IsA(utilityStmt, ExecuteStmt))
|
|
ExplainExecuteQuery((ExecuteStmt *) utilityStmt, es,
|
|
queryString, params);
|
|
else if (IsA(utilityStmt, NotifyStmt))
|
|
appendStringInfoString(es->str, "NOTIFY\n");
|
|
else
|
|
appendStringInfoString(es->str,
|
|
"Utility statements have no plan structure\n");
|
|
}
|
|
|
|
/*
|
|
* ExplainOnePlan -
|
|
* given a planned query, execute it if needed, and then print
|
|
* EXPLAIN output
|
|
*
|
|
* Since we ignore any DeclareCursorStmt that might be attached to the query,
|
|
* if you say EXPLAIN ANALYZE DECLARE CURSOR then we'll actually run the
|
|
* query. This is different from pre-8.3 behavior but seems more useful than
|
|
* not running the query. No cursor will be created, however.
|
|
*
|
|
* This is exported because it's called back from prepare.c in the
|
|
* EXPLAIN EXECUTE case, and because an index advisor plugin would need
|
|
* to call it.
|
|
*/
|
|
void
|
|
ExplainOnePlan(PlannedStmt *plannedstmt, ExplainState *es,
|
|
const char *queryString, ParamListInfo params)
|
|
{
|
|
QueryDesc *queryDesc;
|
|
instr_time starttime;
|
|
double totaltime = 0;
|
|
int eflags;
|
|
|
|
/*
|
|
* Use a snapshot with an updated command ID to ensure this query sees
|
|
* results of any previously executed queries.
|
|
*/
|
|
PushUpdatedSnapshot(GetActiveSnapshot());
|
|
|
|
/* Create a QueryDesc requesting no output */
|
|
queryDesc = CreateQueryDesc(plannedstmt, queryString,
|
|
GetActiveSnapshot(), InvalidSnapshot,
|
|
None_Receiver, params, es->analyze);
|
|
|
|
INSTR_TIME_SET_CURRENT(starttime);
|
|
|
|
/* If analyzing, we need to cope with queued triggers */
|
|
if (es->analyze)
|
|
AfterTriggerBeginQuery();
|
|
|
|
/* Select execution options */
|
|
if (es->analyze)
|
|
eflags = 0; /* default run-to-completion flags */
|
|
else
|
|
eflags = EXEC_FLAG_EXPLAIN_ONLY;
|
|
|
|
/* call ExecutorStart to prepare the plan for execution */
|
|
ExecutorStart(queryDesc, eflags);
|
|
|
|
/* Execute the plan for statistics if asked for */
|
|
if (es->analyze)
|
|
{
|
|
/* run the plan */
|
|
ExecutorRun(queryDesc, ForwardScanDirection, 0L);
|
|
|
|
/* We can't clean up 'till we're done printing the stats... */
|
|
totaltime += elapsed_time(&starttime);
|
|
}
|
|
|
|
/* Create textual dump of plan tree */
|
|
ExplainPrintPlan(es, queryDesc);
|
|
|
|
/*
|
|
* If we ran the command, run any AFTER triggers it queued. (Note this
|
|
* will not include DEFERRED triggers; since those don't run until end of
|
|
* transaction, we can't measure them.) Include into total runtime.
|
|
*/
|
|
if (es->analyze)
|
|
{
|
|
INSTR_TIME_SET_CURRENT(starttime);
|
|
AfterTriggerEndQuery(queryDesc->estate);
|
|
totaltime += elapsed_time(&starttime);
|
|
}
|
|
|
|
/* Print info about runtime of triggers */
|
|
if (es->analyze)
|
|
{
|
|
ResultRelInfo *rInfo;
|
|
bool show_relname;
|
|
int numrels = queryDesc->estate->es_num_result_relations;
|
|
List *targrels = queryDesc->estate->es_trig_target_relations;
|
|
int nr;
|
|
ListCell *l;
|
|
|
|
show_relname = (numrels > 1 || targrels != NIL);
|
|
rInfo = queryDesc->estate->es_result_relations;
|
|
for (nr = 0; nr < numrels; rInfo++, nr++)
|
|
report_triggers(rInfo, show_relname, es->str);
|
|
|
|
foreach(l, targrels)
|
|
{
|
|
rInfo = (ResultRelInfo *) lfirst(l);
|
|
report_triggers(rInfo, show_relname, es->str);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Close down the query and free resources. Include time for this in the
|
|
* total runtime (although it should be pretty minimal).
|
|
*/
|
|
INSTR_TIME_SET_CURRENT(starttime);
|
|
|
|
ExecutorEnd(queryDesc);
|
|
|
|
FreeQueryDesc(queryDesc);
|
|
|
|
PopActiveSnapshot();
|
|
|
|
/* We need a CCI just in case query expanded to multiple plans */
|
|
if (es->analyze)
|
|
CommandCounterIncrement();
|
|
|
|
totaltime += elapsed_time(&starttime);
|
|
|
|
if (es->analyze)
|
|
appendStringInfo(es->str, "Total runtime: %.3f ms\n",
|
|
1000.0 * totaltime);
|
|
}
|
|
|
|
/*
|
|
* ExplainPrintPlan -
|
|
* convert a QueryDesc's plan tree to text and append it to es->str
|
|
*
|
|
* The caller should have set up the options fields of *es, as well as
|
|
* initializing the output buffer es->str. Other fields in *es are
|
|
* initialized here.
|
|
*
|
|
* NB: will not work on utility statements
|
|
*/
|
|
void
|
|
ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
|
|
{
|
|
Assert(queryDesc->plannedstmt != NULL);
|
|
es->pstmt = queryDesc->plannedstmt;
|
|
es->rtable = queryDesc->plannedstmt->rtable;
|
|
ExplainNode(queryDesc->plannedstmt->planTree, queryDesc->planstate,
|
|
NULL, 0, es);
|
|
}
|
|
|
|
/*
|
|
* report_triggers -
|
|
* report execution stats for a single relation's triggers
|
|
*/
|
|
static void
|
|
report_triggers(ResultRelInfo *rInfo, bool show_relname, StringInfo buf)
|
|
{
|
|
int nt;
|
|
|
|
if (!rInfo->ri_TrigDesc || !rInfo->ri_TrigInstrument)
|
|
return;
|
|
for (nt = 0; nt < rInfo->ri_TrigDesc->numtriggers; nt++)
|
|
{
|
|
Trigger *trig = rInfo->ri_TrigDesc->triggers + nt;
|
|
Instrumentation *instr = rInfo->ri_TrigInstrument + nt;
|
|
char *conname;
|
|
|
|
/* Must clean up instrumentation state */
|
|
InstrEndLoop(instr);
|
|
|
|
/*
|
|
* We ignore triggers that were never invoked; they likely aren't
|
|
* relevant to the current query type.
|
|
*/
|
|
if (instr->ntuples == 0)
|
|
continue;
|
|
|
|
if (OidIsValid(trig->tgconstraint) &&
|
|
(conname = get_constraint_name(trig->tgconstraint)) != NULL)
|
|
{
|
|
appendStringInfo(buf, "Trigger for constraint %s", conname);
|
|
pfree(conname);
|
|
}
|
|
else
|
|
appendStringInfo(buf, "Trigger %s", trig->tgname);
|
|
|
|
if (show_relname)
|
|
appendStringInfo(buf, " on %s",
|
|
RelationGetRelationName(rInfo->ri_RelationDesc));
|
|
|
|
appendStringInfo(buf, ": time=%.3f calls=%.0f\n",
|
|
1000.0 * instr->total, instr->ntuples);
|
|
}
|
|
}
|
|
|
|
/* Compute elapsed time in seconds since given timestamp */
|
|
static double
|
|
elapsed_time(instr_time *starttime)
|
|
{
|
|
instr_time endtime;
|
|
|
|
INSTR_TIME_SET_CURRENT(endtime);
|
|
INSTR_TIME_SUBTRACT(endtime, *starttime);
|
|
return INSTR_TIME_GET_DOUBLE(endtime);
|
|
}
|
|
|
|
/*
|
|
* ExplainNode -
|
|
* converts a Plan node into ascii string and appends it to es->str
|
|
*
|
|
* planstate points to the executor state node corresponding to the plan node.
|
|
* We need this to get at the instrumentation data (if any) as well as the
|
|
* list of subplans.
|
|
*
|
|
* outer_plan, if not null, references another plan node that is the outer
|
|
* side of a join with the current node. This is only interesting for
|
|
* deciphering runtime keys of an inner indexscan.
|
|
*
|
|
* If indent is positive, we indent the plan output accordingly and put "->"
|
|
* in front of it. This should only happen for child plan nodes.
|
|
*/
|
|
static void
|
|
ExplainNode(Plan *plan, PlanState *planstate,
|
|
Plan *outer_plan,
|
|
int indent, ExplainState *es)
|
|
{
|
|
const char *pname;
|
|
|
|
if (indent)
|
|
{
|
|
Assert(indent >= 2);
|
|
appendStringInfoSpaces(es->str, 2 * indent - 4);
|
|
appendStringInfoString(es->str, "-> ");
|
|
}
|
|
|
|
if (plan == NULL)
|
|
{
|
|
appendStringInfoChar(es->str, '\n');
|
|
return;
|
|
}
|
|
|
|
switch (nodeTag(plan))
|
|
{
|
|
case T_Result:
|
|
pname = "Result";
|
|
break;
|
|
case T_Append:
|
|
pname = "Append";
|
|
break;
|
|
case T_RecursiveUnion:
|
|
pname = "Recursive Union";
|
|
break;
|
|
case T_BitmapAnd:
|
|
pname = "BitmapAnd";
|
|
break;
|
|
case T_BitmapOr:
|
|
pname = "BitmapOr";
|
|
break;
|
|
case T_NestLoop:
|
|
switch (((NestLoop *) plan)->join.jointype)
|
|
{
|
|
case JOIN_INNER:
|
|
pname = "Nested Loop";
|
|
break;
|
|
case JOIN_LEFT:
|
|
pname = "Nested Loop Left Join";
|
|
break;
|
|
case JOIN_FULL:
|
|
pname = "Nested Loop Full Join";
|
|
break;
|
|
case JOIN_RIGHT:
|
|
pname = "Nested Loop Right Join";
|
|
break;
|
|
case JOIN_SEMI:
|
|
pname = "Nested Loop Semi Join";
|
|
break;
|
|
case JOIN_ANTI:
|
|
pname = "Nested Loop Anti Join";
|
|
break;
|
|
default:
|
|
pname = "Nested Loop ??? Join";
|
|
break;
|
|
}
|
|
break;
|
|
case T_MergeJoin:
|
|
switch (((MergeJoin *) plan)->join.jointype)
|
|
{
|
|
case JOIN_INNER:
|
|
pname = "Merge Join";
|
|
break;
|
|
case JOIN_LEFT:
|
|
pname = "Merge Left Join";
|
|
break;
|
|
case JOIN_FULL:
|
|
pname = "Merge Full Join";
|
|
break;
|
|
case JOIN_RIGHT:
|
|
pname = "Merge Right Join";
|
|
break;
|
|
case JOIN_SEMI:
|
|
pname = "Merge Semi Join";
|
|
break;
|
|
case JOIN_ANTI:
|
|
pname = "Merge Anti Join";
|
|
break;
|
|
default:
|
|
pname = "Merge ??? Join";
|
|
break;
|
|
}
|
|
break;
|
|
case T_HashJoin:
|
|
switch (((HashJoin *) plan)->join.jointype)
|
|
{
|
|
case JOIN_INNER:
|
|
pname = "Hash Join";
|
|
break;
|
|
case JOIN_LEFT:
|
|
pname = "Hash Left Join";
|
|
break;
|
|
case JOIN_FULL:
|
|
pname = "Hash Full Join";
|
|
break;
|
|
case JOIN_RIGHT:
|
|
pname = "Hash Right Join";
|
|
break;
|
|
case JOIN_SEMI:
|
|
pname = "Hash Semi Join";
|
|
break;
|
|
case JOIN_ANTI:
|
|
pname = "Hash Anti Join";
|
|
break;
|
|
default:
|
|
pname = "Hash ??? Join";
|
|
break;
|
|
}
|
|
break;
|
|
case T_SeqScan:
|
|
pname = "Seq Scan";
|
|
break;
|
|
case T_IndexScan:
|
|
pname = "Index Scan";
|
|
break;
|
|
case T_BitmapIndexScan:
|
|
pname = "Bitmap Index Scan";
|
|
break;
|
|
case T_BitmapHeapScan:
|
|
pname = "Bitmap Heap Scan";
|
|
break;
|
|
case T_TidScan:
|
|
pname = "Tid Scan";
|
|
break;
|
|
case T_SubqueryScan:
|
|
pname = "Subquery Scan";
|
|
break;
|
|
case T_FunctionScan:
|
|
pname = "Function Scan";
|
|
break;
|
|
case T_ValuesScan:
|
|
pname = "Values Scan";
|
|
break;
|
|
case T_CteScan:
|
|
pname = "CTE Scan";
|
|
break;
|
|
case T_WorkTableScan:
|
|
pname = "WorkTable Scan";
|
|
break;
|
|
case T_Material:
|
|
pname = "Materialize";
|
|
break;
|
|
case T_Sort:
|
|
pname = "Sort";
|
|
break;
|
|
case T_Group:
|
|
pname = "Group";
|
|
break;
|
|
case T_Agg:
|
|
switch (((Agg *) plan)->aggstrategy)
|
|
{
|
|
case AGG_PLAIN:
|
|
pname = "Aggregate";
|
|
break;
|
|
case AGG_SORTED:
|
|
pname = "GroupAggregate";
|
|
break;
|
|
case AGG_HASHED:
|
|
pname = "HashAggregate";
|
|
break;
|
|
default:
|
|
pname = "Aggregate ???";
|
|
break;
|
|
}
|
|
break;
|
|
case T_WindowAgg:
|
|
pname = "WindowAgg";
|
|
break;
|
|
case T_Unique:
|
|
pname = "Unique";
|
|
break;
|
|
case T_SetOp:
|
|
switch (((SetOp *) plan)->strategy)
|
|
{
|
|
case SETOP_SORTED:
|
|
switch (((SetOp *) plan)->cmd)
|
|
{
|
|
case SETOPCMD_INTERSECT:
|
|
pname = "SetOp Intersect";
|
|
break;
|
|
case SETOPCMD_INTERSECT_ALL:
|
|
pname = "SetOp Intersect All";
|
|
break;
|
|
case SETOPCMD_EXCEPT:
|
|
pname = "SetOp Except";
|
|
break;
|
|
case SETOPCMD_EXCEPT_ALL:
|
|
pname = "SetOp Except All";
|
|
break;
|
|
default:
|
|
pname = "SetOp ???";
|
|
break;
|
|
}
|
|
break;
|
|
case SETOP_HASHED:
|
|
switch (((SetOp *) plan)->cmd)
|
|
{
|
|
case SETOPCMD_INTERSECT:
|
|
pname = "HashSetOp Intersect";
|
|
break;
|
|
case SETOPCMD_INTERSECT_ALL:
|
|
pname = "HashSetOp Intersect All";
|
|
break;
|
|
case SETOPCMD_EXCEPT:
|
|
pname = "HashSetOp Except";
|
|
break;
|
|
case SETOPCMD_EXCEPT_ALL:
|
|
pname = "HashSetOp Except All";
|
|
break;
|
|
default:
|
|
pname = "HashSetOp ???";
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
pname = "SetOp ???";
|
|
break;
|
|
}
|
|
break;
|
|
case T_Limit:
|
|
pname = "Limit";
|
|
break;
|
|
case T_Hash:
|
|
pname = "Hash";
|
|
break;
|
|
default:
|
|
pname = "???";
|
|
break;
|
|
}
|
|
|
|
appendStringInfoString(es->str, pname);
|
|
switch (nodeTag(plan))
|
|
{
|
|
case T_IndexScan:
|
|
if (ScanDirectionIsBackward(((IndexScan *) plan)->indexorderdir))
|
|
appendStringInfoString(es->str, " Backward");
|
|
appendStringInfo(es->str, " using %s",
|
|
explain_get_index_name(((IndexScan *) plan)->indexid));
|
|
/* FALL THRU */
|
|
case T_SeqScan:
|
|
case T_BitmapHeapScan:
|
|
case T_TidScan:
|
|
case T_SubqueryScan:
|
|
case T_FunctionScan:
|
|
case T_ValuesScan:
|
|
case T_CteScan:
|
|
case T_WorkTableScan:
|
|
ExplainScanTarget((Scan *) plan, es);
|
|
break;
|
|
case T_BitmapIndexScan:
|
|
appendStringInfo(es->str, " on %s",
|
|
explain_get_index_name(((BitmapIndexScan *) plan)->indexid));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (es->costs)
|
|
appendStringInfo(es->str, " (cost=%.2f..%.2f rows=%.0f width=%d)",
|
|
plan->startup_cost, plan->total_cost,
|
|
plan->plan_rows, plan->plan_width);
|
|
|
|
/*
|
|
* We have to forcibly clean up the instrumentation state because we
|
|
* haven't done ExecutorEnd yet. This is pretty grotty ...
|
|
*/
|
|
if (planstate->instrument)
|
|
InstrEndLoop(planstate->instrument);
|
|
|
|
if (planstate->instrument && planstate->instrument->nloops > 0)
|
|
{
|
|
double nloops = planstate->instrument->nloops;
|
|
|
|
appendStringInfo(es->str,
|
|
" (actual time=%.3f..%.3f rows=%.0f loops=%.0f)",
|
|
1000.0 * planstate->instrument->startup / nloops,
|
|
1000.0 * planstate->instrument->total / nloops,
|
|
planstate->instrument->ntuples / nloops,
|
|
planstate->instrument->nloops);
|
|
}
|
|
else if (es->analyze)
|
|
appendStringInfoString(es->str, " (never executed)");
|
|
appendStringInfoChar(es->str, '\n');
|
|
|
|
/* target list */
|
|
if (es->verbose)
|
|
show_plan_tlist(plan, indent, es);
|
|
|
|
/* quals, sort keys, etc */
|
|
switch (nodeTag(plan))
|
|
{
|
|
case T_IndexScan:
|
|
show_scan_qual(((IndexScan *) plan)->indexqualorig,
|
|
"Index Cond", plan, outer_plan, indent, es);
|
|
show_scan_qual(plan->qual,
|
|
"Filter", plan, outer_plan, indent, es);
|
|
break;
|
|
case T_BitmapIndexScan:
|
|
show_scan_qual(((BitmapIndexScan *) plan)->indexqualorig,
|
|
"Index Cond", plan, outer_plan, indent, es);
|
|
break;
|
|
case T_BitmapHeapScan:
|
|
show_scan_qual(((BitmapHeapScan *) plan)->bitmapqualorig,
|
|
"Recheck Cond", plan, outer_plan, indent, es);
|
|
/* FALL THRU */
|
|
case T_SeqScan:
|
|
case T_FunctionScan:
|
|
case T_ValuesScan:
|
|
case T_CteScan:
|
|
case T_WorkTableScan:
|
|
case T_SubqueryScan:
|
|
show_scan_qual(plan->qual,
|
|
"Filter", plan, outer_plan, indent, es);
|
|
break;
|
|
case T_TidScan:
|
|
{
|
|
/*
|
|
* The tidquals list has OR semantics, so be sure to show it
|
|
* as an OR condition.
|
|
*/
|
|
List *tidquals = ((TidScan *) plan)->tidquals;
|
|
|
|
if (list_length(tidquals) > 1)
|
|
tidquals = list_make1(make_orclause(tidquals));
|
|
show_scan_qual(tidquals,
|
|
"TID Cond", plan, outer_plan, indent, es);
|
|
show_scan_qual(plan->qual,
|
|
"Filter", plan, outer_plan, indent, es);
|
|
}
|
|
break;
|
|
case T_NestLoop:
|
|
show_upper_qual(((NestLoop *) plan)->join.joinqual,
|
|
"Join Filter", plan, indent, es);
|
|
show_upper_qual(plan->qual, "Filter", plan, indent, es);
|
|
break;
|
|
case T_MergeJoin:
|
|
show_upper_qual(((MergeJoin *) plan)->mergeclauses,
|
|
"Merge Cond", plan, indent, es);
|
|
show_upper_qual(((MergeJoin *) plan)->join.joinqual,
|
|
"Join Filter", plan, indent, es);
|
|
show_upper_qual(plan->qual, "Filter", plan, indent, es);
|
|
break;
|
|
case T_HashJoin:
|
|
show_upper_qual(((HashJoin *) plan)->hashclauses,
|
|
"Hash Cond", plan, indent, es);
|
|
show_upper_qual(((HashJoin *) plan)->join.joinqual,
|
|
"Join Filter", plan, indent, es);
|
|
show_upper_qual(plan->qual, "Filter", plan, indent, es);
|
|
break;
|
|
case T_Agg:
|
|
case T_Group:
|
|
show_upper_qual(plan->qual, "Filter", plan, indent, es);
|
|
break;
|
|
case T_Sort:
|
|
show_sort_keys(plan, indent, es);
|
|
show_sort_info((SortState *) planstate, indent, es);
|
|
break;
|
|
case T_Result:
|
|
show_upper_qual((List *) ((Result *) plan)->resconstantqual,
|
|
"One-Time Filter", plan, indent, es);
|
|
show_upper_qual(plan->qual, "Filter", plan, indent, es);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/* initPlan-s */
|
|
if (plan->initPlan)
|
|
ExplainSubPlans(planstate->initPlan, indent, es);
|
|
|
|
/* lefttree */
|
|
if (outerPlan(plan))
|
|
{
|
|
/*
|
|
* Ordinarily we don't pass down our own outer_plan value to our child
|
|
* nodes, but in bitmap scan trees we must, since the bottom
|
|
* BitmapIndexScan nodes may have outer references.
|
|
*/
|
|
ExplainNode(outerPlan(plan), outerPlanState(planstate),
|
|
IsA(plan, BitmapHeapScan) ? outer_plan : NULL,
|
|
indent + 3, es);
|
|
}
|
|
|
|
/* righttree */
|
|
if (innerPlan(plan))
|
|
{
|
|
ExplainNode(innerPlan(plan), innerPlanState(planstate),
|
|
outerPlan(plan), indent + 3, es);
|
|
}
|
|
|
|
/* special child plans */
|
|
switch (nodeTag(plan))
|
|
{
|
|
case T_Append:
|
|
ExplainMemberNodes(((Append *) plan)->appendplans,
|
|
((AppendState *) planstate)->appendplans,
|
|
outer_plan, indent, es);
|
|
break;
|
|
case T_BitmapAnd:
|
|
ExplainMemberNodes(((BitmapAnd *) plan)->bitmapplans,
|
|
((BitmapAndState *) planstate)->bitmapplans,
|
|
outer_plan, indent, es);
|
|
break;
|
|
case T_BitmapOr:
|
|
ExplainMemberNodes(((BitmapOr *) plan)->bitmapplans,
|
|
((BitmapOrState *) planstate)->bitmapplans,
|
|
outer_plan, indent, es);
|
|
break;
|
|
case T_SubqueryScan:
|
|
{
|
|
SubqueryScan *subqueryscan = (SubqueryScan *) plan;
|
|
SubqueryScanState *subquerystate = (SubqueryScanState *) planstate;
|
|
|
|
ExplainNode(subqueryscan->subplan, subquerystate->subplan,
|
|
NULL, indent + 3, es);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/* subPlan-s */
|
|
if (planstate->subPlan)
|
|
ExplainSubPlans(planstate->subPlan, indent, es);
|
|
}
|
|
|
|
/*
|
|
* Show the targetlist of a plan node
|
|
*/
|
|
static void
|
|
show_plan_tlist(Plan *plan, int indent, ExplainState *es)
|
|
{
|
|
List *context;
|
|
bool useprefix;
|
|
ListCell *lc;
|
|
int i;
|
|
|
|
/* No work if empty tlist (this occurs eg in bitmap indexscans) */
|
|
if (plan->targetlist == NIL)
|
|
return;
|
|
/* The tlist of an Append isn't real helpful, so suppress it */
|
|
if (IsA(plan, Append))
|
|
return;
|
|
/* Likewise for RecursiveUnion */
|
|
if (IsA(plan, RecursiveUnion))
|
|
return;
|
|
|
|
/* Set up deparsing context */
|
|
context = deparse_context_for_plan((Node *) plan,
|
|
NULL,
|
|
es->rtable,
|
|
es->pstmt->subplans);
|
|
useprefix = list_length(es->rtable) > 1;
|
|
|
|
/* Emit line prefix */
|
|
appendStringInfoSpaces(es->str, indent * 2);
|
|
appendStringInfoString(es->str, " Output: ");
|
|
|
|
/* Deparse each non-junk result column */
|
|
i = 0;
|
|
foreach(lc, plan->targetlist)
|
|
{
|
|
TargetEntry *tle = (TargetEntry *) lfirst(lc);
|
|
|
|
if (tle->resjunk)
|
|
continue;
|
|
if (i++ > 0)
|
|
appendStringInfoString(es->str, ", ");
|
|
appendStringInfoString(es->str,
|
|
deparse_expression((Node *) tle->expr, context,
|
|
useprefix, false));
|
|
}
|
|
|
|
appendStringInfoChar(es->str, '\n');
|
|
}
|
|
|
|
/*
|
|
* Show a qualifier expression
|
|
*
|
|
* Note: outer_plan is the referent for any OUTER vars in the scan qual;
|
|
* this would be the outer side of a nestloop plan. Pass NULL if none.
|
|
*/
|
|
static void
|
|
show_qual(List *qual, const char *qlabel, Plan *plan, Plan *outer_plan,
|
|
int indent, bool useprefix, ExplainState *es)
|
|
{
|
|
List *context;
|
|
Node *node;
|
|
char *exprstr;
|
|
|
|
/* No work if empty qual */
|
|
if (qual == NIL)
|
|
return;
|
|
|
|
/* Convert AND list to explicit AND */
|
|
node = (Node *) make_ands_explicit(qual);
|
|
|
|
/* Set up deparsing context */
|
|
context = deparse_context_for_plan((Node *) plan,
|
|
(Node *) outer_plan,
|
|
es->rtable,
|
|
es->pstmt->subplans);
|
|
|
|
/* Deparse the expression */
|
|
exprstr = deparse_expression(node, context, useprefix, false);
|
|
|
|
/* And add to es->str */
|
|
appendStringInfoSpaces(es->str, indent * 2);
|
|
appendStringInfo(es->str, " %s: %s\n", qlabel, exprstr);
|
|
}
|
|
|
|
/*
|
|
* Show a qualifier expression for a scan plan node
|
|
*/
|
|
static void
|
|
show_scan_qual(List *qual, const char *qlabel,
|
|
Plan *scan_plan, Plan *outer_plan,
|
|
int indent, ExplainState *es)
|
|
{
|
|
bool useprefix;
|
|
|
|
useprefix = (outer_plan != NULL || IsA(scan_plan, SubqueryScan));
|
|
show_qual(qual, qlabel, scan_plan, outer_plan, indent, useprefix, es);
|
|
}
|
|
|
|
/*
|
|
* Show a qualifier expression for an upper-level plan node
|
|
*/
|
|
static void
|
|
show_upper_qual(List *qual, const char *qlabel, Plan *plan,
|
|
int indent, ExplainState *es)
|
|
{
|
|
bool useprefix;
|
|
|
|
useprefix = (list_length(es->rtable) > 1);
|
|
show_qual(qual, qlabel, plan, NULL, indent, useprefix, es);
|
|
}
|
|
|
|
/*
|
|
* Show the sort keys for a Sort node.
|
|
*/
|
|
static void
|
|
show_sort_keys(Plan *sortplan, int indent, ExplainState *es)
|
|
{
|
|
int nkeys = ((Sort *) sortplan)->numCols;
|
|
AttrNumber *keycols = ((Sort *) sortplan)->sortColIdx;
|
|
List *context;
|
|
bool useprefix;
|
|
int keyno;
|
|
char *exprstr;
|
|
|
|
if (nkeys <= 0)
|
|
return;
|
|
|
|
appendStringInfoSpaces(es->str, indent * 2);
|
|
appendStringInfoString(es->str, " Sort Key: ");
|
|
|
|
/* Set up deparsing context */
|
|
context = deparse_context_for_plan((Node *) sortplan,
|
|
NULL,
|
|
es->rtable,
|
|
es->pstmt->subplans);
|
|
useprefix = list_length(es->rtable) > 1;
|
|
|
|
for (keyno = 0; keyno < nkeys; keyno++)
|
|
{
|
|
/* find key expression in tlist */
|
|
AttrNumber keyresno = keycols[keyno];
|
|
TargetEntry *target = get_tle_by_resno(sortplan->targetlist, keyresno);
|
|
|
|
if (!target)
|
|
elog(ERROR, "no tlist entry for key %d", keyresno);
|
|
/* Deparse the expression, showing any top-level cast */
|
|
exprstr = deparse_expression((Node *) target->expr, context,
|
|
useprefix, true);
|
|
/* And add to es->str */
|
|
if (keyno > 0)
|
|
appendStringInfoString(es->str, ", ");
|
|
appendStringInfoString(es->str, exprstr);
|
|
}
|
|
|
|
appendStringInfoChar(es->str, '\n');
|
|
}
|
|
|
|
/*
|
|
* If it's EXPLAIN ANALYZE, show tuplesort explain info for a sort node
|
|
*/
|
|
static void
|
|
show_sort_info(SortState *sortstate, int indent, ExplainState *es)
|
|
{
|
|
Assert(IsA(sortstate, SortState));
|
|
if (es->analyze && sortstate->sort_Done &&
|
|
sortstate->tuplesortstate != NULL)
|
|
{
|
|
char *sortinfo;
|
|
|
|
sortinfo = tuplesort_explain((Tuplesortstate *) sortstate->tuplesortstate);
|
|
appendStringInfoSpaces(es->str, indent * 2);
|
|
appendStringInfo(es->str, " %s\n", sortinfo);
|
|
pfree(sortinfo);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Fetch the name of an index in an EXPLAIN
|
|
*
|
|
* We allow plugins to get control here so that plans involving hypothetical
|
|
* indexes can be explained.
|
|
*/
|
|
static const char *
|
|
explain_get_index_name(Oid indexId)
|
|
{
|
|
const char *result;
|
|
|
|
if (explain_get_index_name_hook)
|
|
result = (*explain_get_index_name_hook) (indexId);
|
|
else
|
|
result = NULL;
|
|
if (result == NULL)
|
|
{
|
|
/* default behavior: look in the catalogs and quote it */
|
|
result = get_rel_name(indexId);
|
|
if (result == NULL)
|
|
elog(ERROR, "cache lookup failed for index %u", indexId);
|
|
result = quote_identifier(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Show the target of a Scan node
|
|
*/
|
|
static void
|
|
ExplainScanTarget(Scan *plan, ExplainState *es)
|
|
{
|
|
char *objectname = NULL;
|
|
RangeTblEntry *rte;
|
|
|
|
if (plan->scanrelid <= 0) /* Is this still possible? */
|
|
return;
|
|
rte = rt_fetch(plan->scanrelid, es->rtable);
|
|
|
|
switch (nodeTag(plan))
|
|
{
|
|
case T_SeqScan:
|
|
case T_IndexScan:
|
|
case T_BitmapHeapScan:
|
|
case T_TidScan:
|
|
/* Assert it's on a real relation */
|
|
Assert(rte->rtekind == RTE_RELATION);
|
|
objectname = get_rel_name(rte->relid);
|
|
break;
|
|
case T_FunctionScan:
|
|
{
|
|
Node *funcexpr;
|
|
|
|
/* Assert it's on a RangeFunction */
|
|
Assert(rte->rtekind == RTE_FUNCTION);
|
|
|
|
/*
|
|
* If the expression is still a function call, we can get the
|
|
* real name of the function. Otherwise, punt (this can
|
|
* happen if the optimizer simplified away the function call,
|
|
* for example).
|
|
*/
|
|
funcexpr = ((FunctionScan *) plan)->funcexpr;
|
|
if (funcexpr && IsA(funcexpr, FuncExpr))
|
|
{
|
|
Oid funcid = ((FuncExpr *) funcexpr)->funcid;
|
|
|
|
objectname = get_func_name(funcid);
|
|
}
|
|
}
|
|
break;
|
|
case T_ValuesScan:
|
|
Assert(rte->rtekind == RTE_VALUES);
|
|
break;
|
|
case T_CteScan:
|
|
/* Assert it's on a non-self-reference CTE */
|
|
Assert(rte->rtekind == RTE_CTE);
|
|
Assert(!rte->self_reference);
|
|
objectname = rte->ctename;
|
|
break;
|
|
case T_WorkTableScan:
|
|
/* Assert it's on a self-reference CTE */
|
|
Assert(rte->rtekind == RTE_CTE);
|
|
Assert(rte->self_reference);
|
|
objectname = rte->ctename;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
appendStringInfoString(es->str, " on");
|
|
if (objectname != NULL)
|
|
appendStringInfo(es->str, " %s", quote_identifier(objectname));
|
|
if (objectname == NULL || strcmp(rte->eref->aliasname, objectname) != 0)
|
|
appendStringInfo(es->str, " %s",
|
|
quote_identifier(rte->eref->aliasname));
|
|
}
|
|
|
|
/*
|
|
* Explain the constituent plans of an Append, BitmapAnd, or BitmapOr node.
|
|
*
|
|
* Ordinarily we don't pass down outer_plan to our child nodes, but in these
|
|
* cases we must, since the node could be an "inner indexscan" in which case
|
|
* outer references can appear in the child nodes.
|
|
*/
|
|
static void
|
|
ExplainMemberNodes(List *plans, PlanState **planstate, Plan *outer_plan,
|
|
int indent, ExplainState *es)
|
|
{
|
|
ListCell *lst;
|
|
int j = 0;
|
|
|
|
foreach(lst, plans)
|
|
{
|
|
Plan *subnode = (Plan *) lfirst(lst);
|
|
|
|
ExplainNode(subnode, planstate[j],
|
|
outer_plan, indent + 3, es);
|
|
j++;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Explain a list of SubPlans (or initPlans, which also use SubPlan nodes).
|
|
*/
|
|
static void
|
|
ExplainSubPlans(List *plans, int indent, ExplainState *es)
|
|
{
|
|
ListCell *lst;
|
|
|
|
foreach(lst, plans)
|
|
{
|
|
SubPlanState *sps = (SubPlanState *) lfirst(lst);
|
|
SubPlan *sp = (SubPlan *) sps->xprstate.expr;
|
|
|
|
appendStringInfoSpaces(es->str, indent * 2);
|
|
appendStringInfo(es->str, " %s\n", sp->plan_name);
|
|
ExplainNode(exec_subplan_get_plan(es->pstmt, sp),
|
|
sps->planstate, NULL, indent + 4, es);
|
|
}
|
|
}
|