背景
客户应用大量使用存储过程,并且创建连接后每次只调用一个函数,获得结果关闭连接,也就是大量短连接场景。
为了尽可能高地实现最大并发数与TPS,我们做了很多尝试。
PL/pgSQL引擎介绍
PostgreSQL 存储过程引擎设计非常先进,预留的接口可以实现非常丰富的语言支持,我们熟知的包括:C、PL/pgSQL、Python、Perl,还有非官方的 Java、PHP、JavaScript 等等。PL/pgSQL是最常用的官方过程语言,性能虽然无法与C语言相比,胜在开发效率高,有测试表明它比其他几种官方语言引擎要快得多。为最大可能的提高函数调用的吞吐量,飞象数据对函数引擎进行了深入细致的分析,并且做了大量尝试和改进。
PL/pgSQL 简单计划
首先来看 pl_exec.c 中的函数 exec_prepare_plan,它调用SPI生成执行计划,然后检查这个计划是否是简单计划,比如 IF (a>10) THEN之中的(a>10)是简单表达式,它对应的就是一个简单计划。在 exec_simple_check_plan 可以看到简单表达式的检查条件:1、只能是单一查询树2、只能是 Query3、...
比如以下含有集计、Window、子查询、……(具体含义可查阅Query结构体定义)都不是:
if (query->hasAggs || query->hasWindowFuncs || query->hasTargetSRFs || query->hasSubLinks || query->cteList || query->jointree->fromlist || query->jointree->quals || query->groupClause || query->groupingSets || query->havingQual || query->windowClause || query->distinctClause || query->sortClause || query->limitOffset || query->limitCount || query->setOperations)复制
简单计划与其他计划有何不同
表达式计算代码位于函数 exec_eval_expr,调用 exec_eval_simple_expr 尝试以简单计划执行,它经过一些检查和准备之后调用执行器的接口函数 ExecEvalExpr 得到结果。如果无法执行,则通过调用 SPI 执行,也就是后边的 exec_run_select 函数调用:
if (exec_eval_simple_expr(estate, expr, &result, isNull, rettype, rettypmod)) return result; /* * Else do it the hard way via exec_run_select */ rc = exec_run_select(estate, expr, 2, NULL);复制
所以应该尽量形成简单计划。
赋值表达式
为变量赋值可以有两种写法
a = b + 1复制
以及
SELECT b + 1 INTO a复制
它们的执行结果是一样的,但效率有天壤之别,前者是一个简单计划,而后者不是。
循环语句
整数步进循环 FOR i IN l..u BY s LOOP 对应结构 PLpgSQL_stmt_fori,执行函数是 exec_stmt_fori。过程:1、先计算最大值最小值2、计算步进值3、开始按步进循环
这种步进循环也可以写成其他方式,比如
i = l;WHILE i<u LOOP i = i + s;END LOOP;复制
它每次循环都需要累加一次、判断一次。
测试赋值表达式
直接赋值
CREATE OR REPLACE FUNCTION f() RETURNS void AS$$DECLARE a int;BEGIN FOR i IN 1..100000 LOOP a = i + 1; END LOOP;END;$$LANGUAGE plpgsql;复制
执行时间
flying=# SELECT f(); f --- (1 row)Time: 46.801 ms复制
SELECT赋值
CREATE OR REPLACE FUNCTION f() RETURNS void AS$$DECLARE a int;BEGIN FOR i IN 1..100000 LOOP SELECT i + 1 INTO a; END LOOP;END;$$LANGUAGE plpgsql;复制
执行时间
flying=# SELECT f(); f --- (1 row)Time: 294.856 ms复制
相差6倍左右,所以能直接赋值就不要使用 SELECT INTO 方式。
测试循环语句
步进循环(赋值语句是为了让测试运算一致,也是为了防止执行引擎过度优化。)
CREATE OR REPLACE FUNCTION f() RETURNS void AS$$DECLARE a int;BEGIN FOR i IN 1..100000 LOOP a = i + 1; END LOOP;END;$$LANGUAGE plpgsql;复制
执行时间
flying=# SELECT f(); f --- (1 row)Time: 27.606 ms复制
WHILE循环
CREATE OR REPLACE FUNCTION f() RETURNS void AS$$DECLARE i int = 0;BEGIN WHILE i<100000 LOOP i = i + 1; END LOOP;END;$$LANGUAGE plpgsql;复制
执行时间
flying=# SELECT f(); f --- (1 row)Time: 49.548 ms复制
相差几乎一倍,所以能用步进的时候不要用其他循环方式。
结论
以此为例,我们还可以在代码中继续挖掘更多不同编码方式带来的影响,大规模使用存储过程时就要考虑它们带来的开销。
即使是简单计划,仍然有优化的方法,敬请期待《原生计算》篇。
敬请关注飞象数据