PostgreSQL:以人为语言(PL / Perl,PL / Python,PL / v8)进行服务器端编程

Postgres以其可扩展性而闻名,这也适用于对过程语言(PL)的支持。没有人能拥有这么长的语言列表来夸耀一门语言,并且这个列表可能根本不受限制:要将语言连接到服务器,不需要额外的工作。您甚至可以提出自己的语言,并使其成为服务器过程语言。 DBMS中的更改不需要这样做。如此之多,这种可扩展性从一开始就已内置到Postgres架构中。

为任务编写PL语言是可能的,有时甚至是必要的。更好的是,如果有人编写了这样的框架来编写语言,以便您不能用C语言编写,而是选择一种对语言开发人员来说更舒适的语言。与FDW一样,可以用Python编写

本文是根据作者在PgConf.Russia 2019PgConf.Russia 2018DevConf 2017会议上就此主题撰写的大量报告和大师班的基础上撰写的

这不是关于异国情调,而是关于最常见的过程语言PL / Perl,PL / Python和PL / V8(即JavaScript)并将它们的功能与PL / pgSQL进行比较。

这些语言什么时候值得使用?什么时候缺少SQL和PL / pgSQL?

  • 然后,当您需要使用复杂的结构和算法时,例如:遍历树,或者需要进行HTML或XML解析时,尤其是从存档中提取树时;
  • 当您需要动态生成复杂的SQL(报告,ORM)时。在PL / pgSQL上,它不仅不方便,而且在某些情况下也会运行缓慢;
  • Perl Python, C/C++, Perl Python . . , Oracle. , Postgres . Perl Python .
  • — . , , untrusted- ( — . ), Perlu Python(3)u, PL/V8. Postgres , , FDW, , . . !
  • 还有一件事:如果您要用C编写东西,那么您可以用这些语言制作一个更适合快速开发的原型。

如何在Postgres中嵌入语言


要实现您需要的语言:用C编写一到三个函数:

  • HANDLER-将使用该语言执行功能的调用处理程序(这是必需的部分);
  • INLINE-匿名块处理程序(如果您希望语言支持匿名块);
  • 验证程序-创建功能时的代码验证功能(如果要完成此验证)。

此处此处的文档中对此进行了详细描述

“开箱即用的语言”和其他语言


开箱即用仅支持四种语言:PL / pgSQLPL / PerlPL / PythonPL / Tcl,但是挠痒痒是对历史的致敬:现在很少有人使用它,我们不再谈论它了。
Postgres社区支持PL / Perl,PL / Python,当然还有PL / pgSQL。他们的维护者(公司,社区或对DBMS内部使用该语言感兴趣的特定开发人员)可以支持其他非盒装语言PL / V8宣传Google。但是有时会有原因怀疑PL / V8的万里无云的未来。 Google当前的PL / V8项目维护者Jerry Sievert正在考虑基于其他引擎(例如QuickJS)的基于Postgres服务器的JS支持,因为PL / V8难以构建,并且需要3-5 GB Linux在构建时会出现各种各样的东西,这通常会在不同的OS上引起问题。但是PL / V8被广泛使用并经过全面测试。 PL / JS可能会替代其他JS引擎出现,或者暂时只是一个名称,在过渡期间我们会习惯。

PL / Java很少使用。我个人不需要用PL / Java编写,因为在PL / Perl和PL / V8中,几乎所有任务都有足够的功能。甚至Python也没有特别添加功能。PL / R对于喜欢统计数据并且喜欢这种语言的人很有用。我们也不会在这里谈论他。

流行的语言不一定会在编写存储时流行:有PL / PHP,但现在几乎没有任何人支持它-很少有人想要在上面编写服务器过程。对于PL / Ruby语言,情况似乎是相同的,尽管该语言似乎更现代。

正在开发一种基于Go的过程语言,请参见PL / Go,以及PL / Lua。有必要研究它们。对于顽固的外壳爱好者,甚至还有PL / Sh,甚至很难想象它的用途。

至少有一种专门针对其任务的领域特定过程语言(DSL)-PL / Proxy,过去在代理和平衡服务器负载方面非常流行。

在本文中,我们将介绍主要的,最常用的语言。当然,这是PL / PgSQL,PL / Perl,PL / Python和PL / V8,我们在下面将它们称为PL / *

``开箱即用''的语言实际上几乎是开箱即用地安装的-通常安装很轻松。但是要安装PL / V8,如果您没有在操作系统的存储库中找到具有必要版本的软件包,这几乎是一项壮举,因为为此您将必须实际构建整个V8,或者换句话说,构建Chromium。同时,整个开发基础架构将连同V8一起从google.com下载-数千兆字节的流量。对于Ubuntu下的Postgres 11,尚未出现PL / V8软件包,到目前为止,版本库中仅提供了适用于PG 10的V8,如果需要,请手工组装。同样重要的是,您将在存储库中找到的版本很可能很旧。在发布本文时,最新版本为2.3.14。

安装语言本身之后,还必须“创建”该语言-在系统目录中注册它。这应该由团队来完成。

CREATE EXTENSION plperl;

(代替plperl,您可以替换另一种语言的名称,有些细微差别,请参见下文)。
我们看看发生了什么:

test_langs=# \x
test_langs=# \dL+
List of languages
-[ RECORD 1 ]-----+---------------------------------
Name              | plperl
Owner             | postgres
Trusted           | t
Internal language | f
Call handler      | plperl_call_handler()
Validator         | plperl_validator(oid)
Inline handler    | plperl_inline_handler(internal)
Access privileges |
Description       | PL/Perl procedural language
-[ RECORD 2 ]-----+---------------------------------
Name              | plpgsql
Owner             | postgres
Trusted           | t
Internal language | f
Call handler      | plpgsql_call_handler()
Validator         | plpgsql_validator(oid)
Inline handler    | plpgsql_inline_handler(internal)
Access privileges |
Description       | PL/pgSQL procedural language
[ RECORD 3 ]-----+---------------------------------
Name              | plv8
Owner             | postgres
Trusted           | t
Internal language | f
Call handler      | plv8_call_handler()
Validator         | plv8_call_validator(oid)
Inline handler    | plv8_inline_handler(internal)
Access privileges |
Description       |

PL / pgSQL不需要专门创建;它始终已经存在于数据库中。

注意!PL / pgSQL不应与SQL混淆。这是另一种语言。但是,Postgres也可以用普通SQL编写函数。

标准品


在DBMS领域,他们经常谈论遵守SQL标准。程序语言也有标准,尽管很少被谈论。SQL / PSM标准与DB2过程语言高度兼容。尽管从概念上讲它们很接近,但是它的实现与PL / pgSQL相比还很遥远。

SQL / JRT是Java过程的标准,而PL / Java是很好的匹配。

受信任和不受信任的语言


Postgres程序语言是受信任的(TRUSTED)和不受信任的(UNTRUSTED)。
在TRUSTED语言中,不可能直接使用I / O(包括网络),甚至不能直接使用系统资源。因此,任何数据库用户都可以创建这样的功能,破坏某些东西,而他将无法学到太多。 UNTRUSTED语言的功能只能由主管创建。

如果语言解释器支持此类限制,则可以将其用于创建TRUSTED和UNTRUSTED语言。因此,对于Perl,有多种语言plperlplperlu。给写信最后,它给出了语言的不受信任的特征。Python仅存在于不受信任的版本中。PL / v8-相反,仅在受信任的情况下。因此,PL / v8无法从磁盘加载任何模块或库,只能从数据库加载。

使用UNTRUSTED语言的功能可以执行任何操作:发送电子邮件,ping网站,登录外部数据库并执行HTTP请求。受信任的语言仅限于处理数据库中的数据。

受信任包括:plpgsql, plperl, plv8, pljava

由UNTRUSTED包括:plperlu, pljavau, plpython2u, plpython3u

请注意:没有像TRUSTED这样的PL / Python(因为您不能在该处设置对资源的访问限制),而PLpgSQL和PL / V8则是相反的:它们不是未绑定的。

但是Perl和Java在这两个版本中都可用。

PL / pgSQL与PL / *


PL / pgSQL代码本身可与Postgres具有的所有数据类型一起使用。其他语言没有很多Postgres类型,语言解释器负责将数据转换为语言的内部表示形式,用文本替换晦涩的类型。但是,可以在TRANSFORM的帮助下为他提供帮助,我将在本文结尾处详细讨论。

PL / pgSQL中的函数调用通常更昂贵。其他语言的函数无需查看系统目录即可访问其库。 PL / pgSQL不能那样工作。由于支持许多类型,PL / pgSQL中的某些查询工作了很长时间:要添加两个整数,解释器需要意识到他正在处理整数而不是其他一些奇异类型,然后决定如何折叠它们,然后才真正折叠它们。

由于PL / pgSQL是Trusted,因此无法使用它处理网络和磁盘。

在处理嵌套数据结构时,PL / pgSQL仅具有用于JSON的Postgres工具,这些工具非常繁琐且无用,而在其他语言中,使用嵌套结构要简单得多且更经济。

PL / *具有其自己的内存管理,因此您需要监视内存或限制它。

您应该仔细监视错误处理,这对于每个人也有所不同。

但是在PL / *中,存在一个全局解释器上下文,并且可以将其用于例如缓存数据(包括查询计划)。如果语言为UNTRUSTED,则网络和驱动器可用。所有这些语言通常都是通过SPI与数据库一起使用的,但稍后会介绍更多。

让我们仔细看看PL / *语言的功能。

PL / Perl


Perl解释器是内存中的大量代码,但是幸运的是,在打开连接时不会创建代码,而仅在启动第一个存储过程/函数PL / Perl时创建。初始化后,将执行Postgres配置参数中指定的代码。通常,将加载模块并进行预计算。 如果您在数据库运行时将其添加到配置文件中,请使Postgres重新读取配置。在本文中,示例使用一个模块来可视化数据结构。 有用于分别初始化TRUSTED和UNTRUSTED Perl的参数,当然还有参数那些用Perl编程的人知道,没有它不是一种语言,而是一种误解。

plperl.on_init= 'use Data::Dumper;'
plperl.on_plperl_init= ' ... '
plperl.on_plperlu_init= ' ... '
plperl.use_strict= on


Data::Dumper

use_strict=onstrict

PL / Python


在其中,解释程序是在首次访问时以相同的方式创建的。在这里重要的是立即决定要使用哪个python:第二个还是第三个。如您所知,Python存在两种流行的版本(Python 2和Python 3),但问题在于它们的“笨拙”无法在一个过程中融洽相处:名称上存在冲突。如果您在一个会话中使用v2,然后调用v3,则Postgres将崩溃,对于服务器进程(后端),这将是一个致命错误。要访问其他版本,您需要打开另一个会话。

与Perl不同,无法告知python在初始化期间要做什么。另一个不便之处是:单班轮工作不便。

在所有Python函数中,定义了两个字典-static SD和global GD。全局允许在一个后端内与所有功能交换数据-同时具有吸引力和危险性。每个函数都有一个静态字典。

在PL / Python中,您可以进行子事务,我们将在下面进行讨论。

PL / V8


这是仅受信任的。

方便地,JSON数据会自动转换为JS结构。在PL / V8中,就像在PL / Python中一样,您可以进行子事务。有一个用于简化函数调用的接口。这是唯一可以定义窗口函数的程序语言。他们建议可以在PL / R上定义它们,但是这种语言不在本文的讨论范围之内。

而且只有在PL / V8中,执行超时。的确,默认情况下未打开它,并且如果您手动构建PL / V8,则需要说它在组装期间已打开,然后可以使用配置参数为函数调用设置超时。

PL / V8中的初始化看起来很有趣:由于它是受信任的,因此无法从磁盘读取库,也无法从任何地方加载任何内容。他只能从基地拿走他需要的一切。因此,定义了一个存储的初始化函数,该函数在语言解释器启动时调用。函数名称在特殊的配置参数中指定:

plv8.start_proc=my_init # ( PL/V8-)

在初始化期间,可以通过将全局变量和函数的值分配给此变量的属性来创建它们。例如,像这样:

CREATE OR REPLACE FUNCTION my_init()
RETURNS void LANGUAGE plv8 AS $$
     this.get_57 = function() { return 57; }; //   
     this.pi_square = 9.8696044;  //   
$$;
SET plv8.start_proc = 'my_init';
DO LANGUAGE plv8 $$
     plv8.elog(NOTICE, pi_square, get_57() );
$$;

实践中PL / Perl与PL / Python与PL / V8的比较


你好,世界!


让我们用这三种语言的输出执行一个简单的练习,首先是在PL / Perl中并且让他做其他有用的事情,例如,告诉他的版本:

DO $$
     elog(NOTICE,"Hello World! $]");
$$ LANGUAGE plperl;

NOTICE:  Hello World!
DO

您也可以使用通常的Perl函数warndie

现在在PL / Python中。更确切地说,在PL / Python3u(不受信任)上-用于确定性。

DO $$
     import sys
     plpy.notice('Hello World! ' , hint=" ", detail=sys.version_info)
$$ LANGUAGE plpython3u;


NOTICE:  Hello World! 
DETAIL:  sys.version_info(major=3, minor=6, micro=9, releaselevel='final', serial=0)
HINT:   
DO

可以使用throw 'Errmsg'您可以从Postgres消息中提取很多内容:它们包含提示,详细信息,行号和许多其他参数。在PL / Python中可以传递它们,但不能在考虑中的其他语言中传递它们:它们的含义只能用纯文本行来诅咒。

在PL / Python中,每个postgres日志记录级别都有其自己的功能:NOTICE,WARNING,DEBUG,LOG,INFO,FATAL。如果为ERROR,则表示事务已下降;如果为FATAL,则整个后端均下降。幸运的是,此事尚未解决。你可以在这里阅读

PL / V8

在这种语言中,Hello world与Pearl非常相似。您可以退出exception使用throw,这也将是错误处理,尽管这些工具没有Python中的先进工具。如果你写plv8.elog(ERROR),顺便说一句,效果是一样的。

DO $$
     plv8.elog(NOTICE, 'Hello World!', plv8.version);
$$ LANGUAGE plv8;

NOTICE:  Hello World! 2.3.14
DO

与基地合作


现在,让我们看看如何使用存储过程中的数据库。 Postgres具有SPI(服务器编程接口)。这是一组C函数,所有扩展作者均可使用。几乎所有的PL语言都为SPI提供包装器,但是每种语言都有一些不同。

与PL / PgSQL和其他过程语言相比,用C编写但使用SPI的函数不太可能获得显着收益。但是,绕过SPI并在没有中介的情况下处理数据的C函数(例如table_beginscan/heap_getnext)将加快一个数量级。

PL / Java也使用SPI。但是使用JDBC和JDBC标准仍然需要使用数据库。对于PL / Java中的代码创建者而言,一切都像在客户端应用程序上进行一样,但是JNI(Java本机接口)将对数据库的调用转换为相同的SPI函数。这很方便,并且没有将这个原理转换为PL / Perl和PL / Python的基本障碍,但是由于某种原因,这还没有完成,到目前为止,它在计划中是不可见的。

当然,如果您愿意,可以按照通常的方式-通过DBI或Psycopg前往外国基地。可以到本地数据库,但是为什么。

如果您没有进入整体主题“客户端上的基本流程还是客户端上的流程”,而是立即从最大程度地接近数据进行处理(至少是为了不通过网络驱动大量样本),那么使用存储在服务器上的功能的解决方案就可以了自然。

性能:请记住,SPI有一些开销,函数中的SQL查询可能比没有函数时要慢。第13个postgres包含Konstantin Knizhnik补丁程序,可降低这些成本。但是,当然,在存储的函数中查询结果的处理不需要将结果传输到客户端,因此在性能方面可能是有益的。

安全:一组经过调试和测试的功能可将数据库结构与用户隔离,防止SQL注入和其他恶作剧。否则,这将使每个应用程序开发人员头疼。

代码重用:如果大量复杂的应用程序与数据库一起使用,则将有用的功能存储在服务器上很方便,而不是在每个应用程序中再次编写它们。

我们如何以及以什么形式从数据库中获取数据


Perl中,一切都很简单明了。调用spi_exec_query返回已处理的行数,状态和SQL查询选择的行数组:

DO $$ 
     warn Data::Dumper::Dumper(
          spi_exec_query('SELECT 57 AS x')
     )
$$ LANGUAGE plperl;

WARNING:  $VAR1 = {
          'rows' => [
                    {
                      'x' => '57'
                    }
                  ],
          'processed' => 1,
          'status' => 'SPI_OK_SELECT'
        };

Python中,查询和结果看起来像这样,但是这里的函数并不返回数据结构,而是一个可以用不同方式使用的特殊对象。通常,它伪装成一个数组,因此,您可以从中提取字符串。

DO $$ 
     plpy.notice(
          plpy.execute('SELECT 57 AS x')
     )
$$ LANGUAGE plpython3u;

NOTICE:  <PLyResult status=5 nrows=1 rows=[{'x': 57}]>
DO

现在我们走第一行,离开X并得到值-数字。

DO $$ 
     plpy.notice(
          plpy.execute('SELECT 57 AS x')[0]['x']
      )
$$ LANGUAGE plpython3u;

NOTICE:  57
DO

PL / V8中

DO $$ 
     plv8.elog(NOTICE, JSON.stringify(
          plv8.execute('SELECT 57 as x'))
     );
$$ LANGUAGE plv8;

NOTICE:  [{"x":57}]
DO

为了查看结构,我们使用了库函数JSON.stringify,它不需要专门加载,默认情况下已经准备好用作PL / v8的一部分。

屏蔽层


为避免恶意SQL注入,必须对查询中的某些字符进行转义。为此,首先存在像SPI包装器一样工作的SPI函数和相应的函数(用C编写)。例如,在PL / Perl中:

quote_literal-接受撇号并加倍'和\。设计用于筛选文本数据。
quote_nullable-相同,但undef转换为NULL。
quote_ident-如有必要,引用表或字段的名称。在构造SQL查询并替换其中的数据库对象名称时很有用。

PL / Perl

DO $$
     warn "macy's";
     warn quote_literal("macy's");
$$ LANGUAGE plperl;

WARNING:  macy's at line 2.
WARNING:  'macy''s' at line 3.
DO

请记住:表名不能像文本行一样转义。这就是为什么有一个功能quote_ident

但是在PL / Perl中,还有其他函数可以屏蔽单个post-gres类型的数据: 函数应接受任何类型并将非典型可疑字符转换为显然安全的字符。它适用于大量类型,但是,并非所有类型都适用。例如,她不会理解范围类型,只会将它们简单地理解为文本字符串。

encode_bytea
decode_bytea
encode_array_literal
encode_typed_literal
encode_array_constructor


quote_typed_literal

DO $$
     warn encode_typed_literal(
          ["", " "], "text[]"
     );
$$ LANGUAGE plperl;

WARNING:  {," "} at line 2.
DO

三个类似的功能PL / Python的,他们在几乎相同的方式工作:

plpy.quote_literal
plpy.quote_nullable
plpy.quote_ident


DO $$ plpy.notice(
     plpy.quote_literal("Macy's"));
$$ LANGUAGE plpython3u;
NOTICE:  'Macy''s'
DO

PL / V8中的 功能是否相同

当然!一切都取决于语法功能。

plv8.quote_literal
plv8.quote_nullable
plv8.quote_ident


DO $$
    plv8.elog(NOTICE, plv8.quote_nullable("Macy's"));
$$ LANGUAGE plv8;

NOTICE:  'Macy''s'

性能


哪种语言最快?通常他们回答:C。但是正确的答案是C SQL。为什么要使用SQL?事实是这种语言的功能并非总是显式执行。它可以嵌入到请求中(调度程序会将功能嵌入主请求的主体中),可以根据请求进行优化,并且结果更快。但是在什么条件下可以将代码嵌入请求中?您可以在此处了解一些简单的条件。例如,不应以所有者的权利(为SECURITY DEFINER)执行功能。最简单的功能将适合这些条件。

在本文中,我们将不认真地“量度”膝盖。我们需要一个粗略的比较。首先打开时间:

\timing

让我们尝试一下SQL(以下命令的执行时间是作者在已卸载六岁的PC上获得的四舍五入的平均值。可以将它们相互比较,但它们并不声称是科学的):

SELECT count(*) FROM pg_class;
0.5 ms

它运作非常快。在其他语言中,浪费时间从该语言调用函数。当然,由于解释程序的初始化,第一次请求运行速度会变慢。然后稳定下来。

让我们尝试PL / pgSQL

DO $$
     DECLARE a int;
     BEGIN
          SELECT count(*) INTO a FROM pg_class;
     END;
$$ LANGUAGE plpgsql;
0.7 ms

PL / Perl

DO $$
     my $x = spi_exec_query('SELECT count(*) FROM pg_class');
$$ LANGUAGE plperl;
0.7 ms

PL / Python:

DO $$
     x = plpy.execute('SELECT count(*) FROM pg_class');
$$ LANGUAGE plpythonu;
0.8 ms

是Python2。现在是Python 3(请记住:Python2和Python3在同一个会话中不能和平共处,可能会导致名称冲突):

DO $$
     x = plpy.execute('SELECT count(*) FROM pg_class');
$$ LANGUAGE plpython3u;
0.9ms

最后,PL / V8

DO $$
     var x = plv8.execute('SELECT count(*) FROM pg_class');
$$ LANGUAGE plv8 ;
0.9 ms

但这是非常快的。让我们尝试执行查询1000次或1百万次,突然之间的差异将更加明显:

PL / pgSQL

DO $$
     DECLARE a int; i int;
     BEGIN FOR i IN 0..999999 LOOP
          SELECT count(*) INTO a FROM pg_class;
    END LOOP;
END;
$$ LANGUAGE plpgsql;
53s

PL / Perl

DO $$
     for (0..999999) {
          spi_exec_query('SELECT count(*) FROM pg_class');
     }
$$ LANGUAGE plperl;
102s

PL / Python 3

DO $$
     for i in range (0,1000000) :
          plpy.execute('SELECT count(*) FROM pg_class')
$$ LANGUAGE plpython3u;
98s

PL / V8

DO $$
     for(var i=0;i<1000;i++)
          plv8.execute('SELECT count(*) FROM pg_class');
$$ LANGUAGE plv8;
100ms

请注意,使用PL / V8,该实验以一千次而不是一百万次迭代完成。凭借适度的资源,PL / V8可以在一百万次操作中消耗掉所有内存并完全挂掉汽车。经过一千次迭代,postgres进程选择了3.5GB内存和100%写入磁盘。实际上,postgres启动了V8环境,并且当然会占用内存。执行完请求后,这个涡轮怪物将不会退还内存。要释放内存,您必须结束会话。

我们看到PL / pgSQL已经比PL / Perl和PL / Python快2倍。 PL / V8仍然落后于他们,但在文章结尾时,他已部分康复。

通常,在这些实验中使用Python的Perl显示出大致相同的结果。Perl过去不如Python;在现代版本中,它稍快一些。第三个python慢​​于第二个。整体差异在15%以内。

PREPARE的性能


认识的人会明白:出事了。PL / pgSQL可以自动缓存查询计划,并且在PL / *中,每次再次计划查询时。以一种很好的方式,您需要准备请求,建立一个请求计划,然后根据该计划,它们应根据需要执行多次。在PL / *中,您可以显式使用查询计划,我们将尝试从PL / Perl开始

DO $$
     my $h = spi_prepare('SELECT count(*) FROM pg_class');
     for (0..999999) {
          spi_exec_prepared($h);
     }
     spi_freeplan($h);
$$ LANGUAGE plperl;
60s

PL / Python 3

DO $$
     h = plpy.prepare('SELECT count(*) FROM pg_class')
     for i in range (0,1000000): plpy.execute(h)
$$ LANGUAGE plpython3u;
62s

PL / V8

DO $$
     var h=plv8.prepare('SELECT count(*) FROM pg_class');
     for(var i=0;i<1000;i++) h.execute();
$$ LANGUAGE plv8;
53ms

使用prepare我们的两种语言,我们几乎赶上了PL / pgSQL,而第三种语言也想要,但由于对内存的需求不断增长,未能达到终点。

但是如果您不考虑内存,那么很显然所有语言几乎都是正面交锋-并非偶然。他们的瓶颈现在很常见-通过SPI使用数据库。

计算性能


我们看到语言的性能取决于使用数据库。为了将语言相互比较,让我们尝试计算一些内容而不求助于数据库,例如平方和。

PL / pgSQL

DO $$
     DECLARE i bigint; a bigint;
     BEGIN a=0;
     FOR i IN 0..1000000 LOOP
          a=a+i*i::bigint;
     END LOOP;
END;
$$ LANGUAGE plpgsql;
280ms

PL / Perl

DO $$
     my $a=0;
     for my $i (0..1000000) { $a+=$i*$i; };
     warn $a;
$$ LANGUAGE plperl;
63ms

PL / Python 3

DO $$
a=0
for i in range(1,1000001): a=a+i*i
$$ LANGUAGE plpython3u;
73ms

PL / V8

DO $$
     var a=0;
     for(var i=0;i<=1000000;i++) a+=i*i;
     plv8.elog(NOTICE, a);
$$ language plv8;
7.5ms

我们看到PL / Perl和PL / Python超过并超过了PL / pgSQL,它们快了4倍。八个人都在流泪!但这真的是一无是处吗?还是我们可以得到它?是的,我们会。

JavaScript中的数字是浮点数,结果很快,但不准确:333333833333127550而不是333333833333500000。

这是用于计算确切结果的公式

∑ = n*(n+1)*(2n+1)/6

作为练习,您可以使用数学归纳法证明这一点。

按照笑声的顺序

DO LANGUAGE plv8 $$
plv8.elog(NOTICE, parseInt(33333383333312755033)) $$;

NOTICE:
33333383333312754000

在Javascript中,parseInt它仍然执行浮点运算,而不是Int。

尽管如此,BigInt于2018年出现在V8中,现在可以确定地对其进行计数,但不利于速度,因为它不是64位整数,而是任意位深度的整数。但是,在PL / V8中,这种创新尚未实现。在其他过程语言中,numeric通过特殊的库支持任意位数(SQL的模拟)。

在Perl中,有一个Math :: BigFloat模块,用于以任意精度进行算术;在Python中,Bigfloat围绕GNU MPFRCython包装器

性能功能进行分类


这是一个实际示例,该示例显示了使用不同语言编写的函数按功能排序的性能差异。任务:对包含日记帐发行编号的文本字段进行排序,可能如下:

1
2
3
4-5
6
6A
6
11
12

那些。它实际上是一个字符串,但它以数字开头,您需要按这些数字进行排序。因此,为了正确地按字符串排序,我们在数字部分的左边加零以得到:

0000000001
0000000002
0000000003
0000000004-5
0000000006
0000000006A
0000000006
0000000011
0000000012

是的,我知道这不是解决问题的唯一方法(甚至还不是很正确)。但是,例如,它将做到。

要请求类型,SELECT ... ORDER BY nsort(n)我们在PL / Perl,SQL,PL / Python和PL / V8中编写了将日记帐号转换为以下形式的函数:

CREATE OR REPLACE FUNCTION nsort(text) RETURNS text 
   LANGUAGE PLPERL IMMUTABLE AS $$
    my $x = shift;
    return ($x =~ /^\s*(\d+)(.*)$/)
        ? sprintf("%010d", $1).$2
        : $x;
$$;

CREATE OR REPLACE FUNCTION _nsort(x text) RETURNS text
     LANGUAGE SQL  IMMUTABLE  AS $$
 WITH y AS (
    SELECT regexp_match(x,'^\s*(\d*)(.*)$') as z
 )
 SELECT CASE WHEN z[1] = '' THEN x ELSE lpad(z[1],10,'0') || z[2] END FROM y;
$$;

CREATE OR REPLACE FUNCTION py_nsort(x text) RETURNS text 
   LANGUAGE plpython2u IMMUTABLE AS $$
import re
r = re.match('^\s*(\d+)(.*)$', x)
return x if r == None else ('%010d' % int(r.group(1))) + r.group(2)
$$;

CREATE OR REPLACE FUNCTION js_nsort(x text) RETURNS text 
   LANGUAGE plv8 IMMUTABLE AS $$
var m = x.match(/^\s*(\d+)(.*)$/);
if(m) { return m[1].padStart(10-m[1].length,'0') + m[2]; }
else { return x; } 
$$;

在我的1.55万本期刊文章库中,使用PL / Perl中的函数进行查询大约需要64毫秒,而PL / Python中为120毫秒,而PL / PgSQL中为200毫秒。但最快-PL / v8:54ms。

注意:尝试排序时,请提供必要的工作内存量,以便排序进入内存(然后将显示“ EXPLAIN” Sort Method: quicksort)。内存量由参数设置work_mem

set work_mem = '20MB';

记忆


Perl不喜欢环状结构;他不知道如何清理它们。如果您a有一个指向b和的b指针a,则该参考计数器将永远不会被重置,并且内存也不会被释放。

垃圾回收语言还有其他问题。例如,何时释放内存还是根本不释放内存是未知的。或者-如果您不小心处理此问题-收集者将在最不适当的时刻去收集垃圾。

但是,还有一些与Postgres直接相关的内存管理功能。有SPI分配的结构,Perl并不总是意识到它们需要被释放。

PL / Perl

并非如此:

CREATE OR REPLACE function cr()
RETURNS int LANGUAGE plperl AS
$$
     return spi_exec_query(
           'SELECT count(*) FROM pg_class'
     )->{rows}->[0]->{count};
$$;

因此,它去了:

CREATE OR REPLACE function cr()
RETURNS int LANGUAGE plperl AS
$$
     my $h = spi_prepare(
          'SELECT count(*) FROM pg_class'
     );
     return spi_exec_prepared($h)->{rows}->[0]->{count};
$$;

执行后,处理程序$h将保持活动状态,尽管事实上并没有与之保持任何联系的事实。

没关系,您只需要记住需要使用以下命令显式释放资源spi_freeplan($h)

CREATE OR REPLACE function cr()
RETURNS int LANGUAGE plperl AS
$$
     my $h = spi_prepare(
          'select count(*) from pg_class'
     );
     my $res = spi_exec_prepared($h)->{rows}->[0]->{count};
     spi_freeplan($h);
     return $res;
$$;

PL / Python:

Python 永不流行,该计划会自动发布:

CREATE OR REPLACE function cr3() RETURNS int
LANGUAGE plpythonu as
$$
     return plpy.execute(
           'select count(*) from pg_class'
     )[0]['count']
$$;

PL / V8

与Perl的故事相同。它不会像这样流动:

CREATE OR REPLACE FUNCTION crq() RETURNS int
LANGUAGE plv8 AS
$$
     return plv8.execute(
          'select count(*) from pg_class‘
     )[0].count;
$$;

因此,它去了:

CREATE OR REPLACE FUNCTION crq() RETURNS int
LANGUAGE plv8 AS
$$
     var h = plv8.prepare(
          'select count(*) from pg_class'
     );
     return h.execute()[0].count;
$$;

再说一遍:不要忘记释放资源。在这里它。h.free();

它不流:

CREATE OR REPLACE FUNCTION crq() RETURNS int
LANGUAGE plv8 AS
$$
     var h = plv8.prepare(
          'select count(*) from pg_class'
     );
     var r = h.execute()[0].count;
     h.free();
     return r;
$$;

参量


现在是时候了解如何将参数传递给函数了。在示例中,我们将4个带有类型的参数传递给函数:

  • 整个;
  • 数组;
  • bytea和
  • jsonb

他们如何进入PL / Perl

CREATE OR REPLACE FUNCTION crq(a int, b
bytea, c int[], d jsonb ) RETURNS void
LANGUAGE plperl AS
$$
    warn Dumper(@_);
$$;

SELECT crq(1,'abcd', ARRAY[1,2,3],'{"a":2,"b":3}');


WARNING:  $VAR1 = '1';
$VAR2 = '\\x61626364';
$VAR3 = bless( {
                 'array' => [
                              '1',
                              '2',
                              '3'
                            ],
                 'typeoid' => 1007
               }, 'PostgreSQL::InServer::ARRAY' );
$VAR4 = '{"a": 2, "b": 3}';
 crq 
-----
(1 row)

是JSON还是JSONB-在这种情况下,没有区别:它们仍然以字符串形式出现。这是多功能性的费用:Postgres有很多类型,不同程度的“嵌入”。要求开发人员立即使用新类型为所有PL / *提供转换功能,这将太多了。默认情况下,许多类型作为字符串传递。但这并不总是很方便,您必须解析这些术语。当然,我希望Postgres数据能立即变成适当的Perl结构。默认情况下不会发生这种情况,但是从9.6开始,出现了TRANSFORM机制-定义类型转换函数的能力:CREATE TRANSFORM

要创建TRANSFORM,您需要用C编写两个函数:一个将某种类型的数据转换为一侧,另一侧转换为另一种。请注意,TRANSFORM在四个地方起作用:

  • 将参数传递给函数时;
  • 返回函数值时;
  • 将参数传递给函数内部的SPI调用时;
  • 收到函数内部SPI调用的结果后。

由Anton Bykov开发的针对Perl和Python的TRANSFORM JSONB,出现在Postgres的第11版中。现在,您无需解析JSONB,它就会作为相应的结构立即进入Perl。您必须创建jsonb_plperl扩展,然后才能使用TRANSFORM:

CREATE EXTENSION jsonb_plperl;
CREATE OR REPLACE FUNCTION crq2(d jsonb)
RETURNS void LANGUAGE plperl
TRANSFORM FOR TYPE jsonb AS $$
     warn Dumper(@_);
$$;

您可以调用此函数来验证JSONB已变成珍珠哈希:

SELECT crq2( '{"a":2,"b":3}');


WARNING:  $VAR1 = {
          'a' => '2',
          'b' => '3'
        };
 crq2 
------
(1 row)

完全不同的事情!

本文的作者还参与了TRANSFORM的开发。原来,这种简单的数据类型boolean以不方便的形式传递给PL / Perl,例如文本字符串't''f'。但是按照Perl的理解,字符串'f'是正确的。为了消除不便,发明一个补丁,该补丁定义了Boolean类型的转换。该补丁已在PostgreSQL 13上发布,即将发布。由于其简单性,bool_plperl可以用作编写任何其他转换的最小启动模型。

我希望有人能为其他数据类型(字节,数组,日期,数字)开发TRANSFORM。

现在,让我们看看如何在Python中传递参数

CREATE EXTENSION jsonb_plpython3u;
CREATE OR REPLACE FUNCTION pdump(a int, b bytea, c int[], d jsonb ) RETURNS void
LANGUAGE plpython3u
TRANSFORM FOR TYPE jsonb AS $$
      plpy.warning(a,b,c,d)
$$;

SELECT pdump(1,'abcd', ARRAY[1,2,3],'{"a":2,"b":3}');


WARNING:  (1, b'abcd', [1, 2, 3], {'a': Decimal('2'), 'b': Decimal('3')})
 pdump 
-------
(1 row)

将数组转换为数组-很好(因为PG10版本多维数组也已正确传输到python)。在Perl中,数组被转换为特殊类的对象。好吧,jsonb变了。如果没有TRANSFORM,则jsonb将作为字符串传递。

现在,让我们看看参数以什么形式进入JS

CREATE OR REPLACE FUNCTION jsdump(a int, b bytea, c int[], d jsonb) RETURNS void
LANGUAGE plv8 AS $$
     plv8.elog(WARNING,a,b,c,d)
$$;

SELECT jsdump(1,'abcd', ARRAY[1,2,3],'{"a":2,"b":3}');


WARNING:  1 97,98,99,100 1,2,3 [object Object]
jsdump 
-------
(1 row)

JSONB无需任何转换即可转换为JavaScript对象!临时Postgres类型也将转换为Date JS类型。布尔值也一样。所有转换都已内置到PL / V8中。

无限工作


INFINITY常量并不经常使用,但是随便使用它很危险。在PostgreSQL中,对于某些临时和浮点类型,Infinity和-Infinity作为特殊值存在。但是必须详细讨论将Infinity转换为过程语言的过程,反之亦然,因为使用它们不仅可以依赖于语言,还可以依赖于库,操作系统甚至是硬件。

Python有一个定义数字无穷大的Numpy模块:

import numpy as nm
a = nm.inf
b = -nm.inf
print(a, b)

inf -inf

Perl也具有无穷大,它使用"infinity"可以缩短为的字符串"inf"例如,您可以说:

perl -e 'print 1 * "inf"'

Inf

要么

perl -e 'print 1/"inf"'

0

在PL / Perl,PL / Python,PL / v8中,可以正确传递Postgres的数值无穷大,但无限日期不太正确。相反,在PL / Perl和PL / Python中,没有用于时间的内置数据类型,而是一个字符串。在PL / V8中,有一个内置类型Date,而postgres中的通常日期变成了它。但是V8不知道无休止的日期,当转移时,它变成Invalid Date

将参数传递给准备好的请求


返回prepare,考虑如何将参数传递到那里。不同的语言有很多共同点,因为它们都是基于SPI的。

PL / Perl中准备查询时,需要确定要传递的参数的类型,并且在运行查询时,只需指定这些参数的值(这些参数以相同的方式传递给PL / pgSQL)。

DO LANGUAGE plperl $$
     my $h= spi_prepare('SELECT * FROM pg_class WHERE
          relname ~ $1', 'text' );                     #   
     warn Dumper(spi_exec_prepared($h, 'pg_language')); #   
     spi_freeplan($h);
$$;

PL / Python中,本质是相同的,但是语法略有不同:

DO LANGUAGE plpython3u $$
     h= plpy.prepare('SELECT relname FROM pg_class WHERE relname ~ $1', ['text'] )
     plpy.notice(.execute (['pg_language']))
$$;

PL / V8中,差异很小:

DO LANGUAGE plv8 $$
    var h= plv8.prepare('SELECT relname FROM pg_class WHERE relname ~ $1', ['text'] );
    plv8.elog(NOTICE, h.execute (['pg_language']));
    h.free();
$$;

PL / Java中,一切都不同。在那里,显然不使用SPI,但是形成了到数据库的伪JDBC连接。对于PL / Java程序员来说,一切都像他在创建客户端应用程序一样。这很方便,也可以进行PL / Perl和PL / Python的设计,但是由于某种原因,它没有完成(但是,没有人禁止创建更多PL / Perl和PL / Python的实现)。

使用游标


我们进入数据库时​​使用的所有SPI函数spi_exec_query()以及其他函数都有一个参数,该参数限制返回的行数。如果您需要大量返回的行,那么就不能没有光标将它们向上拉一点。

游标使用所有这些语言。PL / Perl中,
spi_exec_query返回一个游标,您可以一次从中提取字符串。不必关闭游标;它会自行关闭。但是,如果要重新发现它,可以使用command显式关闭它close()

DO LANGUAGE plperl $$
    my $cursor = spi_query('SELECT * FROM pg_class');
    my $row;
    while(defined($row = spi_fetchrow($cursor))) {
         warn $row->{relname};
    }
$$;

WARNING:  pg_statistic at line 5.
WARNING:  pg_toast_2604 at line 5.
WARNING:  pg_toast_2604_index at line 5.
WARNING:  pg_toast_2606 at line 5.
WARNING:  pg_toast_2606_index at line 5.
WARNING:  pg_toast_2609 at line 5.
WARNING:  pg_toast_2609_index at line 5.
...

PL / Python中,所有内容都非常相似,但是将光标显示为可以循环浏览的对象:

h = plpy.prepare('SELECT ...');
cursor = plpy.cursor(h);
for row in cursor:
...
cursor.close() //  

PL / v8中,所有内容也非常相似,但不要忘记释放准备好的查询计划:

var h = plv.prepare('SELECT ...');
var cursor = h.cursor();
var row;
while(row = cursor.fetch()) {
...
}
cursor.close();
h.free();

PL / V8:快速访问功能


在PL / V8中,您可以从常规SELECT中调用一个函数,而不必按名称查找它,然后立即使用启动它plv8.find_function(name);但是请记住,在JS中,函数不能像PostgreSQL中那样是多态的,在PostgreSQL中,具有相同名称但具有不同参数的函数可以共存。当然,在PL / v8中,我们可以创建多态函数,但是在尝试使用find_function会出错。

ERROR:  Error: more than one function named "jsdump"

如果名称上的函数是明确的,则可以在不进行SPI和类型转换的情况下调用该函数,即 快多了。例如,像这样:

DO LANGUAGE plv8 $$
plv8.find_function('jsdump')(1, 'abc');
$$;

交易次数


Postgres 11有很多乐趣:真正的程序已经出现Postgres过去仅具有功能。快乐的原因不仅在于与SQL标准的兼容性和合规性,还在于原因:在过程中您可以提交和回滚事务。

PL / Perl和PL / Python已经具有用于管理事务的SPI功能,而PL / V8还没有。在PL / Perl中,这些函数被称为spi_commit()spi_rollback(),并且在文档中提供了使用示例在PL / Python中,这是plpy.commit()plpy.rollback()

子交易


子事务在复杂的多级逻辑中方便进行正确的错误处理。

事务内部的PL / pgSQL中,每个带有EXCEPTION关键字的块都是一个子事务。您可以阅读有关在这种情况下可能出现的一些性能和可靠性问题,例如,在此处PL / Perl

没有显式的子事务,但是可以通过savaepoints模拟它们。显然,如果您愿意,可以很容易地编写一个Pearl模块,该模块以显式形式实现子事务。PL / Python中,子交易早就出现了:从9.5 显式开始,在此之前有隐式交易。您可以定义一个事务,将其包装

try-并执行。如果子交易失败,那么我们就落入该区块except;如果它没有下跌,则就进入该区块else并继续前进。

try:
     with plpy.subtransaction():
          plpy.execute("...")
          plpy.execute("...")
except plpy.SPIError, e:
. . .
else:
. . .

PL / V8中 存在类似的设计,仅使用JS语法。

try {
plv8.subtransaction(function() {
plv8.execute('UPDATE...');
plv8.execute('UPDATE...');
});
}
catch(e) {
...
}

结论


尝试,但不要滥用:)了解PL / *可以带来一些好处。像任何工具一样,他们喜欢被用于预期的目的。

PL / v8非常有前景,但是有时它的行为异常并且存在许多问题。因此,如果适合您的任务,最好将语言开箱即用。

我要感谢Igor Levshin(伊戈尔),他为我准备本文的材料提供了很多帮助,并提出了一些有用的想法,还有叶夫根尼·谢尔盖耶夫(Evgeny Sergeev)和阿列克谢·法德耶夫(Alexey Fadeev)提出了一些更正建议。

All Articles