1. SQL/PLSQL简述
SQL语句是指单条可以直接执行的语句,例如: select * from xxxx_table;。而plsql(Procedural Language/SQL),顾名思义,可以理解为多条SQL语句组成的有一定的业务逻辑关系的语句块,例如C语言中的函数就是表达了一个执行单元。
那么SQL语句和PLSQL语句在PostgreSQL中的执行有何区别呢?
在PosgreSQL实现中的区别:
- 功能上,SQL是直接由PG的SQL模块做语法解析并执行的,也就是源码中的src/backend目录下的代码来完成的,SQL模块一次只能支持单条语句的执行,多条语句之间的执行是完全独立的完整过程,由客户端发起的两次单独过程(有人肯定会说prepare不是,其实prepare无非就是一次传输了多组参数,SQL执行的是同一条相同语句,与我讲的不是同一个东西),也就是由一个分号分割的独立语句。
- 功能上,PLSQL要复杂得多,他可以实现复杂的业务逻辑,他首先需要plsql模块对其语法进行解析,在PG中,plsql模块是作为一个插件的形式存在,他的源代码实现在src/pl/plpgsql中。
- 执行步骤不同,由服务进程接到创建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端流程可以大概分为如下几步:
- Postgres服务进程接受到创建函数或存储过程的请求消息;
- SQL端针对这种语句进行简单语法解析,执行路径规划,不深入解析函数体;
- Portal过程,根据语法解析生成的计划树,一步步判断进入到调用plsql语法分析的入口;
- 保存解析完成后的创建的函数到系统表,供用户调用。
我们把创建语句统称为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]
- 函数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;
}
- 进入函数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;
- 在函数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);
}
}
}
}
- 在函数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;
}
- 上面的步骤都是在做路径选择,没有做具体的工作,是该做点具体的事情了,看看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数据库内核分析》,点开链接可以听听,有点意思。
《数据库系统概论(第4版)》,点开链接可以听听,有点意思。
更多交流加群: PostgreSQL内核开发群 876673220
亲,记得点赞、留言、打赏额!!!