Por quê?
por que a versão C é muito mais rápida?
Um array PostgreSQL é uma estrutura de dados bastante ineficiente. Ele pode conter qualquer tipo de dados e é capaz de ser multidimensional, então muitas otimizações simplesmente não são possíveis. No entanto, como você viu, é possível trabalhar com o mesmo array muito mais rápido em C.
Isso porque o acesso ao array em C pode evitar muito do trabalho repetido envolvido no acesso ao array PL/PgSQL. Basta dar uma olhada em
src/backend/utils/adt/arrayfuncs.c
, array_ref
. Agora veja como ele é invocado de src/backend/executor/execQual.c
em ExecEvalArrayRef
. Que é executado para cada acesso individual ao array de PL/PgSQL, como você pode ver anexando gdb ao pid encontrado em select pg_backend_pid()
, definindo um ponto de interrupção em ExecEvalArrayRef
, continuando e executando sua função. Mais importante, em PL/PgSQL cada instrução que você executa é executada através do maquinário do executor de consulta. Isso torna as declarações pequenas e baratas bastante lentas, mesmo considerando o fato de serem pré-preparadas. Algo como:
a := b + c
é realmente executado por PL/PgSQL mais parecido com:
SELECT b + c INTO a;
Você pode observar isso se aumentar os níveis de depuração o suficiente, anexar um depurador e interromper em um ponto adequado ou usar o
auto_explain
módulo com análise de instrução aninhada. Para lhe dar uma ideia de quanta sobrecarga isso impõe quando você está executando muitas pequenas instruções simples (como acessos a arrays), dê uma olhada neste exemplo de backtrace e minhas notas sobre ele. Há também uma sobrecarga de inicialização significativa para cada chamada de função PL/PgSQL. Não é enorme, mas é o suficiente para somar quando estiver sendo usado como um agregado.
Uma abordagem mais rápida em C
No seu caso eu provavelmente faria em C, como você fez, mas evitaria copiar o array quando chamado como agregado. Você pode verificar se está sendo invocado no contexto agregado:
if (AggCheckCallContext(fcinfo, NULL))
e em caso afirmativo, use o valor original como um espaço reservado mutável, modificando-o e retornando-o em vez de alocar um novo. Em breve escreverei uma demonstração para verificar se isso é possível com arrays... (atualização) ou não tão breve, esqueci o quão horrível é trabalhar com arrays PostgreSQL em C. Aqui vamos nós:
// append to contrib/intarray/_int_op.c
PG_FUNCTION_INFO_V1(add_intarray_cols);
Datum add_intarray_cols(PG_FUNCTION_ARGS);
Datum
add_intarray_cols(PG_FUNCTION_ARGS)
{
ArrayType *a,
*b;
int i, n;
int *da,
*db;
if (PG_ARGISNULL(1))
ereport(ERROR, (errmsg("Second operand must be non-null")));
b = PG_GETARG_ARRAYTYPE_P(1);
CHECKARRVALID(b);
if (AggCheckCallContext(fcinfo, NULL))
{
// Called in aggregate context...
if (PG_ARGISNULL(0))
// ... for the first time in a run, so the state in the 1st
// argument is null. Create a state-holder array by copying the
// second input array and return it.
PG_RETURN_POINTER(copy_intArrayType(b));
else
// ... for a later invocation in the same run, so we'll modify
// the state array directly.
a = PG_GETARG_ARRAYTYPE_P(0);
}
else
{
// Not in aggregate context
if (PG_ARGISNULL(0))
ereport(ERROR, (errmsg("First operand must be non-null")));
// Copy 'a' for our result. We'll then add 'b' to it.
a = PG_GETARG_ARRAYTYPE_P_COPY(0);
CHECKARRVALID(a);
}
// This requirement could probably be lifted pretty easily:
if (ARR_NDIM(a) != 1 || ARR_NDIM(b) != 1)
ereport(ERROR, (errmsg("One-dimesional arrays are required")));
// ... as could this by assuming the un-even ends are zero, but it'd be a
// little ickier.
n = (ARR_DIMS(a))[0];
if (n != (ARR_DIMS(b))[0])
ereport(ERROR, (errmsg("Arrays are of different lengths")));
da = ARRPTR(a);
db = ARRPTR(b);
for (i = 0; i < n; i++)
{
// Fails to check for integer overflow. You should add that.
*da = *da + *db;
da++;
db++;
}
PG_RETURN_POINTER(a);
}
e anexe isso a
contrib/intarray/intarray--1.0.sql
:CREATE FUNCTION add_intarray_cols(_int4, _int4) RETURNS _int4
AS 'MODULE_PATHNAME'
LANGUAGE C IMMUTABLE;
CREATE AGGREGATE sum_intarray_cols(_int4) (sfunc = add_intarray_cols, stype=_int4);
(mais corretamente você criaria
intarray--1.1.sql
e intarray--1.0--1.1.sql
e atualize intarray.control
. Este é apenas um hack rápido.) Usar:
make USE_PGXS=1
make USE_PGXS=1 install
para compilar e instalar.
Agora
DROP EXTENSION intarray;
(se você já tiver) e CREATE EXTENSION intarray;
. Agora você terá a função agregada
sum_intarray_cols
disponível para você (como seu sum(int4[])
, bem como o add_intarray_cols
de dois operandos (como seu array_add
). Ao se especializar em arrays inteiros, toda a complexidade desaparece. Muitas cópias são evitadas no caso agregado, pois podemos modificar com segurança o array "state" (o primeiro argumento) no local. Para manter as coisas consistentes, no caso de invocação não agregada, obtemos uma cópia do primeiro argumento para que ainda possamos trabalhar com ele no local e devolvê-lo.
Essa abordagem pode ser generalizada para oferecer suporte a qualquer tipo de dados usando o cache fmgr para pesquisar a função add para os tipos de interesse, etc. Não estou particularmente interessado em fazer isso, portanto, se você precisar (digamos, para somar colunas de
NUMERIC
arrays) então... divirta-se. Da mesma forma, se você precisar lidar com comprimentos de matriz diferentes, provavelmente poderá descobrir o que fazer a partir do acima.