1. 查询执行策略

在PostgreSQL中,用户输入的SQL语句被分为两种类型,并被两种不同的执行部件处理:

(1)可优化语句(Optimizable statement)————由执行器(Executor)执行器去执行。可优化语句主要是DML语句(SELECT、INSERT、UPDATE、DELETE等),这里语句的特点是均需要查询相关满足条件的元组,然后将这些元组返回给用户,或者在这些元组上进行某些操作后写回磁盘。因此在经过查询编译器处理后,会为其生成一个或多个执行计划树(Plan Tree),用于查询满足相关条件的元组并作相应处理。由于在执行计划树的生成过程中会根据查询优化理论进行重写和优化,以加快查询速度,因此,这类语句被称为可优化语句。可优化语句包含一个或多个经过重写和优化过的查询计划树,执行器会严格根据计划树进行处理。执行器函数名为ProcessQuery,各种实现在src/backend/executor目录中。可优化语句经查询编译器会被转换称为执行计划树(PlannedStmt)

(2)非可优化语句————由功能处理器(Utility Processor)处理。该类语句主要包括DDL语句,这类语句包含查询数据元组之外的各种操作,语句之间功能相对独立,所以也被称为功能性语句。功能处理器函数名为ProcessUtility,各种实现在src/backend/commands目录中。非可优化语句没有执行计划树(Statement)

从查询编译器输出执行计划,到执行计划被具体的执行部件处理这一过程,被称作执行策略的选择过程,负责完成执行策略选择的模块称为执行策略选择器。该部分完成了对于查询编译器输出数据的解析,选择预先设定好的执行流程。下图是查询语句的处理流程:

query_process_line

2. 四种执行策略

PostgreSQL实现了四种执行策略:
(1)PORTAL_ONE_SELECT:如果用户提交的SQL语句中仅包含一个SELECT类型查询,则查询执行器会使用执行器来处理该SQL语句。换句话说,这种策略用于处理仅有一个可优化原子操作的情况。
(2)PORTAL_ONE_RETURNING(PORTAL_ONE_MOD_WITH):如果用户提交的SQL语句中包含一个带有RETURNING字句的INSERT/UPDATE/DELETE语句,查询执行器会选择这种策略。因为处理该类语句应该先完成所有操作(对元组的修改操作),然后返回结果(例如操作是否成功、被修改的元组数量等),以减少出错的风险。查询执行器在执行时,将首先处理所有满足条件的元组,并将执行过程的结果缓存,然后将结果返回。
(3)PORTAL_UTIL_SELECT:如果用户提交的SQL语句是一个功能类型语句,但是其返回结果类似SELECT语句(例如EXPLAIN和SHOW),查询执行器将选择这种策略。以此种策略执行时,同样首先执行语句并获取完整结果,并将结果缓存起来,然后将结果返回给用户。
(4)PORTAL_MULTI_QUERY:用于处理除以上三种情况之外的操作。从其名称中的“MULTI”就能够看出,这个策略更具有一般性,能够处理一个或多个原子操作,并根据操作的类型选择合适的处理部件。
简单来说,PORTAL_ONE_SELECT是用来处理SELECT语句的,该策略会调用执行器来执行;PORTAL_ONE_RETURNING(PORTAL_ONE_MOD_WITH)面向UPDATE/INSERT/DELETE等需要进行元组操作且需要缓存结果的语句,该策略也是调用执行器执行;PORTAL_UTIL_SELECT面向单一DDL语句,该策略调用功能处理器来执行;PORTAL_MULTI_QUERY是前面三种策略的混合类型,它可以处理多个原子操作。

下面是代码中的描述:

/*
 * We have several execution strategies for Portals, depending on what
 * query or queries are to be executed.  (Note: in all cases, a Portal
 * executes just a single source-SQL query, and thus produces just a
 * single result from the user's viewpoint.  However, the rule rewriter
 * may expand the single source query to zero or many actual queries.)
 *
 * PORTAL_ONE_SELECT: the portal contains one single SELECT query.  We run
 * the Executor incrementally as results are demanded.  This strategy also
 * supports holdable cursors (the Executor results can be dumped into a
 * tuplestore for access after transaction completion).
 *
 * PORTAL_ONE_RETURNING: the portal contains a single INSERT/UPDATE/DELETE
 * query with a RETURNING clause (plus possibly auxiliary queries added by
 * rule rewriting).  On first execution, we run the portal to completion
 * and dump the primary query's results into the portal tuplestore; the
 * results are then returned to the client as demanded.  (We can't support
 * suspension of the query partway through, because the AFTER TRIGGER code
 * can't cope, and also because we don't want to risk failing to execute
 * all the auxiliary queries.)
 *
 * PORTAL_ONE_MOD_WITH: the portal contains one single SELECT query, but
 * it has data-modifying CTEs.  This is currently treated the same as the
 * PORTAL_ONE_RETURNING case because of the possibility of needing to fire
 * triggers.  It may act more like PORTAL_ONE_SELECT in future.
 *
 * PORTAL_UTIL_SELECT: the portal contains a utility statement that returns
 * a SELECT-like result (for example, EXPLAIN or SHOW).  On first execution,
 * we run the statement and dump its results into the portal tuplestore;
 * the results are then returned to the client as demanded.
 *
 * PORTAL_MULTI_QUERY: all other cases.  Here, we do not support partial
 * execution: the portal's queries will be run to completion on first call.
 */
typedef enum PortalStrategy
{
	PORTAL_ONE_SELECT,
	PORTAL_ONE_RETURNING,
	PORTAL_ONE_MOD_WITH,
	PORTAL_UTIL_SELECT,
	PORTAL_MULTI_QUERY
} PortalStrategy;

3. 策略选择的实现

执行策略选择器的工作是根据查询编译器给出的查询计划树链表来为当前查询选择四种执行策略中的一种。在这个过程中,执行策略选择器会使用数据结构PortalData来存储查询计划树链表以及最后选中的执行策略等信息,我们通常也把这个数据结构称为“Portal”。

typedef struct PortalData *Portal;

typedef struct PortalData
{
	/* Bookkeeping data */
	const char *name;			/* portal's name */
	const char *prepStmtName;	/* source prepared statement (NULL if none) */
	MemoryContext heap;			/* subsidiary memory for portal */
	ResourceOwner resowner;		/* resources owned by portal */
	void		(*cleanup) (Portal portal);		/* cleanup hook */
	SubTransactionId createSubid;		/* the ID of the creating subxact */

	/*
	 * if createSubid is InvalidSubTransactionId, the portal is held over from
	 * a previous transaction
	 */

	/* The query or queries the portal will execute */
	const char *sourceText;		/* text of query (as of 8.4, never NULL) */
	const char *commandTag;		/* command tag for original query */
	List	   *stmts;			/* PlannedStmts and/or utility statements */
	CachedPlan *cplan;			/* CachedPlan, if stmts are from one */

	ParamListInfo portalParams; /* params to pass to query */

	/* Features/options */
	PortalStrategy strategy;	/* see above */
	int			cursorOptions;	/* DECLARE CURSOR option bits */

	/* Status data */
	PortalStatus status;		/* see above */
	bool		portalPinned;	/* a pinned portal can't be dropped */

	/* If not NULL, Executor is active; call ExecutorEnd eventually: */
	QueryDesc  *queryDesc;		/* info needed for executor invocation */

	/* If portal returns tuples, this is their tupdesc: */
	TupleDesc	tupDesc;		/* descriptor for result tuples */
	/* and these are the format codes to use for the columns: */
	int16	   *formats;		/* a format code for each column */

	/*
	 * Where we store tuples for a held cursor or a PORTAL_ONE_RETURNING or
	 * PORTAL_UTIL_SELECT query.  (A cursor held past the end of its
	 * transaction no longer has any active executor state.)
	 */
	Tuplestorestate *holdStore; /* store for holdable cursors */
	MemoryContext holdContext;	/* memory containing holdStore */

	/*
	 * atStart, atEnd and portalPos indicate the current cursor position.
	 * portalPos is zero before the first row, N after fetching N'th row of
	 * query.  After we run off the end, portalPos = # of rows in query, and
	 * atEnd is true.  If portalPos overflows, set posOverflow (this causes us
	 * to stop relying on its value for navigation).  Note that atStart
	 * implies portalPos == 0, but not the reverse (portalPos could have
	 * overflowed).
	 */
	bool		atStart;
	bool		atEnd;
	bool		posOverflow;
	long		portalPos;

	/* Presentation data, primarily used by the pg_cursors system view */
	TimestampTz creation_time;	/* time at which this portal was defined */
	bool		visible;		/* include this portal in pg_cursors? */
}	PortalData;

查询执行器执行一个SQL语句时都会一个Portal作为输入数据,Portal中存放了与执行该SQL语句相关的所有信息(包括查询树、计划树、执行状态等),Portal及其主要字段如图所示。其中,stmts字段是由查询编译器输出的原子操作链表,图中仅列出了两种可能的原子操作PlannedStmtQuery,两者都能包含查询计划树,用于保存含有查询的操作。当然,有些含有查询计划树的原子操作不一定是SELECT语句,例如游标的声明(utilityStmt字段不为空),以及SELECT INTO类型的语句(intoClause字段不为空)。对于UPDATE、INSERT、DELETE类型,含有RETURNING字句时returningList字段不为空。

portal_strcutrues

PostgreSQL主要根据原子操作的命令类型以及stmts中原子操作的个数来为Portal选择合适的执行策略。由查询编译器输出的每一个查询计划树中都包含有一个类型为CmdType的字段,用于标识该原子操作对应的命令类型。命令类型分为六类,使用枚举类型定为:

/*
 * CmdType -
 *	  enums for type of operation represented by a Query or PlannedStmt
 *
 * This is needed in both parsenodes.h and plannodes.h, so put it here...
 */
typedef enum CmdType
{
	CMD_UNKNOWN,
	CMD_SELECT,					/* select stmt */
	CMD_UPDATE,					/* update stmt */
	CMD_INSERT,					/* insert stmt */
	CMD_DELETE,
	CMD_UTILITY,				/* cmds like create, destroy, copy, vacuum,
								 * etc. */
	CMD_NOTHING					/* dummy command for instead nothing rules
								 * with qual */
} CmdType;

选择PORTAL_ONE_SELECT策略应满足以下条件:

  • stmts链表中只有一个PlannedStmt类型或是Query类型的节点。
  • 节点是CMD_SELECT类型的操作。
  • 节点的utilityStmt字段和intoClause字段为空。

选择PORTAL_UTIL_SELECT策略应满足以下条件:

  • stmts链表仅有一个Query类型的节点。
  • 节点是CMD_UTILITY类型操作。
  • 节点的utilityStmt字段保存的是FETCH语句(类型为T_FetchStmt)、EXECUTE语句(类型为T_ExecuteStmt)、EXPLAIN语句(类型为T_ExplainStmt)或是SHOW语句(类型为T_VariableShowStmt)之一。

而PORTAL_ONE_RETURNING策略适用于stmts链表中只有一个包含RETURNING字句(returningList不为空)的原子操作。其他的各种情况都将以PORTAL_MULTI_QUERY
策略进行处理。执行策略选择器的主函数名为ChoosePortalStrategy,其输入为PortalData的stmts链表,输出是预先定义的执行策略枚举类型。该函数的执行流程如图:

choosestrategy

4. Portal的执行

Portal是查询执行器执行一个SQL语句的“门户”,所有SQL语句的执行都从一个选择好执行策略的Portal开始。所有Portal的执行过程都必须依次调用PortalStart(初始化)、PortalRun(执行)、PortalDrop(清理)三个过程,PostgreSQL为Portal提供的几种执行策略实现了单独的执行流程,每种策略的Portal在执行时会经过不同的处理过程。Portal的创建、初始化、执行及清理过程都在exec_simple_query函数中进行,其过程如下:
(1)调用函数CreatePortal创建一个干净的Portal,其中内存上下文、资源跟踪器、清理函数等都已经设置好,但sourceText、stmts等字段并没有设置。
(2)调用函数PortalDefineQuery为刚创建的Poral设置sourceText、stmts等字段,这些字段的值都来自于查询编译器输出的结果,其中还会将Portal的状态设置为PORTAL_DEFINED表示Portal已被定义。
(3)调用函数PortalStart对定义好的Portal进行初始化,初始化工作主要如下:

  • 调用ChoosePortalStrategy为Portal选择策略。
  • 如果选择的是PORTAL_ONE_SELECT策略,调用CreateQueryDesc为Portal创建查询描述符。
  • 如果选择的是PORTAL_ONE_RETURNING或PORTAL_ONE_MOD_WITH或PORTAL_UTIL_SELECT策略,为Portal创建返回元组的描述符。
  • 将Portal的状态设置为PORTAL_READY,表示Portal已经初始化好,准备开始执行。

(4)调用函数PortalRun执行Portal,该函数将按照Portal中选择的策略调用相应的执行部件来执行Portal。
(5)调用函数PortalDrop清理Portal,主要是对Portal运行中所占用的资源进行释放,特别是用于缓存结果的资源。

下图显示了四种执行策略在各自的处理过程中的函数调用关系,该图从总体上展示了各种策略的执行步骤以及对应执行部件的入口:

portal_execute

对于PORTAL_ONE_SELECT策略的Portal,其中包含一个简单SELECT类型的查询计划树,在PortalStart中将调用ExecutorStart进行Executor(执行器)初始化,然后在PortalRun中调用ExecutorRun开始执行器的执行过程。
PORTAL_ONE_RETURNING和PORTAL_UTIL_SELECT策略需要在执行后将结果缓存,然后将缓存的结果按要求进行返回。因此,在PortalStart中仅会初始化返回元组的结构描述信息。接着PortalRun会调用FillPortalStore执行查询计划得到所有的记过元组并填充到缓存中,然后调用RunFromStore从缓存中获取元组并返回。从上图可以看到,FillPortalStore中对于查询计划的执行会根据策略不同而调用不同的处理部件,PORTAL_ONE_RETURNING策略会使用PortalRunMulti进行处理,而PORTAL_UTIL_SELECT使用PortalRunUtility处理。PORTAL_MULTI_QUERY策略在执行过程中,PortalRun会使用PortalRunMulti进行处理。

本文总结自《PostgreSQL数据库内核分析》。