sql >> Databasteknik >  >> RDS >> PostgreSQL

Varför är PostgreSQL-arrayåtkomst så mycket snabbare i C än i PL/pgSQL?

Varför?

varför är C-versionen så mycket snabbare?

En PostgreSQL-array är i sig en ganska ineffektiv datastruktur. Den kan innehålla alla datatyp och den kan vara flerdimensionell, så många optimeringar är helt enkelt inte möjliga. Men som du har sett är det möjligt att arbeta med samma array mycket snabbare i C.

Det beror på att matrisåtkomst i C kan undvika mycket av det upprepade arbetet som är involverat i PL/PgSQL-matrisåtkomst. Ta bara en titt på src/backend/utils/adt/arrayfuncs.c , array_ref . Titta nu på hur det anropas från src/backend/executor/execQual.c i ExecEvalArrayRef . Som körs för varje enskild arrayåtkomst från PL/PgSQL, som du kan se genom att bifoga gdb till pid som hittas från select pg_backend_pid() , ange en brytpunkt vid ExecEvalArrayRef , fortsätter och kör din funktion.

Ännu viktigare, i PL/PgSQL körs varje sats du kör genom frågeexekveringsmaskineriet. Detta gör små, billiga uttalanden ganska långsamma även om man tar hänsyn till det faktum att de är förberedda. Något i stil med:

a := b + c

exekveras faktiskt av PL/PgSQL mer som:

SELECT b + c INTO a;

Du kan observera detta om du vrider felsökningsnivåerna tillräckligt höga, ansluter en debugger och bryter vid en lämplig punkt, eller använder auto_explain modul med kapslad satsanalys. För att ge dig en uppfattning om hur mycket overhead detta medför när du kör många små enkla uttalanden (som array-åtkomster), ta en titt på det här exemplet på bakåtspårning och mina anteckningar om det.

Det finns också en betydande startoverhead till varje PL/PgSQL-funktionsanrop. Det är inte stort, men det räcker att lägga ihop när det används som ett aggregat.

En snabbare strategi i C

I ditt fall skulle jag förmodligen göra det i C, som du har gjort, men jag skulle undvika att kopiera arrayen när den anropas som ett aggregat. Du kan kontrollera om det anropas i aggregerat sammanhang:

if (AggCheckCallContext(fcinfo, NULL))

och om så är fallet, använd det ursprungliga värdet som en föränderlig platshållare, modifiera det och returnera det istället för att allokera ett nytt. Jag ska skriva en demo för att verifiera att detta är möjligt med arrayer inom kort... (uppdatering) eller inte så snart, jag har glömt hur fruktansvärt hemskt att arbeta med PostgreSQL-arrayer i C är. Nu kö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);
}

och lägg till detta till 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);

(mer korrekt skulle du skapa intarray--1.1.sql och intarray--1.0--1.1.sql och uppdatera intarray.control . Det här är bara ett snabbt hack.)

Använd:

make USE_PGXS=1
make USE_PGXS=1 install

att kompilera och installera.

Nu DROP EXTENSION intarray; (om du redan har det) och CREATE EXTENSION intarray; .

Du kommer nu att ha den aggregerade funktionen sum_intarray_cols tillgänglig för dig (som din sum(int4[]) , såväl som tvåoperanden add_intarray_cols (som din array_add ).

Genom att specialisera sig på heltalsmatriser försvinner en hel massa komplexitet. En massa kopiering undviks i det aggregerade fallet, eftersom vi säkert kan modifiera "state"-arrayen (det första argumentet) på plats. För att hålla saker och ting konsekventa får vi en kopia av det första argumentet i fallet med icke-aggregerad anrop så att vi fortfarande kan arbeta med det på plats och returnera det.

Det här tillvägagångssättet skulle kunna generaliseras för att stödja vilken datatyp som helst genom att använda fmgr-cachen för att leta upp add-funktionen för typen/typerna av intresse, etc. Jag är inte särskilt intresserad av att göra det, så om du behöver det (säg, för att summera kolumner av NUMERIC arrays) så ... ha kul.

På samma sätt, om du behöver hantera olika arraylängder, kan du förmodligen räkna ut vad du ska göra från ovanstående.



  1. Sammankoppla grupper i SQL Server

  2. Komma igång med SQL på Oracle Application Express

  3. Vad är skillnaden mellan kommaseparerade joins och join on syntax i MySQL?

  4. Främmande nyckel till ett av många bord?