第 17 课 PostgreSQL plsql函数创建过程

1. SQL/PLSQL简述

SQL语句是指单条可以直接执行的语句,例如: select * from xxxx_table;。而plsql(Procedural Language/SQL),顾名思义,可以理解为多条SQL语句组成的有一定的业务逻辑关系的语句块,例如C语言中的函数就是表达了一个执行单元。

那么SQL语句和PLSQL语句在PostgreSQL中的执行有何区别呢?

在PosgreSQL实现中的区别:

  1. 功能上,SQL是直接由PG的SQL模块做语法解析并执行的,也就是源码中的src/backend目录下的代码来完成的,SQL模块一次只能支持单条语句的执行,多条语句之间的执行是完全独立的完整过程,由客户端发起的两次单独过程(有人肯定会说prepare不是,其实prepare无非就是一次传输了多组参数,SQL执行的是同一条相同语句,与我讲的不是同一个东西),也就是由一个分号分割的独立语句。
  2. 功能上,PLSQL要复杂得多,他可以实现复杂的业务逻辑,他首先需要plsql模块对其语法进行解析,在PG中,plsql模块是作为一个插件的形式存在,他的源代码实现在src/pl/plpgsql中。
  3. 执行步骤不同,由服务进程接到创建plsql函数的消息,然后调用plsql的插件接口去创建plsql函数,创建成功后,保存在系统表中,后续才能调用。SQL语句不需要plsql插件进行解析和创建,直接执行语句,然后返回结果,都是SQL模块完成的。一般的plsql是先创建执行体(函数或存储过程),然后再调用创建出来的函数。如果PG实现了支持匿名函数,那么从用户角度看他是不需要创建的,和SQL语句一样创建和执行是一次调用完成的。但是从执行逻辑上看是不一样的,匿名函数不在这里讨论。

综上所述,在PG中存在两个做语法解析的实现,SQL模块属于后台服务的核心功能模块,只能支持单条语句的执行,并返回执行结果。PLSQL是作为插件先解析复杂函数,然后调用SPI接口(SQL模块对外接口,只供进程内部其他模块调用)逐条执行语句。PLSQL需要组织整个语句块的执行逻辑,并保存上下文关系。plsql模块对于用户来讲是不可见的,对于用户来讲可以和SQL模块一样归为服务端过程。但是我们要理解plsql的功能,就需要在内部和SQL模块做功能上的区分。

2. PLSQL创建函数在SQL端的流程

从上面的内容,我们了解到plsql语句执行的前后关系,本节将详细讲解plsql在执行语法解析之前的过程,需要掌握他是怎么进入plsql语法解析流程的,以及什么情况下会调到plsql模块。创建plsql函数在SQL端流程可以大概分为如下几步:

  1. Postgres服务进程接受到创建函数或存储过程的请求消息;
  2. SQL端针对这种语句进行简单语法解析,执行路径规划,不深入解析函数体;
  3. Portal过程,根据语法解析生成的计划树,一步步判断进入到调用plsql语法分析的入口;
  4. 保存解析完成后的创建的函数到系统表,供用户调用。

我们把创建语句统称为DDL(Data Definition Languages)语句,创建函数和存储过程都是属于这种语句,如下面的形式:

CREATE [ OR REPLACE ] FUNCTION  name ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | := } default_expr ] [, ...] ] )
[{RETURNS | RETURN} rettype ] [IMMUTABLE | STABLE | VOLATILE | SECURITY INVOKER | SECURITY DEFINER]
{ AS | IS }
{
  [ label_name ]
  [ DECLARE ]
   [ variable_declaration ]
   [ cursor_declaration ]
  BEGIN
    sequence_of_statement
  END [ label_name ]
}

CREATE [ OR REPLACE ] PROCEDURE  ProcedureName ( [<ParameterList>[, ...n]] )
{AS | IS}
[<<LabelName>>]
[DECLARE]
    [<VariableDeclaration>]
    [<CursorDeclaration>]
BEGIN
  <SequenceOfStatements>
END [LabelName];

<VariableDeclaration> ::= {PlsqlVariable <DataType>}[; ...n]
  1. 函数PostgresMain()是服务进程循环等待请求的主函数,他死循环的等待客户端的请求,在接受到消息后,对消息进行解析。消息内容的第一个字符标识了消息的类别,‘Q’表示查询,‘P’表示做分析,‘B’表示绑定(支持prepare的功能),‘E’表示执行,还有其他的消息类型。而我们的psql创建过程的消息类型是‘Q’。
case 'Q':           /* simple query */
{
    const char *query_string;

    /* Set statement_timestamp() */
    SetCurrentStatementStartTimestamp();

    query_string = pq_getmsgstring(&input_message);
    pq_getmsgend(&input_message);

    if (am_walsender)
        exec_replication_command(query_string);
    else
        exec_simple_query(query_string);

    send_ready_for_query = true;
}
  1. 进入函数exec_simple_query(),调用函数pg_parse_query()->pg_parse_query()->raw_parser()进行语法分析,这是在SQL端做简单的语法解析,并不会深入到函数体内部进行语法解析,只是检查前面的“CREATE [ OR REPLACE ] FUNCTION"等符号,返回一个原始的语法树链表,然后对语法树进行路径规划生成查询树、生成计划树,最后调用PortalRun来开始处理具体的创建逻辑。
static void exec_simple_query(const char *query_string) {
    /* 语法检查,并生成原始语法树 */
    parsetree_list = pg_parse_query(query_string);
        
    foreach(parsetree_item, parsetree_list) {
        /* 语法分析和规则重写,返回查询树 */
        querytree_list = pg_analyze_and_rewrite(parsetree, query_string, NULL,0);
        /* 生成计划 */
        plantree_list = pg_plan_queries(querytree_list, CURSOR_OPT_PARALLEL_OK, NULL);
        /* 开始执行具体工作前的准备流程 */
        PortalStart(portal, NULL, 0, InvalidSnapshot);
        /* 开始执行具体工作 */
        (void) PortalRun(portal,
                         FETCH_ALL,
                         isTopLevel,
                         receiver,
                         receiver,
                         completionTag);
    }
}

3.在函数PortalRun()中开始正真的处理语句,前面都是处理基本的语法分析和路径优化,我们的"CREATE [ OR REPLACE ] PROCEDURE"属于PORTAL_MULTI_QUERY类型。

bool
PortalRun(Portal portal, long count, bool isTopLevel,
          DestReceiver *dest, DestReceiver *altdest,
          char *completionTag) {
    /* 根据语句类型选择执行流程 */
    switch (portal->strategy) {
        case PORTAL_ONE_SELECT:
        case PORTAL_ONE_RETURNING:
        case PORTAL_ONE_MOD_WITH:
        case PORTAL_UTIL_SELECT:
             // done xxxxx
             break;
        case PORTAL_MULTI_QUERY:
            PortalRunMulti(portal, isTopLevel, false, dest, altdest, completionTag);
            /* 阻止重新执行Portal的命令 */
            MarkPortalDone(portal);
            /* 总是在RunMulti结束标记完成 */
            result = true;
            break;
    }
}

我们看看PORTAL_MULTI_QUERY的定义是:所有其他情况。这里,我们不支持部分执行:Portal的查询将在第一次调用时运行到完成。这里表示SQL处理不了这种创建语句,需要外部插件来完成。顾名思义,是多语句查询的意思,其实这个不是查询语句,PG是后期增加plsql模块,为了不对SQL模块做大的改动,把这种归类到其他的查询语句类型。其实不符合设计的理念。

typedef enum PortalStrategy
{
    PORTAL_ONE_SELECT,
    PORTAL_ONE_RETURNING,
    PORTAL_ONE_MOD_WITH,
    PORTAL_UTIL_SELECT,
    PORTAL_MULTI_QUERY
} PortalStrategy;
  1. 在函数PortalRunMulti()循环处理语句,对于我们的创建函数的语句来说,只有一条,只循环一次就完成了。具体内容分析看源码注释。
static void
PortalRunMulti(Portal portal,
               bool isTopLevel, bool setHoldSnapshot,
               DestReceiver *dest, DestReceiver *altdest,
               char *completionTag) {
  foreach(stmtlist_item, portal->stmts) {
    /* 
     * 我们的创建plsql函数的语句是一个 {type = T_CreateFunctionStmt} ,
     * 不是一个PlannedStmt, 进入else执行
     */
    if (IsA(stmt, PlannedStmt) && ((PlannedStmt *) stmt)->utilityStmt == NULL) {
       // don't care
    } else {
       /* 
        * 下面选择标准就是是否需要设置完成标签,但是个人感觉这里是没有必要的做
        * 出这样的判断,因为 completionTag从外部传进来,可以NULL,如果为NULL
        * 则便是不需要,只需在最终赋值的地方判断是否为空便是。即使需要这样一个
        * 返回的标签,也大可不必为了这样一个单独返回值,为所有的调用函数增加一
        * 个参数一路传递下来,应该在stmt增加一个字段。
        */
       if (pstmt->canSetTag) {
          Assert(!active_snapshot_set);
          /* statement can set tag string */
          PortalRunUtility(portal, stmt, isTopLevel, false, dest, completionTag);
        } else {
          Assert(IsA(stmt, NotifyStmt));
          /* stmt added by rewrite cannot set tag */
          PortalRunUtility(portal, stmt, isTopLevel, false, altdest, NULL);
        }
     }  
  }
}
  1. 在函数PortalRunUtility()中,这个函数只是做了一个镜像保存,实际具体的工作是调用如下:
ProcessUtility()
  ->ProcessUtility()
    ->pglogical_ProcessUtility()
      ->standard_ProcessUtility()
        ->ProcessUtilitySlow()
/*
 * Execute a utility statement inside a portal.
 */
static void
PortalRunUtility(Portal portal, PlannedStmt *pstmt,
                 bool isTopLevel, bool setHoldSnapshot,
                 DestReceiver *dest, char *completionTag) {
    ProcessUtility(pstmt,
                   portal->sourceText,
                   isTopLevel ? PROCESS_UTILITY_TOPLEVEL : PROCESS_UTILITY_QUERY,
                   portal->portalParams,
                   portal->queryEnv,
                   dest,
                   completionTag);
}

void
ProcessUtility(PlannedStmt *pstmt,
               const char *queryString,
               ProcessUtilityContext context,
               ParamListInfo params,
               QueryEnvironment *queryEnv,
               DestReceiver *dest,
               char *completionTag) {
    /*
     * 为插件准备的一个接口,如果有插件需要监听所有的语句执行,已经进行语句
     * 检查,就需要设置该回调函数,插件回调完成后,最后还是会调用
     * standard_ProcessUtility()回到正常流程。这里一般是为pglogical_ProcessUtility
     */
    if (ProcessUtility_hook)
        (*ProcessUtility_hook) (pstmt, queryString,
                                context, params, queryEnv,
                                dest, completionTag);
    else
        standard_ProcessUtility(pstmt, queryString,
                                context, params, queryEnv,
                                dest, completionTag);
}

void
standard_ProcessUtility(PlannedStmt *pstmt,
                        const char *queryString,
                        ProcessUtilityContext context,
                        ParamListInfo params,
                        QueryEnvironment *queryEnv,
                        DestReceiver *dest,
                        char *completionTag) {

        default:
            /* All other statement types have event trigger support */
            ProcessUtilitySlow(pstate, pstmt, queryString,
                               context, params, queryEnv,
                               dest, completionTag);
}

static void
ProcessUtilitySlow(ParseState *pstate,
                   PlannedStmt *pstmt,
                   const char *queryString,
                   ProcessUtilityContext context,
                   ParamListInfo params,
                   QueryEnvironment *queryEnv,
                   DestReceiver *dest,
                   char *completionTag) {

            case T_CreateFunctionStmt:  /* CREATE INTERNAL FUNCTION */
                address = CreateFunction((CreateFunctionStmt *) parsetree, queryString);
                break;
}
  1. 上面的步骤都是在做路径选择,没有做具体的工作,是该做点具体的事情了,看看CreateFunction()函数,他通过一系列的调用,来完成函数的创建。他的参数CreateFunctionStmt结构看下下面。
CreateFunction()
  ->ProcedureCreate()
    ->plpgsql_validator()   --进入plsql插件模块
      ->plpgsql_compile()
        ->do_compile()
          ->plpgsql_yyparse()  --到这里才到我们plsql语法解析,文章的主题
/*
 * CreateFunction
 *   Execute a CREATE FUNCTION (or CREATE PROCEDURE) utility statement.
 */
ObjectAddress
CreateFunction(ParseState *pstate, CreateFunctionStmt *stmt) {
    /* 有待分析 */
}

/* ----------------------
 *      Create Function Statement
 * ----------------------
 */
typedef struct CreateFunctionStmt
{
    NodeTag     type;
    bool        is_procedure;   /* it's really CREATE PROCEDURE */
    bool        replace;        /* T => replace if already exists */
    List       *funcname;       /* qualified name of function to create */
    List       *parameters;     /* a list of FunctionParameter */
    TypeName   *returnType;     /* the return type */
    List       *options;        /* a list of DefElem */
} CreateFunctionStmt;

3. PLSQL创建函数在plsql端语法解析过程

未完成待续。。。


上一课 第 16 课 查询过程源码分析

PostgreSQL更多文章专辑链接,点我查看

发现更多宝藏

我在喜马拉雅上分享声音

《PostgreSQL数据库内核分析》,点开链接可以听听,有点意思。

《数据库系统概论(第4版)》,点开链接可以听听,有点意思。

更多IT有声课程,点我发现更多

更多交流加群: PostgreSQL内核开发群 876673220

亲,记得点赞、留言、打赏额!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容