postgres

在postgres backend接收客户端发送的simple query消息,sql语句就在这个消息当中,执行exec_simple_query函数来处理这个消息,即执行sql查询。

执行流程

先大致梳理一下代码流程

static void  
exec_simple_query(const char *query_string)  
{
	// 1.启动一个事务命令  
	start_xact_command();
	// 删除未命名的prepared statement,避免资源浪费和可能导致的命名冲突问题
	drop_unnamed_stmt();
	// 2. 解析sql语句生成解析树
	oldcontext = MemoryContextSwitchTo(MessageContext);
	parsetree_list = pg_parse_query(query_string);
	MemoryContextSwitchTo(oldcontext);
	// 处于历史原因,在一个消息当中的多条语句会在一个事务块中执行,根据解析树的个数来判断是否需要使用隐式事务块
	use_implicit_block = (list_length(parsetree_list) > 1);
	
	// 3. 执行sql,遍历解析树并执行
	foreach(parsetree_item, parsetree_list)
	{/*...*/}
 
	// 4. 结束事务
	finish_xact_command();
}
  1. 启动事务
  2. 解析sql生成解析树
  3. 遍历执行每一个解析树
  4. 结束事务

接下来并主要从这节点来进行讲解。

启动事务

sql生成的所有查询是在一个命令块(事务)当中的,除非使用了BEGIN/COMMIT/ABORT这样的命令来强制启动一个新事务。

static void  
start_xact_command(void)  
{  
    // 没有启动事务则启动一个事务  
    if (!xact_started)  
    {  
       StartTransactionCommand();  
  
       xact_started = true;  
    }  
	// 有必要则启动statement超时。如果超时已经启动了,那么就不会重新启动重置超时时间。
	// 避免了finish_xact_command结束之前,重复调用start_xact_command的开销。
	// 如果期望能够重置的话,那么需要显示禁用超时
    enable_statement_timeout();  
  
    // 启动超时,检查客户端是否离开。
    if (client_connection_check_interval > 0 &&  
       IsUnderPostmaster &&  
       MyProcPort &&  
       !get_timeout_active(CLIENT_CONNECTION_CHECK_TIMEOUT))  
       enable_timeout_after(CLIENT_CONNECTION_CHECK_TIMEOUT,  
                       client_connection_check_interval);  
}

有关事务具体如何启动,以及涉及的超时机制在专门的章节事务设计原理超时机制 进行描述,这里只看整体的逻辑思路。

未命名prepared statement的清理

这里插入一小节。 我们知道对于会反复执行的sql语句,我们可以将其预处理为prepared statement,使用占位符表示参数,然后后续调用的时候只需要指定使用的prepared statement的名称和具体的参数值就可以使用了。省略了sql的网络传输以及backend语法解析的过程,可以提高执行性能。

不过这里介绍的都是具名的prepared statement,它们是可以跨连接调用的。而这里介绍的unamed prepared statement就是指名称为空的prepared statement,因为没有名字无法标识,所以它是事务级别的为了避免资源浪费需要在每次事务启动的时候清理一下

这不是重点,在这里提一下有个认识即可。

解析SQL语法树

在这里的话就是精简化的三行代码

oldcontext = MemoryContextSwitchTo(MessageContext);
parsetree_list = pg_parse_query(query_string);
MemoryContextSwitchTo(oldcontext);

在这个解析的过程中涉及上下文的切换,这和postgres的内存上下文系统的设计相关,详细的内容补充到MemoryContext系统,这里只需要知道是用来管理对应的内存即可。

然后pg_parse_query这个方法用来解析语法树,进入其实现扣除不关心的部分

List *  
pg_parse_query(const char *query_string)  
{  
    List      *raw_parsetree_list;  
  
    if (log_parser_stats)  
       ResetUsage();  
  
    raw_parsetree_list = raw_parser(query_string, RAW_PARSE_DEFAULT);  
  
    if (log_parser_stats)  
       ShowUsage("PARSER STATISTICS");  
 
    return raw_parsetree_list;  
}

关键点在于调用了raw_parser函数,这是postgres语法器的入口,这里只是包装了一下,可以根据开关选择是否打印语法解析耗费的资源。

postgres语法解析器是使用flex&bison生成词法和语法解析器来做sql解析的,这也是一个专题,会补充在SQL Parser:使用flex&bison完成SQL解析器

这里我们需要了解的就是解析后的结果是一个树形结构,在postgres统一用Node表示,List则是postgres中的一个线性结构列表。

执行SQL

这里的逻辑是最核心最重要的,不过也比较重。在这里慢慢梳理一下逻辑。 sql的执行是遍历解析出来的parsetree来执行的,我们需要了解的就是循环当中的逻辑。

// foreach宏是遍历List结构的一种写法,实现了其它语言的foreach语法
foreach(parsetree_item, parsetree_list)
{
	// 首先将parsetree_item类型转为RawStmt
	RawStmt    *parsetree = lfirst_node(RawStmt, parsetree_item);
	// 获取语句的类型,名称等信息,并设置进程标题
	commandTag = CreateCommandTag(parsetree->stmt);  
	cmdtagname = GetCommandTagNameAndLen(commandTag, &cmdtaglen);  
	set_ps_display_with_len(cmdtagname, cmdtaglen);
	
	// 开始命令,做一些初始化工作,但是目前实现为空,也就是无实际意义。
	BeginCommand(commandTag, dest);
 
	// 检查事务状态
	// 如果处于中止的事务当中,并且当前语句又非事务退出语句,那么就会记录错误  
	if (IsAbortedTransactionBlockState() &&  
	    !IsTransactionExitStmt(parsetree->stmt))  
	    ereport(ERROR,  
	          (errcode(ERRCODE_IN_FAILED_SQL_TRANSACTION),  
	           errmsg("current transaction is aborted, "  
	                "commands ignored until end of transaction block"),  
	           errdetail_abort()));
	// 确保处于一个事务当中。在执行的最开始已经启动过事务了,但是执行的sql语句当中可能有例如commit, rollback这样的事务控制语句,会结束一个事务。所以这里进行了检查
	start_xact_command();
 
	// 多条语句需要进入隐式事务来强制和后面的语句组合在一起,否则存在COMMIT/ROLLBACK语句的话就会导致在这之后的语句不在一个分组当中  
	if (use_implicit_block)  
	    BeginImplicitTransactionBlock();
	
	// 检查取消信号判断退出
	CHECK_FOR_INTERRUPTS();
 
	// 为后续的处理流程analysis和planning按需准备快照
	if (analyze_requires_snapshot(parsetree))  
	{  
	    PushActiveSnapshot(GetTransactionSnapshot());  
	    snapshot_set = true;  
	}
 
	// 切换合适上下文用来后续analysis, rewrite, plan步骤树的构建
	// 多条语句的话需要每条语句使用per-parsetree上下文,最后一条和只有一条的情况下直接使用MessageContext即可。
	if (lnext(parsetree_list, parsetree_item) != NULL)  
	{  
	    per_parsetree_context =  
	       AllocSetContextCreate(MessageContext,  
	                        "per-parsetree message context",  
	                        ALLOCSET_DEFAULT_SIZES);  
	    oldcontext = MemoryContextSwitchTo(per_parsetree_context);  
	}  
	else  
	    oldcontext = MemoryContextSwitchTo(MessageContext);
 
	// 执行分析和重写原始的解析树
	querytree_list = pg_analyze_and_rewrite_fixedparams(parsetree, query_string,  
                                        NULL, 0, NULL);  
	// 执行plan过程规划
	plantree_list = pg_plan_queries(querytree_list, query_string,  
                         CURSOR_OPT_PARALLEL_OK, NULL);
	// 如果前面analysis,plan使用了新的快照,那么现在结束这个快照
	// 这里还有一些逻辑没有理清,继续使用这个快照会有一些问题 TODO:待确定
	if (snapshot_set)  
	    PopActiveSnapshot();
	// 再次检查是否取消
 
	
	// ==========命令的运行时在一个Portal当中完成的===========
	// 创建一个无名的portal来运行查询。  
	portal = CreatePortal("", true, true);  
	portal->visible = false;  
	  
	// 设置portal需要执行的命令信息  
	PortalDefineQuery(portal,  
	               NULL,  
	               query_string,  
	               commandTag,  
	               plantree_list,  
	               NULL);  
	  
	// 为portal的执行做一些准备工作  
	PortalStart(portal, NULL, 0, InvalidSnapshot);  
	  
	// 选择合适的输出格式,默认是TRXT,除非从二进制游标fetch  
	format = 0;          /* TEXT is default */  
	if (IsA(parsetree->stmt, FetchStmt))  
	{  
	    FetchStmt  *stmt = (FetchStmt *) parsetree->stmt;  
	  
	    if (!stmt->ismove)  
	    {  
	       Portal    fportal = GetPortalByName(stmt->portalname);  
	  
	       if (PortalIsValid(fportal) &&  
	          (fportal->cursorOptions & CURSOR_OPT_BINARY))  
	          format = 1; /* BINARY */  
	    }  
	}  
	PortalSetResultFormat(portal, 1, &format);  
	  
	// 现在可以创建目的地的接收对象  
	receiver = CreateDestReceiver(dest);  
	if (dest == DestRemote)  
	    SetRemoteDestReceiverParams(receiver, portal);  
	  
	// 切换到事务上下文来执行  
	MemoryContextSwitchTo(oldcontext);  
	  
	// 运行portal  
	(void) PortalRun(portal,  
	              FETCH_ALL,  
	              true, /* always top level */  
	              true,  
	              receiver,  
	              receiver,  
	              &qc);  
	// 清理执行完成后的Portal资源
	receiver->rDestroy(receiver);  
	  
	PortalDrop(portal, false);
	// 执行结束后,命令,事务,超时等的相关处理
	if (lnext(parsetree_list, parsetree_item) == NULL)  
	{  
		// 最后一个sql执行结束后,关闭隐式事务块,完成事务  
	    if (use_implicit_block)  
	       EndImplicitTransactionBlock();  
	    finish_xact_command();  
	}  
	else if (IsA(parsetree->stmt, TransactionStmt))  
	{
		// 不是最后一条sql,但是时事务控制语句,那么也需要结束事务
	   finish_xact_command();  
	}  
	else  
	{  
		// 中间的sql执行结束处理
	    Assert(!(MyXactFlags & XACT_FLAGS_NEEDIMMEDIATECOMMIT));  
		CommandCounterIncrement();  
		// 取消超时,每个query的超时是独立的
		disable_statement_timeout();  
	}  
  
	// 告诉客户端查询执行结束  
	EndCommand(&qc, dest, false);  
	
	// 如果创建了per-parsetree上下文,则在这里删除  
	if (per_parsetree_context)  
	    MemoryContextDelete(per_parsetree_context);
}

这里的逻辑非常多,这里也只是梳理的流程,很多相关的知识还需要在其它章节进行了解。 可以这样理解执行过程

  1. 每个sql执行前需要确保处于正确的事务状态和快照当中
  2. 基于原始的解析树执行analysis,planning处理
    1. analysis:对原始的解析树验证语意正确性,检查查询中使用的表,列,函数等信息都是正确可用的。即验证原始的解析树是一个能够被正确执行的查询。
    2. planning:生成执行计划,描述如何执行查询获得结果的操作步骤,并进行优化
  3. 创建Portal,负责执行命令
  4. 执行结束后,清理portal。设置事务、快照、客户端响应、资源释放等清理工作,为下一个查询或者客户端请求做准备。

analysis、planning、实际执行的过程还是需要在专门的主题进行介绍。

结束事务

这就是调用事务提交的命令,同样在事务设计原理当中去了解其实现原理。 一般来说,事务提交会在最后一个sql执行结束后就执行,在这里的调用其实只有在sql的解析树为空的时候才会有用。

不过执行的逻辑都是一样的,单独提到这里进行说明方便一些。

static void  
finish_xact_command(void)  
{  
    // 取消sql执行超时
    disable_statement_timeout();  
	// 事务还在开启,那么就提交事务
    if (xact_started)  
    {  
       CommitTransactionCommand();  
 
       xact_started = false;  
    }  
}