在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();
}- 启动事务
- 解析sql生成解析树
- 遍历执行每一个解析树
- 结束事务
接下来并主要从这节点来进行讲解。
启动事务
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);
}这里的逻辑非常多,这里也只是梳理的流程,很多相关的知识还需要在其它章节进行了解。 可以这样理解执行过程
- 每个sql执行前需要确保处于正确的事务状态和快照当中
- 基于原始的解析树执行analysis,planning处理
- analysis:对原始的解析树验证语意正确性,检查查询中使用的表,列,函数等信息都是正确可用的。即验证原始的解析树是一个能够被正确执行的查询。
- planning:生成执行计划,描述如何执行查询获得结果的操作步骤,并进行优化
- 创建Portal,负责执行命令
- 执行结束后,清理portal。设置事务、快照、客户端响应、资源释放等清理工作,为下一个查询或者客户端请求做准备。
analysis、planning、实际执行的过程还是需要在专门的主题进行介绍。
结束事务
这就是调用事务提交的命令,同样在事务设计原理当中去了解其实现原理。 一般来说,事务提交会在最后一个sql执行结束后就执行,在这里的调用其实只有在sql的解析树为空的时候才会有用。
不过执行的逻辑都是一样的,单独提到这里进行说明方便一些。
static void
finish_xact_command(void)
{
// 取消sql执行超时
disable_statement_timeout();
// 事务还在开启,那么就提交事务
if (xact_started)
{
CommitTransactionCommand();
xact_started = false;
}
}