Защо?
защо версията C е толкова по-бърза?
Самият PostgreSQL масив е доста неефективна структура от данни. Може да съдържа всякакво тип данни и е в състояние да бъде многоизмерен, така че много оптимизации просто не са възможни. Въпреки това, както видяхте, е възможно да се работи със същия масив много по-бързо в C.
Това е така, защото достъпът до масив в C може да избегне много от повтарящата се работа, свързана с достъпа до PL/PgSQL масив. Просто погледнете src/backend/utils/adt/arrayfuncs.c
, array_ref
. Сега вижте как се извиква от src/backend/executor/execQual.c
в ExecEvalArrayRef
. Което работи за достъп до всеки отделен масив от PL/PgSQL, както можете да видите, като прикачите gdb към pid, намерен от select pg_backend_pid()
, задаване на точка на прекъсване в ExecEvalArrayRef
, продължаване и стартиране на вашата функция.
По-важното е, че в PL/PgSQL всеки израз, който изпълнявате, се изпълнява през машината за изпълнение на заявки. Това прави малките, евтини изявления доста бавни, дори като се вземе предвид фактът, че са предварително подготвени. Нещо като:
a := b + c
всъщност се изпълнява от PL/PgSQL повече като:
SELECT b + c INTO a;
Можете да наблюдавате това, ако включите нивата на отстраняване на грешки достатъчно високи, прикачите програма за отстраняване на грешки и прекъснете в подходяща точка или използвате auto_explain
модул с анализ на вложени изрази. За да ви дам представа колко режийни разходи налага това, когато изпълнявате много малки прости изрази (като достъп до масив), разгледайте този пример за обратна проследяване и моите бележки за него.
Също така има значителни разходи при стартиране към всяко извикване на PL/PgSQL функция. Не е огромен, но е достатъчен да се сумира, когато се използва като съвкупност.
По-бърз подход в C
Във вашия случай вероятно бих го направил в C, както сте направили вие, но бих избегнал копирането на масива, когато се извиква като агрегат. Можете да проверите дали се извиква в обобщен контекст:
if (AggCheckCallContext(fcinfo, NULL))
и ако е така, използвайте оригиналната стойност като променлив заместител, като я модифицирате и след това я връщате, вместо да разпределяте нова. Ще напиша демонстрация, за да проверя дали това е възможно с масиви скоро... (актуализация) или не толкова скоро, забравих колко ужасна е работата с PostgreSQL масиви в C. Отиваме:
// 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);
}
и добавете това към 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);
(по-правилно бихте създали intarray--1.1.sql
и intarray--1.0--1.1.sql
и актуализирайте intarray.control
. Това е само бърз хак.)
Използвайте:
make USE_PGXS=1
make USE_PGXS=1 install
за компилиране и инсталиране.
Сега DROP EXTENSION intarray;
(ако вече го имате) и CREATE EXTENSION intarray;
.
Вече ще имате агрегатната функция sum_intarray_cols
достъпен за вас (като вашия sum(int4[])
, както и add_intarray_cols
с два операнда (като вашия array_add
).
Като се специализира в целочислени масиви, цял куп сложност изчезва. В обобщения случай се избягва куп копиране, тъй като можем безопасно да модифицираме масива "state" (първия аргумент) на място. За да поддържаме нещата последователни, в случай на неагрегирано извикване получаваме копие на първия аргумент, за да можем все още да работим с него на място и да го върнем.
Този подход може да бъде обобщен, за да поддържа всеки тип данни, като се използва кеша fmgr за търсене на функцията за добавяне за типа(овете) от интерес и т.н. Не съм особено заинтересован да правя това, така че ако имате нужда от него (да речем, за сумиране на колони от NUMERIC
масиви) тогава ... забавлявайте се.
По същия начин, ако трябва да обработвате различни дължини на масива, вероятно можете да разберете какво да правите от горното.