Hvorfor?
hvorfor er C-versionen så meget hurtigere?
Et PostgreSQL-array er i sig selv en ret ineffektiv datastruktur. Den kan indeholde hvilken som helst datatype, og den er i stand til at være multidimensionel, så mange optimeringer er bare ikke mulige. Men som du har set, er det muligt at arbejde med det samme array meget hurtigere i C.
Det er fordi array-adgang i C kan undgå meget af det gentagne arbejde, der er involveret i PL/PgSQL-array-adgang. Bare tag et kig på src/backend/utils/adt/arrayfuncs.c
, array_ref
. Se nu på, hvordan det kaldes fra src/backend/executor/execQual.c
i ExecEvalArrayRef
. Som kører for hver individuel matrixadgang fra PL/PgSQL, som du kan se ved at vedhæfte gdb til pid'en fundet fra select pg_backend_pid()
, indstille et brudpunkt ved ExecEvalArrayRef
, fortsætter og kører din funktion.
Endnu vigtigere, i PL/PgSQL køres hver sætning, du udfører, gennem forespørgselseksekveringsmaskineriet. Dette gør små, billige udsagn ret langsomme, selv når der tages højde for det faktum, at de er forberedt på forhånd. Noget som:
a := b + c
udføres faktisk af PL/PgSQL mere som:
SELECT b + c INTO a;
Du kan observere dette, hvis du skruer debug-niveauerne højt nok, vedhæfter en debugger og bryder på et passende sted eller bruger auto_explain
modul med indlejret udsagnsanalyse. For at give dig en idé om, hvor meget overhead dette medfører, når du kører en masse små simple sætninger (som array-adgange), skal du tage et kig på dette eksempel på backtrace og mine noter om det.
Der er også en betydelig opstartsomkostning til hver PL/PgSQL funktion påkaldelse. Det er ikke stort, men det er nok at lægge sammen, når det bliver brugt som et aggregat.
En hurtigere tilgang i C
I dit tilfælde ville jeg nok gøre det i C, som du har gjort, men jeg ville undgå at kopiere arrayet, når det kaldes som et aggregat. Du kan kontrollere, om det kaldes i samlet kontekst:
if (AggCheckCallContext(fcinfo, NULL))
og hvis det er tilfældet, brug den oprindelige værdi som en foranderlig pladsholder, modificere den og returner den i stedet for at tildele en ny. Jeg vil snart skrive en demo for at bekræfte, at dette er muligt med arrays... (opdatering) eller ikke så kort, jeg glemte, hvor forfærdeligt det er at arbejde med PostgreSQL-arrays i C. Her går vi:
// 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);
}
og tilføj dette til 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);
(mere korrekt ville du oprette intarray--1.1.sql
og intarray--1.0--1.1.sql
og opdater intarray.control
. Dette er bare et hurtigt hack.)
Brug:
make USE_PGXS=1
make USE_PGXS=1 install
at kompilere og installere.
Nu DROP EXTENSION intarray;
(hvis du allerede har det) og CREATE EXTENSION intarray;
.
Du vil nu have den samlede funktion sum_intarray_cols
tilgængelig for dig (som din sum(int4[])
, samt to-operanden add_intarray_cols
(som din array_add
).
Ved at specialisere sig i heltalsarrays forsvinder en hel masse kompleksitet. En masse kopiering undgås i det samlede tilfælde, da vi sikkert kan ændre "state"-arrayet (det første argument) på plads. For at holde tingene konsekvente får vi i tilfælde af ikke-samlet påkald en kopi af det første argument, så vi stadig kan arbejde med det på stedet og returnere det.
Denne tilgang kunne generaliseres til at understøtte enhver datatype ved at bruge fmgr-cachen til at slå tilføjelsesfunktionen op for typen(r) af interesse osv. Jeg er ikke særlig interesseret i at gøre det, så hvis du har brug for det (f.eks. for at summere kolonner af NUMERIC
arrays), så ... have det sjovt.
På samme måde, hvis du har brug for at håndtere uens arraylængder, kan du sikkert finde ud af, hvad du skal gøre ud fra ovenstående.