暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PL/pgSQL 性能提升之三编码方式

飞象数据 2018-11-27
686

背景

客户应用大量使用存储过程,并且创建连接后每次只调用一个函数,获得结果关闭连接,也就是大量短连接场景。

为了尽可能高地实现最大并发数与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
复制

相差几乎一倍,所以能用步进的时候不要用其他循环方式。

结论

以此为例,我们还可以在代码中继续挖掘更多不同编码方式带来的影响,大规模使用存储过程时就要考虑它们带来的开销。

即使是简单计划,仍然有优化的方法,敬请期待《原生计算》篇。

敬请关注飞象数据

文章转载自飞象数据,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论