PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Perché l'accesso all'array PostgreSQL è molto più veloce in C rispetto a PL/pgSQL?

Perché?

perché la versione C è molto più veloce?

Un array PostgreSQL è di per sé una struttura dati piuttosto inefficiente. Può contenere qualsiasi tipo di dati ed è in grado di essere multidimensionale, quindi molte ottimizzazioni non sono possibili. Tuttavia, come hai visto, è possibile lavorare con lo stesso array molto più velocemente in C.

Questo perché l'accesso all'array in C può evitare gran parte del lavoro ripetuto coinvolto nell'accesso all'array PL/PgSQL. Dai un'occhiata a src/backend/utils/adt/arrayfuncs.c , array_ref . Ora guarda come viene invocato da src/backend/executor/execQual.c in ExecEvalArrayRef . Che funziona per ogni singolo accesso all'array da PL/PgSQL, come puoi vedere allegando gdb al pid trovato da select pg_backend_pid() , impostando un punto di interruzione in ExecEvalArrayRef , continuando ed eseguendo la tua funzione.

Ancora più importante, in PL/PgSQL ogni istruzione eseguita viene eseguita tramite il macchinario di esecuzione delle query. Ciò rende le affermazioni piccole ed economiche piuttosto lente anche tenendo conto del fatto che sono pre-preparate. Qualcosa come:

a := b + c

viene effettivamente eseguito da PL/PgSQL più simile a:

SELECT b + c INTO a;

Puoi osservarlo se aumenti i livelli di debug sufficientemente alti, colleghi un debugger e interrompi in un punto adatto o usi auto_explain modulo con analisi delle istruzioni nidificate. Per darti un'idea dell'overtrace che questo impone quando esegui molte piccole istruzioni semplici (come gli accessi agli array), dai un'occhiata a questo esempio di backtrace e alle mie note su di esso.

C'è anche un overhead di avvio significativo a ciascuna chiamata di funzione PL/PgSQL. Non è enorme, ma è sufficiente per sommare quando viene utilizzato come aggregato.

Un approccio più veloce in C

Nel tuo caso lo farei probabilmente in C, come hai fatto tu, ma eviterei di copiare l'array quando chiamato come aggregato. Puoi verificare se viene richiamato nel contesto aggregato:

if (AggCheckCallContext(fcinfo, NULL))

e in tal caso, utilizzare il valore originale come segnaposto mutabile, modificandolo e poi restituendolo invece di assegnarne uno nuovo. Scriverò una demo per verificare che ciò sia possibile con gli array a breve ... (aggiornamento) o non così a breve, ho dimenticato quanto sia assolutamente orribile lavorare con gli array PostgreSQL in C. Eccoci:

// 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 aggiungilo 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);

(più correttamente creeresti intarray--1.1.sql e intarray--1.0--1.1.sql e aggiorna intarray.control . Questo è solo un trucco veloce.)

Usa:

make USE_PGXS=1
make USE_PGXS=1 install

da compilare e installare.

Ora DROP EXTENSION intarray; (se ce l'hai già) e CREATE EXTENSION intarray; .

Ora avrai la funzione di aggregazione sum_intarray_cols a tua disposizione (come il tuo sum(int4[]) , così come i due operandi add_intarray_cols (come il tuo array_add ).

Specializzandosi in array di interi, tutta una serie di complessità scompare. Una serie di copie viene evitata nel caso aggregato, poiché possiamo tranquillamente modificare l'array "state" (il primo argomento) sul posto. Per mantenere le cose coerenti, nel caso di un'invocazione non aggregata otteniamo una copia del primo argomento in modo che possiamo ancora lavorare con esso sul posto e restituirlo.

Questo approccio potrebbe essere generalizzato per supportare qualsiasi tipo di dati utilizzando la cache fmgr per cercare la funzione add per i tipi di interesse, ecc. Non sono particolarmente interessato a farlo, quindi se ne hai bisogno (ad esempio, per sommare colonne di NUMERIC array) allora... buon divertimento.

Allo stesso modo, se devi gestire lunghezze di array dissimili, probabilmente puoi capire cosa fare da quanto sopra.