PostgreSQL: Programação do lado do servidor na linguagem humana (PL / Perl, PL / Python, PL / v8)

O Postgres é conhecido por sua extensibilidade, que também se aplica ao suporte a linguagens procedurais (PL). Ninguém pode se gabar de um idioma com uma lista de idiomas desse tamanho, e potencialmente essa lista não é de todo limitada: para conectar o idioma ao servidor, não é necessário nenhum esforço extra. Você pode até criar seu próprio idioma e torná-lo um idioma processual do servidor. Alterações no DBMS não exigirão isso. Como muito mais, essa extensibilidade foi incorporada à arquitetura do Postgres desde o início.

É possível e às vezes necessário escrever linguagens PL para tarefas. Melhor ainda, se alguém escrever uma estrutura para escrever linguagens para que você possa escrever não em C, mas para escolher uma linguagem que seja mais confortável para um desenvolvedor de linguagem. Como no FDW, que pode ser escrito em Python .

Este artigo foi escrito com base em vários relatórios e master classes sobre esse tópico, elaborados pelo autor nas conferências PgConf.Russia 2019 , PgConf.Russia 2018 e DevConf 2017 .

Não se trata de exotismo, mas das linguagens de procedimentos mais comuns PL / Perl, PL / Python e PL / V8 (ou seja, JavaScript) e comparação de seus recursos com PL / pgSQL.

Quando vale a pena usar esses idiomas? Quando o SQL e o PL / pgSQL estão ausentes?

  • Então, quando você precisar trabalhar com estruturas complexas, com algoritmos: atravessando árvores, por exemplo, ou quando a análise de HTML ou XML for necessária, especialmente ao extraí-las dos arquivos;
  • Quando você precisa gerar dinamicamente SQL complexo (relatórios, ORM). No PL / pgSQL, não é apenas inconveniente, mas também funciona mais devagar em alguns casos;
  • Perl Python, C/C++, Perl Python . . , Oracle. , Postgres . Perl Python .
  • — . , , untrusted- ( — . ), Perlu Python(3)u, PL/V8. Postgres , , FDW, , . . !
  • E mais uma coisa: se você escrever algo em C, poderá criar um protótipo nessas linguagens que sejam mais adaptadas ao desenvolvimento rápido.

Como incorporar um idioma no Postgres


Para implementar o idioma que você precisa: escreva em C de uma a três funções:

  • HANDLER - um manipulador de chamadas que executará uma função no idioma (esta é uma parte necessária);
  • INLINE - manipulador de blocos anônimos (se você quiser que o idioma suporte blocos anônimos);
  • VALIDADOR - função de verificação de código ao criar uma função (se você deseja que essa verificação seja feita).

Isso é descrito em detalhes na documentação aqui e aqui .

"Idiomas prontos para uso" e outros idiomas


Existem apenas quatro idiomas suportados "prontos para uso": PL / pgSQL , PL / Perl , PL / Python e PL / Tcl , mas o cócegas é um tributo à história: poucas pessoas o usam agora, não falamos mais sobre isso.
PL / Perl, PL / Python e, é claro, PL / pgSQL são suportados pela comunidade Postgres. O suporte para outros idiomas que não são da caixa é dos mantenedores - empresas, comunidades ou desenvolvedores específicos interessados ​​em fazer o idioma funcionar dentro do DBMS. PL / V8 promove o Google. Mas de tempos em tempos existem razõesduvide do futuro sem nuvens do PL / V8. O atual mantenedor do projeto PL / V8 do Google, Jerry Sievert, está considerando o suporte a JS baseado em servidor postgres com base em um mecanismo diferente (como o QuickJS), pois o PL / V8 é difícil de construir e requer 3-5 GB todo tipo de coisa no Linux durante a construção, e isso geralmente leva a problemas em diferentes sistemas operacionais. Mas o PL / V8 é amplamente utilizado e exaustivamente testado. É possível que PL / JS apareça como uma alternativa a outro mecanismo JS, ou por enquanto apenas como um nome, ao qual nos acostumaremos durante o período de transição.

PL / Java raramente é usado. Eu, pessoalmente, não precisava escrever em PL / Java, porque no PL / Perl e no PL / V8 há funcionalidade suficiente para quase todas as tarefas. Até o Python não adiciona recursos em particular. PL / RÚtil para quem gosta de estatísticas e ama este idioma. Também não vamos falar sobre ele aqui.

As linguagens populares não são necessariamente populares nos armazenamentos de gravação: existe o PL / PHP, mas agora praticamente não é suportado por ninguém - há poucos que desejam escrever procedimentos de servidor. Para a linguagem PL / Ruby, a imagem é de alguma forma a mesma, embora a linguagem pareça ser mais moderna.

Uma linguagem procedural baseada em Go está sendo desenvolvida, veja PL / Go e, ao que parece, PL / Lua . Será necessário estudá-los. Para os fãs teimosos da concha, há até PL / Sh , é difícil imaginar o que possa ser.

Há pelo menos uma linguagem procedural específica de domínio (DSL) que é estritamente especializada para sua tarefa - PL / Proxy, que costumava ser muito popular para proxy e balanceamento de carga do servidor.

Neste artigo, abordaremos os principais idiomas mais usados. Obviamente, isso é PL / PgSQL, PL / Perl, PL / Python e PL / V8, os chamaremos de PL / * abaixo .

Os idiomas “prontos para o uso” são realmente quase literalmente instalados prontos para uso - geralmente a instalação é simples. Mas para instalar o PL / V8, se você não encontrou um pacote com a versão necessária no repositório do seu sistema operacional, isso é quase uma façanha, pois para isso você precisará criar o V8 inteiro ou, em outras palavras, o Chromium. Ao mesmo tempo, toda a infraestrutura de desenvolvimento será baixada do google.com juntamente com a própria V8 - conte com alguns gigabytes de tráfego. Para o Postgres 11 no Ubuntu, o pacote PL / V8 ainda não apareceu, apenas o V8 para PG 10 está disponível no repositório até o momento.Se desejar, monte-o manualmente. Também é importante que a versão que você encontrará no repositório provavelmente seja bastante antiga. No momento da publicação do artigo, a versão mais recente é 2.3.14.

Após a instalação do idioma, você também deve "criar" o idioma - registre-o no diretório do sistema. Isso deve ser feito pela equipe.

CREATE EXTENSION plperl;

(em vez de plperl, você pode substituir o nome de outro idioma, existem algumas nuances, veja abaixo).
Nós olhamos para o que aconteceu:

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       |

O PL / pgSQL não precisa ser criado especialmente, pois sempre está no banco de dados.

Atenção! PL / pgSQL não deve ser confundido com SQL. Este é um idioma diferente. No entanto, o Postgres também pode escrever funções em SQL simples.

Padrões


No mundo do DBMS, eles costumam falar sobre conformidade com os padrões SQL. As linguagens processuais também têm padrões, embora não sejam discutidas com tanta frequência. O padrão SQL / PSM é altamente compatível com a linguagem processual do DB2. Sua implementação está longe de PL / pgSQL, embora conceitualmente eles estejam próximos.

SQL / JRT é o padrão para procedimentos Java e PL / Java é uma boa combinação.

Idiomas confiáveis ​​e não confiáveis


As linguagens procedurais do Postgres são confiáveis ​​(CONFIÁVEIS) e não confiáveis ​​(NÃO CONFIÁVEIS).
Em idiomas CONFIÁVEIS, não há possibilidade de trabalho direto com E / S, incluindo a rede e, de fato, com os recursos do sistema. Portanto, essas funções podem ser criadas por qualquer usuário do banco de dados, estragar alguma coisa e ele não poderá aprender muito. Funções em idiomas não confiáveis ​​só podem ser criadas por um supervisor.

Se o intérprete de idiomas suportar essas restrições, ele poderá ser usado para criar os idiomas TRUSTED e UNTRUSTED. Então, com o Perl, existem diferentes idiomas plperle plperlu. Letra uno final, revela o caráter não confiável da língua. O Python existe apenas em uma versão não confiável. PL / v8 - pelo contrário, apenas em confiável. Como resultado, o PL / v8 não pode carregar nenhum módulo ou biblioteca do disco, apenas do banco de dados.

Uma função no idioma NÃO CONFIÁVEL pode fazer qualquer coisa: envie um email, execute ping em um site, faça login em um banco de dados externo e execute uma solicitação HTTP. Os idiomas confiáveis ​​são limitados ao processamento de dados do banco de dados.

Pelo Trusted incluem: plpgsql, plperl, plv8, pljava.

Por NÃO CONFIÁVEL incluem: plperlu, pljavau, plpython2u, plpython3u.

Observe: o PL / Python não existe como CONFIÁVEL (já que você não pode definir restrições no acesso aos recursos) e PLpgSQL e PL / V8 são o contrário: eles não são CONFIÁVEIS.

Mas Perl e Java estão disponíveis nas duas versões.

PL / pgSQL vs PL / *


O código PL / pgSQL funciona nativamente com todos os tipos de dados que o Postgres possui. Outros idiomas não possuem muitos tipos de Postgres, e o intérprete de idiomas se encarrega de converter os dados em uma representação interna do idioma, substituindo os tipos obscuros pelo texto. No entanto, ele pode ser ajudado com a ajuda do TRANSFORM, sobre o qual falarei mais perto do final do artigo.

As chamadas de função no PL / pgSQL costumam ser mais caras. Funções em outros idiomas podem acessar suas bibliotecas sem consultar o catálogo do sistema. O PL / pgSQL não pode funcionar assim. Algumas consultas no PL / pgSQL funcionam por um longo tempo devido ao fato de muitos tipos serem suportados: para adicionar dois números inteiros, o intérprete precisa perceber que ele está lidando com números inteiros e não com outros tipos exóticos, e depois decide como dobrá-los e somente depois disso os dobrará.

Como PL / pgSQL é CONFIÁVEL, você não pode trabalhar com a rede e os discos dela.

Quando se trata de trabalhar com estruturas de dados aninhadas, o PL / pgSQL possui apenas ferramentas Postgres para trabalhar com JSON, que são muito complicadas e improdutivas. Em outras linguagens, trabalhar com estruturas aninhadas é muito mais simples e econômico.

O PL / * possui seu próprio gerenciamento de memória e você precisa monitorar a memória, ou talvez limitá-la.

Você deve monitorar cuidadosamente o tratamento de erros, que também é diferente para todos.

Mas no PL / * existe um contexto global de intérpretes e pode ser usado, por exemplo, para armazenar dados em cache, incluindo planos de consulta. Se o idioma não for confiável, a rede e as unidades estarão disponíveis. Todos esses idiomas funcionam com o banco de dados, via de regra, através do SPI, mas mais sobre isso posteriormente.

Vamos considerar em mais detalhes os recursos dos idiomas PL / *.

PL / Perl


O interpretador Perl é um grande pedaço de código na memória, mas, felizmente, ele não é criado quando a conexão é aberta, mas apenas quando o primeiro procedimento / função armazenada PL / Perl é iniciado. Quando é inicializado, o código especificado nos parâmetros de configuração do Postgres é executado. Normalmente, os módulos são carregados e as pré-computações são feitas. Se você adicionou ao arquivo de configuração enquanto o banco de dados está em execução, faça o Postgres reler a configuração. Neste artigo, os exemplos usam um módulo para visualizar estruturas de dados. Existem parâmetros para inicialização separada de TRUSTED e UNTRUSTED Perl e, é claro, um parâmetro . Quem programa em Perl sabe que sem ela não é uma linguagem, mas um mal-entendido.

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


Nele, o intérprete é criado da mesma maneira na primeira vez que é acessado. E aqui é importante decidir imediatamente qual python você deseja: segundo ou terceiro. Como você sabe, o Python existe em duas versões populares (Python 2 e Python 3), mas o problema é que seus so-shki não se dão bem em um único processo: existe um conflito pelo nome. Se você trabalhou com a v2 em uma sessão e depois chamou a v3, o Postgres falhará e, para o processo do servidor (back-end), esse será um erro fatal. Para acessar uma versão diferente, você precisa abrir outra sessão.

Diferentemente do Perl, não é possível dizer ao python o que fazer durante a inicialização. Outro inconveniente: os liners únicos são inconvenientes.

Em todas as funções do Python, dois dicionários são definidos - estático SDe global GD. Global permitetroque dados com todas as funções em um back-end - o que é atraente e perigoso ao mesmo tempo. Cada função possui um dicionário estático.

No PL / Python, você pode fazer subtransações, as quais discutiremos abaixo.

PL / V8


É apenas confiável.

Convenientemente, os dados JSON são convertidos automaticamente em uma estrutura JS. No PL / V8, como no PL / Python, você pode fazer subtransações. Existe uma interface para chamadas de funções simplificadas. Essa é a única linguagem processual em questão na qual as funções da janela podem ser definidas . Eles sugerem que eles podem ser definidos no PL / R , mas esse idioma está fora do escopo deste artigo.

E somente no PL / V8 há um tempo limite de execução. É verdade que não está ativado por padrão, e se você criar o PL / V8 manualmente, precisará dizer que ele foi ativado durante a montagem e, em seguida, você pode definir tempos limite para chamadas de função com o parâmetro de configuração.

A inicialização no PL / V8 parece interessante: como é confiável, não pode ler a biblioteca do disco, não pode carregar nada de qualquer lugar. Ele pode pegar tudo o que precisa apenas da base. Portanto, é definida uma função inicializada armazenada, chamada quando o intérprete de idioma é iniciado. O nome da função é especificado em um parâmetro de configuração especial:

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

Durante a inicialização, variáveis ​​e funções globais podem ser criadas atribuindo seus valores aos atributos dessa variável. Por exemplo, assim:

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() );
$$;

Comparação de PL / Perl vs PL / Python vs PL / V8 na prática


Olá Mundo!


Vamos executar um exercício simples com o resultado desta frase nos três idiomas, primeiro no PL / Perl . E deixe que ele faça outra coisa útil, por exemplo, conta sua versão:

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

NOTICE:  Hello World!
DO

Você também pode usar as funções Perl habituais warne die.

Agora em PL / Python . Mais precisamente no PL / Python3u (não confiável) - para maior certeza.

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

Pode usar throw 'Errmsg'. Há muitas coisas que você pode extrair das mensagens do Postgres: elas contêm Dica, Detalhes, número da linha e muitos outros parâmetros. No PL / Python, eles podem ser passados, mas não nos outros idiomas em consideração: seus meios só podem ser amaldiçoados com uma linha de texto simples.

No PL / Python, cada nível de log do postgres tem sua própria função: AVISO, AVISO, DEBUG, LOG, INFO, FATAL. Se for ERRO, a transação caiu, se FATAL, todo o back-end caiu. Felizmente, o assunto não chegou ao pânico. Você pode ler aqui .

PL / V8

Nesse idioma, o Hello world é muito parecido com o pearl. Você pode parar de exceptionusar throw, e isso também será tratamento de erros, embora as ferramentas não sejam tão avançadas quanto no Python. Se você escreverplv8.elog(ERROR), o efeito será, a propósito, o mesmo.

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

NOTICE:  Hello World! 2.3.14
DO

Trabalhar com a base


Agora vamos ver como trabalhar com um banco de dados a partir de procedimentos armazenados. O Postgres possui uma SPI (Server Programming Interface). Este é um conjunto de funções C que está disponível para todos os autores de extensões. Quase todas as linguagens PL fornecem wrappers para SPI, mas cada linguagem o faz de maneira um pouco diferente.

É improvável que uma função escrita em C, mas usando SPI, proporcione um ganho significativo em comparação com PL / PgSQL e outras linguagens de procedimentos. Mas uma função C que ignora o SPI e trabalha com dados sem intermediários (por exemplo table_beginscan/heap_getnext) funcionará uma ordem de magnitude mais rapidamente.

PL / Java também usa SPI. Mas trabalhar com o banco de dados ainda acontece no estilo do JDBC e no padrão JDBC. Para o criador do código em PL / Java, tudo acontece como se você estivesse trabalhando em um aplicativo cliente, mas o JNI (Java Native Interface) converte chamadas para o banco de dados nas mesmas funções SPI. É conveniente e não há obstáculos fundamentais para traduzir esse princípio em PL / Perl e PL / Python, mas por alguma razão isso não foi feito e, até o momento, não é visível nos planos.

Obviamente, se desejar, você pode ir a bases estrangeiras da maneira usual - através do DBI ou Psycopg . É possível banco de dados local, mas por quê.

Se você não entrar no tópico holístico "processo na base x processo no cliente" e prosseguir imediatamente do processamento máximo mais próximo dos dados (pelo menos para não direcionar amostras gigantes pela rede), a solução para usar as funções armazenadas no servidor será naturalmente.

Desempenho : lembre-se de que o SPI possui alguma sobrecarga e as consultas SQL nas funções podem ser mais lentas do que sem funções. O 13º postgres incluiu um patch de Konstantin Knizhnik , que reduz esses custos. Mas, é claro, o processamento dos resultados da consulta em uma função armazenada não requer a transferência do resultado para o cliente e, portanto, pode ser benéfico em termos de desempenho.

Segurança: um conjunto de funções depuradas e testadas isola a estrutura do banco de dados do usuário, protege contra injeções de SQL e outras travessuras. Caso contrário, continuará sendo uma dor de cabeça para todos os desenvolvedores de aplicativos. Reutilização de

código : se um grande número de aplicativos complexos funcionar com o banco de dados, é conveniente armazenar funções úteis no servidor, em vez de escrevê-las novamente em cada aplicativo.

Como e de que forma obtemos dados do banco de dados


No Perl , tudo é simples e claro. A chamada spi_exec_queryretorna o número de linhas processadas, o status e a matriz de linhas selecionadas pela consulta 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'
        };

No Python, a consulta e o resultado se parecem com isso, mas aqui a função não retorna uma estrutura de dados, mas um objeto especial com o qual você pode trabalhar de maneiras diferentes. Geralmente, ele finge ser uma matriz e, consequentemente, você pode extrair cadeias de caracteres.

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

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

E agora pegamos a 1ª linha, saímos de lá X e obtemos o valor - o número.

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

NOTICE:  57
DO

Em PL / V8 :

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

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

Para ver a estrutura, usamos a função de biblioteca JSON.stringify, que não precisa ser carregada especialmente, ela já está pronta para uso como parte do PL / v8 por padrão.

Blindagem


Para evitar injeções SQL maliciosas, alguns caracteres nas consultas devem ser escapados. Para fazer isso, em primeiro lugar, existem funções SPI e funções correspondentes (escritas em C) em idiomas que funcionam como wrappers SPI. Por exemplo, em PL / Perl:

quote_literal- recebe apóstrofos e dobra 'e \. Projetado para a triagem de dados de texto.
quote_nullable- mesmo, mas undefconvertido para NULL.
quote_ident- cita o nome da tabela ou campo, se necessário. Útil no caso em que você está construindo uma consulta SQL e substituindo os nomes dos objetos de banco de dados nela.

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

Lembre-se: o nome da tabela não deve ser escapado como uma linha de texto. É por isso que existe uma função quote_ident.

Mas no PL / Perl existem outras funções para escapar dos dados de tipos individuais de postagens: Uma função deve aceitar qualquer tipo e transformar caracteres duvidosos atípicos em algo obviamente óbvio. Funciona com um grande número de tipos, mas, no entanto, não com todos. Ela, por exemplo, não entenderá os tipos de intervalo e os perceberá simplesmente como cadeias de texto.

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

Existem três funções semelhantes no PL / Python , e elas funcionam da mesma maneira:

plpy.quote_literal
plpy.quote_nullable
plpy.quote_ident


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

As funções do PL / V8 são iguais ?

Claro! Tudo é o mesmo até características sintáticas.

plv8.quote_literal
plv8.quote_nullable
plv8.quote_ident


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

NOTICE:  'Macy''s'

atuação


Qual idioma é o mais rápido? Geralmente eles respondem: C. Mas a resposta correta é C ou SQL. Por que SQL? O fato é que uma função nesse idioma nem sempre é executada explicitamente. Ele pode ser incorporado à solicitação (o agendador incorporará a função no corpo da solicitação principal), otimizará bem a solicitação e o resultado será mais rápido. Mas sob que condições o código pode ser incorporado em uma solicitação? Existem algumas condições simples que você pode ler sobre, digamos, aqui . Por exemplo, uma função não deve ser executada com os direitos do proprietário (para ser DEFINER DE SEGURANÇA). A maioria das funções simples se encaixa nessas condições.

Neste artigo, mediremos "no joelho", não seriamente. Precisamos de uma comparação aproximada. Primeiro ligue o tempo:

\timing

Vamos tentar o SQL (os tempos de execução dos comandos abaixo são os valores médios arredondados que o autor recebeu em um PC de seis anos sem carga. Eles podem ser comparados entre si, mas não afirmam ser científicos):

SELECT count(*) FROM pg_class;
0.5 ms

Funciona muito rápido. Em outros idiomas, é desperdiçado tempo chamando funções do idioma. Obviamente, na primeira vez em que a solicitação for executada mais lentamente, devido à inicialização do intérprete. Então estabiliza.

Vamos tentar o 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

Era o Python 2. Agora, o Python 3 (lembre-se: Python2 e Python3 não vivem em paz na mesma sessão, é possível um conflito de nomes):

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

E, finalmente, PL / V8 :

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

Mas é de alguma forma muito rápido. Vamos tentar executar a consulta 1000 vezes ou 1 milhão de vezes, de repente a diferença será mais perceptível:

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

Observe que, com o PL / V8, o experimento foi realizado com mil, e não um milhão de iterações. Com recursos moderados, o PL / V8 em um ciclo de 1 milhão de operações consome toda a memória e paralisa completamente o carro. Já em mil iterações, o processo do postgres seleciona 3,5 GB de memória e 100% de gravação no disco. De fato, o postgres lança o ambiente V8 e, é claro, consome memória. Depois de executar a solicitação, esse monstro turbo não vai devolver memória. Para liberar memória, você precisa encerrar a sessão.

Vemos que o PL / pgSQL já é 2 vezes mais rápido que o PL / Perl e o PL / Python. O PL / V8 ainda está um pouco atrás deles, mas no final do artigo ele está parcialmente reabilitado.

Em geral, Perl com Python nessas experiências mostra aproximadamente os mesmos resultados. O Perl costumava ser um pouco inferior ao Python; nas versões modernas, é um pouco mais rápido. O terceiro python é um pouco mais lento que o segundo. Toda a diferença está dentro de 15%.

Desempenho com PREPARE


Pessoas que sabem vão entender: algo está errado. O PL / pgSQL pode armazenar em cache automaticamente os planos de consulta e, em PL / *, cada vez que a consulta foi agendada novamente. De uma maneira boa, você precisa preparar solicitações, criar um plano de solicitações e, de acordo com esse plano, elas devem ser executadas quantas vezes forem necessárias. No PL / *, você pode trabalhar explicitamente com os planos de consulta, que tentaremos começar com o 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

Com preparenossas duas linguagens, quase alcançamos PL / pgSQL, e a terceira também queria, mas não alcançou a linha de chegada devido aos crescentes requisitos de memória.

Mas se você não leva em conta a memória, fica claro que todos os idiomas quase se enfrentam - e não por acaso. O gargalo deles agora é comum - trabalhando com o banco de dados por meio do SPI.

Desempenho computacional


Vemos que o desempenho do idioma descansou no trabalho com o banco de dados. Para comparar idiomas, vamos tentar calcular algo sem recorrer ao banco de dados, por exemplo, a soma dos quadrados.

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

Vemos que PL / Perl e PL / Python ultrapassaram e superaram PL / pgSQL, eles são 4 vezes mais rápidos. E os oito estão rasgando todo mundo! Mas é realmente por nada? Ou vamos conseguir isso pela cabeça? Sim nós vamos.

O número no JavaScript é flutuante e o resultado é rápido, mas não preciso: 333333833333127550 em vez de 33333383333353500000.

Aqui está a fórmula pela qual o resultado exato é calculado :

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

Como exercício, você pode provar isso usando indução matemática.

Na ordem do riso

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

NOTICE:
33333383333312754000

Em Javascript, parseIntele ainda faz um float, não um Int.

No entanto, o BigInt apareceu na V8 em 2018 e agora pode ser contado com certeza, mas com um prejuízo para a velocidade, pois não é um número inteiro de 64 bits, mas um número inteiro de profundidade de bits arbitrária. No entanto, no PL / V8 essa inovação ainda não foi atingida. Em outras linguagens procedurais, números de bits arbitrários (análogos do SQL numeric) são suportados por bibliotecas especiais.

Em Perl, há um módulo Math :: em ponto flutuante para a aritmética com precisão arbitrária, e em Python o em ponto flutuante pacote é um Cython invólucro em torno do GNU MPFR biblioteca .

Funções de desempenho para classificação


Aqui está um exemplo prático, que mostra a diferença no desempenho da classificação por função, se essa função estiver escrita em idiomas diferentes. Tarefa: classificar os campos de texto que contêm os números dos números do diário, que podem ser os seguintes:

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

Essa. na verdade, é uma string, mas começa com um número e você precisa classificar por esses números. Portanto, para classificar corretamente como seqüências de caracteres, suplementamos a parte numérica com zeros à esquerda para obter:

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

Sim, eu sei que essa não é a única solução para o problema (e nem exatamente). Mas, por exemplo, serve.

Para solicitar um tipo, SELECT ... ORDER BY nsort(n)escrevemos funções em PL / Perl, SQL, PL / Python e PL / V8 que convertem os números de diário para este formulário:

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; } 
$$;

Na minha biblioteca de 15,5 mil artigos de periódicos, uma consulta usando uma função no PL / Perl leva cerca de 64ms contra 120ms no PL / Python e 200ms no PL / PgSQL. Mas o mais rápido - PL / v8: 54ms.

Nota: ao experimentar a classificação, forneça a quantidade necessária de memória de trabalho para que a classificação entre na memória (EXPLAIN será exibido Sort Method: quicksort). A quantidade de memória é definida pelo parâmetro work_mem:

set work_mem = '20MB';

Memória


Perl não gosta de estruturas em loop, ele não sabe como limpá-las. Se você ativer um ponteiro para be um bponteiro para a, o contador de referência nunca será redefinido e a memória não será liberada.

Os idiomas de coleta de lixo têm outros problemas. Não se sabe, por exemplo, quando a memória será liberada ou se será liberada. Ou - se você não cuidar disso de propósito - os colecionadores irão coletar lixo no momento mais inoportuno.

Mas também existem recursos de gerenciamento de memória diretamente relacionados ao Postgres. Existem estruturas que o SPI aloca, e o Perl nem sempre percebe que eles precisam ser liberados.

PL / Perl

NÃO é assim:

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

E por aí vai:

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};
$$;

Após a execução, o manipulador $hpermanecerá vivo, apesar de não existir um único vínculo vivo com ele.

Tudo bem, você só precisa se lembrar da necessidade de liberar explicitamente recursos com 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 nunca flui , o plano é liberado automaticamente:

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

PL / V8

Mesma história que Perl. Não flui assim:

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

E por aí vai:

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

Novamente: não esqueça de liberar recursos. Aqui está. h.free();

Não flui:

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;
$$;

Parâmetros


É hora de entender como os argumentos são passados ​​para as funções. Nos exemplos, passaremos 4 parâmetros com tipos para a função:

  • todo;
  • uma matriz;
  • bytea e
  • jsonb

Como eles entram no 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)

Será JSON ou JSONB - neste caso, não faz diferença: eles ainda ficam na forma de uma string. Essa é uma taxa pela versatilidade: o Postgres possui muitos tipos, de diferentes graus de "embutido". Exigir do desenvolvedor que, com o novo tipo, ele imediatamente forneça funções de conversão para todos os PL / *, seria demais. Por padrão, muitos tipos são passados ​​como seqüências de caracteres. Mas isso nem sempre é conveniente, você deve analisar esses termos. Obviamente, gostaria que os dados do Postgres se transformassem imediatamente nas estruturas Perl apropriadas. Por padrão, isso não acontece, mas a partir da 9.6, o mecanismo TRANSFORM apareceu - a capacidade de definir funções de conversão de tipos: CREATE TRANSFORM .

Para criar TRANSFORM, você precisa escrever duas funções em C: uma converterá dados de um determinado tipo para um lado e o outro de volta. Observe que o TRANSFORM trabalha em quatro locais:

  • Ao passar parâmetros para uma função;
  • Ao retornar um valor de função;
  • Ao passar parâmetros para uma chamada SPI dentro de uma função;
  • Após o recebimento do resultado da chamada SPI dentro da função.

TRANSFORMAR JSONB para Perl e Python, desenvolvido por Anton Bykov, apareceu na 11ª versão do Postgres. Agora você não precisa analisar o JSONB, ele entra no Perl imediatamente como a estrutura correspondente. Você deve criar a extensão jsonb_plperl e, em seguida, você pode usar TRANSFORM:

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

Você pode chamar esta função para verificar se o JSONB se transformou em um hash de pérola:

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


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

Uma questão completamente diferente!

O autor deste artigo também ajudou no desenvolvimento de TRANSFORMs. Descobriu-se que um tipo de dados tão simples, como booleanpassado ao PL / Perl de forma inconveniente, como seqüências de texto 't'ou 'f'. Mas, no entendimento de Perl, a string 'f' é verdadeira. Para eliminar o inconveniente, foi inventado um patch que definia a conversão para o tipo booleano . Este patch atingiu o PostgreSQL 13 e estará disponível em breve. Devido à sua simplicidade, o bool_plperl pode servir como um modelo inicial mínimo para gravar qualquer outra conversão.

Espero que alguém desenvolva TRANSFORM para outros tipos de dados (bytea, matrizes, datas, numéricos).

Agora vamos ver como os parâmetros são passados ​​no 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)

Uma matriz é convertida em uma matriz - isso é bom (uma vez que as matrizes multidimensionais da versão PG10 também são corretamente transferidas para python). No Perl, uma matriz foi convertida em um objeto de uma classe especial. Bem, jsonbtransformado. Sem TRANSFORM, o jsonb será passado como uma string.

Agora vamos ver de que forma os parâmetros entram no 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 convertido em um objeto JavaScript sem TRANSFORMAR! Os tipos de Postgres temporários também são convertidos para o tipo JS de data. A mesma coisa com booleano. Todas as transformações já estão incorporadas no PL / V8.

Trabalhar com infinito


A constante INFINITY não é usada com muita frequência, mas o trabalho mal feito com ela é perigoso. No PostgreSQL, Infinity e -Infinity existem como valores especiais para alguns tipos temporários e de ponto flutuante. Mas a transferência do Infinity para linguagens procedurais e vice-versa deve ser discutida em detalhes, pois trabalhar com elas pode depender não apenas da linguagem, mas também das bibliotecas, do sistema operacional e até do hardware.

O Python possui um módulo Numpy que define o infinito numérico:

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

inf -inf

Perl também tem infinito, ele usa uma string "infinity"que pode ser reduzida para "inf". Por exemplo, você poderia dizer:

perl -e 'print 1 * "inf"'

Inf

ou

perl -e 'print 1/"inf"'

0

No PL / Perl, PL / Python, PL / v8, o infinito numérico do Postgres é passado corretamente, mas uma data infinita não está correta. Em vez disso, no PL / Perl e no PL / Python, não há tipo de dados interno para o tempo, uma string chega lá. No PL / V8, existe um tipo interno Date, e a data usual de um postgres se transforma nele. Mas o V8 não sabe a data sem fim e, quando transferido, se transforma Invalid Date.

Passando parâmetros para solicitações preparadas


De volta a prepare, considere como os parâmetros são passados ​​para lá. Idiomas diferentes têm muito em comum, pois todos são baseados no SPI.

Ao preparar uma consulta no PL / Perl , você precisa determinar o tipo de parâmetros que são passados ​​e, ao executar a consulta, especifica apenas os valores desses parâmetros (os parâmetros são passados ​​para o PL / pgSQL da mesma maneira).

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);
$$;

No PL / Python, a essência é a mesma, mas a sintaxe é um pouco diferente:

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

No PL / V8, as diferenças são mínimas:

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();
$$;

No PL / Java, tudo é diferente. Lá, o SPI claramente não é usado, mas uma conexão pseudo-JDBC ao banco de dados é formada. Para um programador de PL / Java, tudo acontece como se ele estivesse criando um aplicativo cliente. Isso é conveniente, e também se pode abordar o design do PL / Perl e PL / Python, mas por algum motivo isso não foi feito (no entanto, ninguém proíbe a criação de mais algumas implementações do PL / Perl e PL / Python).

Trabalhar com cursor


Todas as funções SPI que usamos quando acessamos o banco de dados - spi_exec_query()e outras - têm um parâmetro que limita o número de linhas retornadas. Se você precisar de muitas linhas retornadas, não poderá usar um cursor para puxá-las um pouco.

Os cursores funcionam em todos esses idiomas. Em PL / Perl,
spi_exec_query retorna um cursor do qual você pode extrair cadeias uma de cada vez. Não é necessário fechar o cursor, ele se fechará. Mas se você quiser redescobri-lo novamente, poderá explicitamente fechá-lo com um comando 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.
...

No PL / Python, tudo é muito parecido, mas o cursor é apresentado como um objeto pelo qual você pode percorrer:

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

No PL / v8, tudo também é muito parecido, mas não se esqueça de liberar o plano de consulta preparado:

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

PL / V8: Acesso rápido aos recursos


No PL / V8, você pode chamar uma função não de um SELECT regular, mas encontrá-la pelo nome e iniciá-la imediatamente com plv8.find_function(name);. Mas lembre-se de que, em JS, uma função não pode ser polimórfica, como no PostgreSQL, na qual funções com o mesmo nome, mas com parâmetros diferentes, podem coexistir. No PL / v8, é claro, podemos criar funções polimórficas, mas find_functionhaverá um erro ao tentar usá- lo.

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

Se uma função por nome não for ambígua, poderá ser chamada sem SPI e conversões de tipo, ou seja, muito mais rapido. Por exemplo, assim:

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

Transações


O Postgres 11 é muito divertido: procedimentos reais apareceram . O Postgres costumava ter apenas recursos. A alegria não se deve apenas à compatibilidade e conformidade com o padrão SQL, mas por que: nos procedimentos, você pode confirmar e reverter transações.

O PL / Perl e o PL / Python já possuem funções SPI para gerenciamento de transações, enquanto o PL / V8 ainda não. No PL / Perl, essas funções são chamadas spi_commit()e spi_rollback(), e um exemplo de uso está na documentação . No PL / Python, este é plpy.commit()e plpy.rollback().

Subtransação


As subtransações são convenientes para o tratamento correto de erros na lógica complexa de vários níveis.

No PL / pgSQL dentro de uma transação, cada bloco com a palavra-chave EXCEPTION é uma subtransação. Você pode ler sobre alguns problemas de desempenho e confiabilidade que podem surgir neste caso, por exemplo, aqui .

Não há subtransações explícitas no PL / Perl , mas elas podem ser simuladas através de savaepoints. Aparentemente, se você desejar, é fácil escrever um módulo pearl que implemente subtransações de forma explícita.

No PL / Python, as subtransações apareceram há muito tempo: das 9.5 explícitas , antes as implícitas . Você pode definir uma transação, envolvê-latry-e executar. Se a subtransação cair, cairemos no bloco except, se não cairmos, entraremos no bloco elsee seguiremos em frente.

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

Um design semelhante existe no PL / V8 , apenas na sintaxe JS.

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

Conclusão


Tente, mas não abuse :) O conhecimento de PL / * pode trazer alguns benefícios. Como qualquer ferramenta, eles gostam de ser usados ​​para o propósito a que se destinam.

O PL / v8 é muito promissor, mas às vezes se comporta inesperadamente e tem vários problemas. Portanto, é melhor tirar os idiomas da caixa se eles forem adequados para sua tarefa.

Quero agradecer a Igor Levshin (Igor_Le), que me ajudou muito na preparação do material para o artigo e lançou algumas idéias úteis, além de Evgeny Sergeev e Alexey Fadeev pelas correções que propuseram.

All Articles