Dlaczego?
dlaczego wersja C jest o wiele szybsza?
Tablica PostgreSQL sama w sobie jest dość nieefektywną strukturą danych. Może zawierać dowolne typ danych i może być wielowymiarowy, więc wiele optymalizacji jest po prostu niemożliwych. Jednak, jak widzieliście, możliwa jest znacznie szybsza praca z tą samą tablicą w C.
Dzieje się tak, ponieważ dostęp do tablicy w C może uniknąć wielu powtarzających się prac związanych z dostępem do tablicy PL/PgSQL. Wystarczy spojrzeć na src/backend/utils/adt/arrayfuncs.c
, array_ref
. Teraz spójrz, jak jest wywoływany z src/backend/executor/execQual.c
w ExecEvalArrayRef
. Który działa dla każdego indywidualnego dostępu do tablicy z PL/PgSQL, jak widać, dołączając gdb do pid znalezionego z select pg_backend_pid()
, ustawiając punkt przerwania w ExecEvalArrayRef
, kontynuując i uruchamiając swoją funkcję.
Co ważniejsze, w PL/PgSQL każda wykonywana instrukcja jest uruchamiana przez maszynę wykonującą zapytania. To sprawia, że małe, tanie wypowiedzi są dość powolne, nawet biorąc pod uwagę fakt, że są one przygotowane wcześniej. Coś takiego:
a := b + c
jest faktycznie wykonywany przez PL/PgSQL, bardziej jak:
SELECT b + c INTO a;
Możesz to zaobserwować, jeśli odpowiednio ustawisz poziom debugowania, podłączysz debuger i przerwiesz w odpowiednim momencie lub użyjesz auto_explain
moduł z zagnieżdżoną analizą instrukcji. Aby dać ci wyobrażenie o tym, jak duże obciążenie to nakłada, gdy wykonujesz wiele małych prostych instrukcji (takich jak dostęp do tablicy), spójrz na ten przykładowy ślad wsteczny i moje notatki na jego temat.
Istnieje również znaczna narzuta na rozpoczęcie działalności do każdego wywołania funkcji PL/PgSQL. Nie jest ogromny, ale wystarczy go zsumować, gdy jest używany jako agregat.
Szybsze podejście w C
W twoim przypadku prawdopodobnie zrobiłbym to w C, tak jak to zrobiłeś, ale unikałbym kopiowania tablicy, gdy jest wywoływana jako agregat. Możesz sprawdzić, czy jest on wywoływany w kontekście zagregowanym:
if (AggCheckCallContext(fcinfo, NULL))
a jeśli tak, użyj oryginalnej wartości jako zmiennej zastępczej, modyfikując ją, a następnie zwracając zamiast przydzielania nowej. Wkrótce napiszę demo, aby sprawdzić, czy jest to możliwe z tablicami... (aktualizacja) lub nie tak krótko, zapomniałem, jak okropna jest praca z tablicami PostgreSQL w C. Zaczynamy:
// 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);
}
i dołącz to do 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);
(bardziej poprawnie utworzyłbyś intarray--1.1.sql
i intarray--1.0--1.1.sql
i zaktualizuj intarray.control
. To tylko szybki hack.)
Użyj:
make USE_PGXS=1
make USE_PGXS=1 install
skompilować i zainstalować.
Teraz DROP EXTENSION intarray;
(jeśli już to masz) i CREATE EXTENSION intarray;
.
Będziesz mieć teraz funkcję agregującą sum_intarray_cols
dostępne dla ciebie (jak twój sum(int4[])
, a także dwuargumentowy add_intarray_cols
(jak twój array_add
).
Dzięki specjalizacji w tablicach liczb całkowitych znika cała złożoność. W przypadku zagregowanym unika się wielu kopii, ponieważ możemy bezpiecznie modyfikować tablicę „stanu” (pierwszy argument) na miejscu. Aby zachować spójność, w przypadku wywołania bez agregacji otrzymujemy kopię pierwszego argumentu, abyśmy mogli nadal pracować z nim w miejscu i zwracać go.
Takie podejście można uogólnić w celu obsługi dowolnego typu danych, używając pamięci podręcznej fmgr do wyszukiwania funkcji dodawania dla interesujących typów itp. Nie jestem szczególnie zainteresowany tym, więc jeśli tego potrzebujesz (powiedzmy, sumować kolumny NUMERIC
tablice), a następnie ... baw się dobrze.
Podobnie, jeśli potrzebujesz poradzić sobie z różnymi długościami tablic, prawdopodobnie możesz dowiedzieć się, co zrobić z powyższego.