PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Warum ist der PostgreSQL-Array-Zugriff in C so viel schneller als in PL/pgSQL?

Warum?

Warum ist die C-Version so viel schneller?

Ein PostgreSQL-Array ist selbst eine ziemlich ineffiziente Datenstruktur. Es kann beliebige enthalten Datentyp und kann mehrdimensional sein, sodass viele Optimierungen einfach nicht möglich sind. Wie Sie jedoch gesehen haben, ist es möglich, mit demselben Array viel schneller in C zu arbeiten.

Das liegt daran, dass der Array-Zugriff in C einen Großteil der wiederholten Arbeit vermeiden kann, die mit dem PL/PgSQL-Array-Zugriff verbunden ist. Werfen Sie einfach einen Blick auf src/backend/utils/adt/arrayfuncs.c , array_ref . Sehen Sie sich nun an, wie es von src/backend/executor/execQual.c aufgerufen wird in ExecEvalArrayRef . Was für jeden einzelnen Array-Zugriff ausgeführt wird von PL/PgSQL, wie Sie sehen können, indem Sie gdb an die PID anhängen, die von select pg_backend_pid() gefunden wird , einen Haltepunkt bei ExecEvalArrayRef setzen , fortfahren und Ihre Funktion ausführen.

Noch wichtiger ist, dass in PL/PgSQL jede von Ihnen ausgeführte Anweisung durch die Abfrageausführungsmaschinerie ausgeführt wird. Dadurch werden kleine, billige Anweisungen ziemlich langsam, selbst wenn man berücksichtigt, dass sie vorbereitet sind. Etwas wie:

a := b + c

wird tatsächlich von PL/PgSQL eher wie folgt ausgeführt:

SELECT b + c INTO a;

Sie können dies beobachten, wenn Sie die Debug-Stufen hoch genug drehen, einen Debugger anhängen und an geeigneter Stelle abbrechen oder das auto_explain verwenden Modul mit verschachtelter Anweisungsanalyse. Um Ihnen eine Vorstellung davon zu geben, wie viel Overhead dies verursacht, wenn Sie viele winzige einfache Anweisungen (wie Array-Zugriffe) ausführen, werfen Sie einen Blick auf dieses Beispiel für Backtrace und meine Notizen dazu.

Es gibt auch einen erheblichen Start-Overhead zu jedem PL/PgSQL-Funktionsaufruf. Es ist nicht riesig, aber es reicht aus, um sich zu summieren, wenn es als Aggregat verwendet wird.

Ein schnellerer Ansatz in C

In Ihrem Fall würde ich es wahrscheinlich in C tun, wie Sie es getan haben, aber ich würde vermeiden, das Array zu kopieren, wenn es als Aggregat aufgerufen wird. Sie können prüfen, ob es im Gesamtkontext aufgerufen wird:

if (AggCheckCallContext(fcinfo, NULL))

Verwenden Sie in diesem Fall den ursprünglichen Wert als veränderlichen Platzhalter, ändern Sie ihn und geben Sie ihn dann zurück, anstatt einen neuen zuzuweisen. Ich werde in Kürze eine Demo schreiben, um zu überprüfen, ob dies mit Arrays möglich ist ... (Update) oder nicht so kurz, ich habe vergessen, wie absolut schrecklich es ist, mit PostgreSQL-Arrays in C zu arbeiten. Los geht'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);
}

und hängen Sie dies an contrib/intarray/intarray--1.0.sql an :

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

(genauer gesagt würden Sie intarray--1.1.sql erstellen und intarray--1.0--1.1.sql und aktualisieren Sie intarray.control . Dies ist nur ein schneller Hack.)

Verwendung:

make USE_PGXS=1
make USE_PGXS=1 install

zu kompilieren und zu installieren.

Jetzt DROP EXTENSION intarray; (falls Sie es bereits haben) und CREATE EXTENSION intarray; .

Sie haben jetzt die Aggregatfunktion sum_intarray_cols zur Verfügung (wie Ihr sum(int4[])). , sowie den Zwei-Operanden add_intarray_cols (wie Ihr array_add ).

Durch die Spezialisierung auf Integer-Arrays fällt eine ganze Reihe von Komplexitäten weg. Im aggregierten Fall wird ein Haufen Kopieren vermieden, da wir das Array "state" (das erste Argument) sicher an Ort und Stelle ändern können. Um die Dinge konsistent zu halten, erhalten wir im Falle eines nicht aggregierten Aufrufs eine Kopie des ersten Arguments, damit wir immer noch direkt damit arbeiten und es zurückgeben können.

Dieser Ansatz könnte verallgemeinert werden, um jeden Datentyp zu unterstützen, indem der fmgr-Cache verwendet wird, um die Add-Funktion für die interessierenden Typen usw. nachzuschlagen. Ich bin nicht besonders daran interessiert, also wenn Sie es brauchen (z. B. um Spalten von NUMERIC zu summieren Arrays) dann ... viel Spaß.

Wenn Sie mit unterschiedlichen Array-Längen umgehen müssen, können Sie wahrscheinlich anhand der obigen Ausführungen herausfinden, was zu tun ist.