引言
上一篇文章中,我们学习了SQL中的查询树、火山模型,并对查询执行之前的过程有了一些初步的了解。在本篇文章中,我将对执行阶段的过程,相关的函数调用,做一些简要的解释。
执行过程
对于一个DML(Data Manipulation Language)语句和DDL(Data Definition Language)语句,其查询过程顶层函数调用为exec_simple_query。exec_simple_query函数包含了SQL查询的大部分过程,其中查询执行方面的相关函数调用为:
- CreatePortal():创建Portal,Portal是openGauss里用来表示一个数据库查询的游标的结构体,可以用来执行和管理查询结果。
- PortalStart():对Portal结构体进行一些初始化设置,包括算子初始化和内存分配。
- PortalRun():查询执行的核心,根据Portal结构体的信息,选择不同的执行模式和方法。
- PortalDrop():负责删除一个portal,并释放其开销的内存。
exec_simple_query函数与查询执行相关的执行流程图如下图所示。
CreatePortal
//代码清单1
Portal CreatePortal(const char* name, bool allowDup, bool dupSilent, bool is_from_spi)
{
············
/* make new portal structure */
portal = (Portal)MemoryContextAllocZero(u_sess->top_portal_cxt, sizeof *portal);
/* initialize portal heap context; typically it won't store much */
portal->heap = AllocSetContextCreate(u_sess->top_portal_cxt,
"PortalHeapMemory",
ALLOCSET_SMALL_MINSIZE,
ALLOCSET_SMALL_INITSIZE,
ALLOCSET_SMALL_MAXSIZE);
/* create a resource owner for the portal */
portal->resowner = ResourceOwnerCreate(t_thrd.utils_cxt.CurTransactionResourceOwner, "Portal",
THREAD_GET_MEM_CXT_GROUP(MEMORY_CONTEXT_EXECUTOR));
/* initialize portal fields that don't start off zero */
portal->status = PORTAL_NEW;
············
/* put portal in table (sets portal->name) */
PortalHashTableInsert(portal, name);
············
return portal;
}
CreatePortal函数主要的代码如代码清单1所示。
其主要的功能如下:
- 调用MemoryContextAllocZero函数为portal结构体分配内存空间。
- 调用AllocSetContextCreate函数为portal->heap分配内存空间。heap字段用来存储与portal关联的分配集内存上下文,用来在该portal生命周期内管理(分配和释放)内存。
- 调用ResourceOwnerCreate函数设置该portal所拥有资源的管理者并存储到portal->resowner中,这里主要设置resowner->name=’Portal’,resowner->parent=CurTransactionResourceOwner,其他都初始化为空。resowner字段用来记录和管理资源的相关信息。例如分配资源时,会将资源的指针添加到resowner的资源链表中。
- 初始化portal里面的一些字段。
- 调用PortalHashTableInsert函数在哈希表中插入(portal,name)。
在此函数里我发现了两个有意思的点。
1.do{······}while(0)语句块
在查看PortalHashTableInsert对应的代码时发现其逻辑为宏定义+do{······}while(0)的结构,如代码清单2所示。
//代码清单2
#define PortalHashTableInsert(PORTAL, NAME) \
do { \
PortalHashEnt* hentry = NULL; \
bool found = false; \
hentry = (PortalHashEnt*)hash_search(u_sess->exec_cxt.PortalHashTable, (NAME), HASH_ENTER, &found); \
if (found) { \
ereport(ERROR, (errcode(ERRCODE_SYSTEM_ERROR), errmsg("duplicate portal name"))); \
} \
hentry->portal = PORTAL; \
(PORTAL)->name = hentry->portalname; \
} while (0)
明明do{······}while(0)语句和{······}语句是等效的,那为什么还要使用前者呢,这不是平白增加代码的复杂度和不可读性吗?带着这个疑问,我对do{······}while(0)语句进行了一些学习。
使用do{······}while(0)语句有以下两个好处:
-
提高代码健壮性,避免宏调用出错。
例如定义一个这样的宏:
#define execute() \
execute1(); \
execute2()
在调用的时候这么写:
if(true)
execute();
因为宏调用是直接替换字符串,因此展开后是这个样子的。
if(true)
execute1();
execute2();
出现了意料之外的错误。同样地,在宏定义中使用{}也不能解决问题,说不定还会出现编译错误。一个比较优秀的解决办法是使用do{······}while(0)语句。如下代码:
//宏定义
#define execute() \
do{ \
execute1(); \
execute2(); \
}while(0)
//替换后
if(true)
do{
execute1();
execute2();
}while(0);
问题完美解决。
-
限定变量的作用域
当想使用临时变量,不想对外部代码块的变量造成干扰时,可以考虑使用do{······}while(0)语句,在{}内定义的变量作用于仅局限于{}内,不会对外部产生影响。
2.内存上下文管理
portal中的heap字段对应的内存分配是一种基于块的内存分配方法,在下一篇文章我将详细讲讲这个方法。
小结
本节简单分析了SQL语句查询执行过程中的函数调用,并对CreatePortal函数做了简要解析,发现了opengauss源码里对于提升代码健壮性的一个方法——do{······}while(0)语句。下一篇文章我将解释我在CreatePortal函数里发现的第二个有趣的知识点,以及对查询执行过程中的PortalStart、PortalRun等函数做简要解析。