sql >> Base de Datos >  >> RDS >> PostgreSQL

¿Por qué el acceso a la matriz de PostgreSQL es mucho más rápido en C que en PL/pgSQL?

¿Por qué?

¿Por qué la versión C es mucho más rápida?

Una matriz de PostgreSQL es en sí misma una estructura de datos bastante ineficiente. Puede contener cualquier tipo de datos y es capaz de ser multidimensional, por lo que muchas optimizaciones simplemente no son posibles. Sin embargo, como ha visto, es posible trabajar con la misma matriz mucho más rápido en C.

Esto se debe a que el acceso a matrices en C puede evitar gran parte del trabajo repetido que implica el acceso a matrices PL/PgSQL. Solo eche un vistazo a src/backend/utils/adt/arrayfuncs.c , array_ref . Ahora mire cómo se invoca desde src/backend/executor/execQual.c en ExecEvalArrayRef . Que se ejecuta para cada acceso de matriz individual de PL/PgSQL, como puede ver adjuntando gdb al pid que se encuentra en select pg_backend_pid() , estableciendo un punto de interrupción en ExecEvalArrayRef , continuar y ejecutar su función.

Más importante aún, en PL/PgSQL, cada declaración que ejecuta se ejecuta a través de la maquinaria del ejecutor de consultas. Esto hace que las declaraciones pequeñas y baratas sean bastante lentas, incluso teniendo en cuenta el hecho de que están preparadas previamente. Algo como:

a := b + c

en realidad es ejecutado por PL/PgSQL más como:

SELECT b + c INTO a;

Puede observar esto si aumenta los niveles de depuración lo suficiente, adjunta un depurador y rompe en un punto adecuado, o usa el auto_explain módulo con análisis de declaraciones anidadas. Para darle una idea de la sobrecarga que impone esto cuando está ejecutando muchas declaraciones pequeñas y simples (como accesos a matrices), eche un vistazo a este ejemplo de seguimiento y mis notas al respecto.

También hay una importante gastos generales de puesta en marcha a cada invocación de función PL/PgSQL. No es enorme, pero es suficiente para sumar cuando se usa como un agregado.

Un enfoque más rápido en C

En su caso, probablemente lo haría en C, como lo ha hecho, pero evitaría copiar la matriz cuando se llame como un agregado. Puede verificar si se está invocando en un contexto agregado:

if (AggCheckCallContext(fcinfo, NULL))

y si es así, use el valor original como un marcador de posición mutable, modificándolo y luego devolviéndolo en lugar de asignar uno nuevo. Escribiré una demostración para verificar que esto es posible con arreglos en breve... (actualización) o no tan pronto, olvidé lo absolutamente horrible que es trabajar con arreglos de PostgreSQL en C. Aquí vamos:

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

y agregue esto 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);

(más correctamente crearía intarray--1.1.sql y intarray--1.0--1.1.sql y actualice intarray.control . Esto es solo un truco rápido).

Usar:

make USE_PGXS=1
make USE_PGXS=1 install

para compilar e instalar.

Ahora DROP EXTENSION intarray; (si ya lo tienes) y CREATE EXTENSION intarray; .

Ahora tendrá la función agregada sum_intarray_cols disponible para usted (como su sum(int4[]) , así como los dos operandos add_intarray_cols (como su array_add ).

Al especializarse en matrices de enteros, desaparece una gran cantidad de complejidad. Se evita un montón de copias en el caso agregado, ya que podemos modificar de forma segura la matriz de "estado" (el primer argumento) en el lugar. Para mantener la coherencia, en el caso de una invocación no agregada, obtenemos una copia del primer argumento para poder seguir trabajando con él en el lugar y devolverlo.

Este enfoque podría generalizarse para admitir cualquier tipo de datos mediante el uso de la memoria caché fmgr para buscar la función de agregar para los tipos de interés, etc. No estoy particularmente interesado en hacer eso, así que si lo necesita (digamos, para sumar columnas de NUMERIC arrays) entonces... diviértete.

Del mismo modo, si necesita manejar diferentes longitudes de matriz, probablemente pueda averiguar qué hacer con lo anterior.