SQL 查询执行总体流程(1)

引言

上一篇文章中,我们学习了SQL中的查询树、火山模型,并对查询执行之前的过程有了一些初步的了解。在本篇文章中,我将对执行阶段的过程,相关的函数调用,做一些简要的解释。

执行过程

对于一个DML(Data Manipulation Language)语句和DDL(Data Definition Language)语句,其查询过程顶层函数调用为exec_simple_query。exec_simple_query函数包含了SQL查询的大部分过程,其中查询执行方面的相关函数调用为:

  1. CreatePortal():创建Portal,Portal是openGauss里用来表示一个数据库查询的游标的结构体,可以用来执行和管理查询结果。
  2. PortalStart():对Portal结构体进行一些初始化设置,包括算子初始化和内存分配。
  3. PortalRun():查询执行的核心,根据Portal结构体的信息,选择不同的执行模式和方法。
  4. PortalDrop():负责删除一个portal,并释放其开销的内存。

exec_simple_query函数与查询执行相关的执行流程图如下图所示。

执行流程

CreatePortal

 
  1. //代码清单1
  2. Portal CreatePortal(const char* name, bool allowDup, bool dupSilent, bool is_from_spi)
  3. {
  4. ············
  5. /* make new portal structure */
  6. portal = (Portal)MemoryContextAllocZero(u_sess->top_portal_cxt, sizeof *portal);
  7. /* initialize portal heap context; typically it won't store much */
  8. portal->heap = AllocSetContextCreate(u_sess->top_portal_cxt,
  9. "PortalHeapMemory",
  10. ALLOCSET_SMALL_MINSIZE,
  11. ALLOCSET_SMALL_INITSIZE,
  12. ALLOCSET_SMALL_MAXSIZE);
  13. /* create a resource owner for the portal */
  14. portal->resowner = ResourceOwnerCreate(t_thrd.utils_cxt.CurTransactionResourceOwner, "Portal",
  15. THREAD_GET_MEM_CXT_GROUP(MEMORY_CONTEXT_EXECUTOR));
  16. /* initialize portal fields that don't start off zero */
  17. portal->status = PORTAL_NEW;
  18. ············
  19. /* put portal in table (sets portal->name) */
  20. PortalHashTableInsert(portal, name);
  21. ············
  22. return portal;
  23. }

CreatePortal函数主要的代码如代码清单1所示。

其主要的功能如下:

  1. 调用MemoryContextAllocZero函数为portal结构体分配内存空间。
  2. 调用AllocSetContextCreate函数为portal->heap分配内存空间。heap字段用来存储与portal关联的分配集内存上下文,用来在该portal生命周期内管理(分配和释放)内存。
  3. 调用ResourceOwnerCreate函数设置该portal所拥有资源的管理者并存储到portal->resowner中,这里主要设置resowner->name=’Portal’,resowner->parent=CurTransactionResourceOwner,其他都初始化为空。resowner字段用来记录和管理资源的相关信息。例如分配资源时,会将资源的指针添加到resowner的资源链表中。
  4. 初始化portal里面的一些字段。
  5. 调用PortalHashTableInsert函数在哈希表中插入(portal,name)。

在此函数里我发现了两个有意思的点。

1.do{······}while(0)语句块

在查看PortalHashTableInsert对应的代码时发现其逻辑为宏定义+do{······}while(0)的结构,如代码清单2所示。

 
  1. //代码清单2
  2. #define PortalHashTableInsert(PORTAL, NAME) \
  3. do { \
  4. PortalHashEnt* hentry = NULL; \
  5. bool found = false; \
  6. hentry = (PortalHashEnt*)hash_search(u_sess->exec_cxt.PortalHashTable, (NAME), HASH_ENTER, &found); \
  7. if (found) { \
  8. ereport(ERROR, (errcode(ERRCODE_SYSTEM_ERROR), errmsg("duplicate portal name"))); \
  9. } \
  10. hentry->portal = PORTAL; \
  11. (PORTAL)->name = hentry->portalname; \
  12. } while (0)

明明do{······}while(0)语句和{······}语句是等效的,那为什么还要使用前者呢,这不是平白增加代码的复杂度和不可读性吗?带着这个疑问,我对do{······}while(0)语句进行了一些学习。

使用do{······}while(0)语句有以下两个好处:

  • 提高代码健壮性,避免宏调用出错。

    例如定义一个这样的宏:

     
      
    1. #define execute() \
    2. execute1(); \
    3. execute2()

    在调用的时候这么写:

     
      
    1. if(true)
    2. execute();

    因为宏调用是直接替换字符串,因此展开后是这个样子的。

     
      
    1. if(true)
    2. execute1();
    3. execute2();

    出现了意料之外的错误。同样地,在宏定义中使用{}也不能解决问题,说不定还会出现编译错误。一个比较优秀的解决办法是使用do{······}while(0)语句。如下代码:

     
      
    1. //宏定义
    2. #define execute() \
    3. do{ \
    4. execute1(); \
    5. execute2(); \
    6. }while(0)
    7. //替换后
    8. if(true)
    9. do{
    10. execute1();
    11. execute2();
    12. }while(0);

    问题完美解决。

  • 限定变量的作用域

    当想使用临时变量,不想对外部代码块的变量造成干扰时,可以考虑使用do{······}while(0)语句,在{}内定义的变量作用于仅局限于{}内,不会对外部产生影响。

2.内存上下文管理

portal中的heap字段对应的内存分配是一种基于块的内存分配方法,在下一篇文章我将详细讲讲这个方法。

小结

本节简单分析了SQL语句查询执行过程中的函数调用,并对CreatePortal函数做了简要解析,发现了opengauss源码里对于提升代码健壮性的一个方法——do{······}while(0)语句。下一篇文章我将解释我在CreatePortal函数里发现的第二个有趣的知识点,以及对查询执行过程中的PortalStart、PortalRun等函数做简要解析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值