59.3. Расширяемость
Реализация нового метода доступа индекса традиционно была большой и сложной задачей. Чтобы её решить, необходимо было понимать внутреннее устройство базы данных, в частности работу менеджера блокировок и журнала предзаписи. Но с интерфейсом GiST, реализующим высокий уровень абстракции, разработчик метода доступа должен реализовать только смысловое наполнение индексируемого типа данных. Уровень GiST берёт на себя заботу о параллельном доступе, поддержке журнала и поиске в структуре дерева.
Эту расширяемость не следует путать с расширяемостью других стандартных деревьев поиска в смысле поддержки различных типов данных. Например, Postgres Pro поддерживает расширяемость B-деревьев и индексов по хешу. Это означает, что в Postgres Pro вы можете построить B-дерево или хеш-таблицу по любому желаемому типу данных. Но такие B-деревья будут поддерживать только предикаты сравнений (<
, =
, >
), а индексы по хешу только запросы с равенством.
Поэтому, если вы проиндексируете в Postgres Pro в B-дереве, например, коллекцию изображений, вы сможете выполнять только проверки вида «равны ли изображения X и Y», «меньше ли изображение X изображения Y» и «больше ли изображение X изображения Y». Это может быть полезно, в зависимости от того, как вы определите операции «равно», «меньше» и «больше». Однако, используя индекс на базе GiST, возможно удовлетворять и запросы из предметной области, например, «найти все изображения лошадей» или «найти все пересвеченные изображения».
Всё, что нужно, чтобы получить работающий метод доступа GiST — это реализовать несколько методов, определяющих поведение ключей в дереве. Конечно, эти методы должны быть довольно изощрёнными, чтобы поддерживать изощрённые запросы, но для всех стандартных запросов (B-деревьев, R-деревьев и т. д.) они относительно просты. Словом, GiST сочетает расширяемость с универсальностью, повторным использованием кода и аккуратным интерфейсом.
Класс операторов индекса GiST должен предоставить семь методов и может дополнительно предоставлять ещё два. Корректность индекса обеспечивается реализацией методов same
, consistent
и union
, а его эффективность (по размеру и скорости) будет зависеть от методов penalty
и picksplit
. Последние два основных метода compress
и decompress
позволяют реализовать внутреннее представление данных дерева, не совпадающее с типом индексируемых данных. Данные листьев индекса должны быть индексируемого типа, тогда как в остальных узлах дерева могут быть произвольные структуры C (но при этом должны соблюдаться правила, предъявляемые Postgres Pro к типам данных; прочитайте о varlena
для данных переменного размера). Если внутренний тип данных дерева существует на уровне SQL, в команде CREATE OPERATOR CLASS
можно использовать указание STORAGE
. Необязательный восьмой метод distance
нужно реализовать, только если класс операторов желает поддерживать упорядоченные сканирования (поиск ближайших соседей). Необязательный девятый метод fetch
требуется, если класс операторов желает поддерживать сканирование только индекса.
consistent
Для переданной записи индекса
p
и значения запросаq
эта функция определяет, является ли запись индекса «соответствующей» запросу; то есть, может ли предикат «индексированный_столбец
индексируемый_оператор
q
» удовлетворяться для какой-либо строки, представленной данной записью индекса? Для записей на уровне листьев это равносильно проверке индексируемого условия, тогда как для внутреннего узла дерева требуется определить, нужно ли сканировать поддерево индекса, относящееся к данному узлу. Когда результатtrue
, также должен возвращаться флагrecheck
, показывающий, точно ли удовлетворяется предикат или это лишь потенциально возможно. Еслиrecheck
=false
, это означает, что индекс проверил условие предиката в точности, тогда как приrecheck
=true
проверяемая строка будет только кандидатом на совпадение. В этом случае система автоматически перепроверитиндексируемый_оператор
с действительным значением строки, чтобы окончательно определить, соответствует ли оно запросу. Благодаря этому GiST поддерживает индексы как точной, так и неточной структуры.В SQL эта функция должна объявляться примерно так:
CREATE OR REPLACE FUNCTION my_consistent(internal, data_type, smallint, oid, internal) RETURNS bool AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
А соответствующий код в модуле C может реализовываться по такому шаблону:
PG_FUNCTION_INFO_V1(my_consistent); Datum my_consistent(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); data_type *query = PG_GETARG_DATA_TYPE_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); /* Oid subtype = PG_GETARG_OID(3); */ bool *recheck = (bool *) PG_GETARG_POINTER(4); data_type *key = DatumGetDataType(entry->key); bool retval; /* * Определить возвращаемое значение как функцию стратегии, ключа и запроса. * * Вызовите GIST_LEAF(entry), чтобы узнать текущую позицию в дереве индекса, * что удобно, например для поддержки оператора = (вы можете проверить * равенство в листьях дерева и непустое пересечение в остальных * узлах). */ *recheck = true; /* или false, если проверка точная */ PG_RETURN_BOOL(retval); }
Здесь
key
указывает на элемент в индексе, аquery
— значение, искомое в индексе. ПараметрStrategyNumber
показывает, какой оператор из класса операторов применяется — он соответствует одному из номеров операторов, заданных в командеCREATE OPERATOR CLASS
. В зависимости от того, какие операторы включены в класс, тип данныхquery
может быть разным для разных операторов, но показанный выше шаблон на это не рассчитан.union
Этот метод консолидирует информацию в дереве. Получив набор записей, он должен сгенерировать в индексе новую запись, представляющие все эти записи.
В SQL эта функция должна объявляться так:
CREATE OR REPLACE FUNCTION my_union(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
И соответствующий код в модуле C должен реализовываться по такому шаблону:
PG_FUNCTION_INFO_V1(my_union); Datum my_union(PG_FUNCTION_ARGS) { GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0); GISTENTRY *ent = entryvec->vector; data_type *out, *tmp, *old; int numranges, i = 0; numranges = entryvec->n; tmp = DatumGetDataType(ent[0].key); out = tmp; if (numranges == 1) { out = data_type_deep_copy(tmp); PG_RETURN_DATA_TYPE_P(out); } for (i = 1; i < numranges; i++) { old = out; tmp = DatumGetDataType(ent[i].key); out = my_union_implementation(out, tmp); } PG_RETURN_DATA_TYPE_P(out); }
Как можно заметить, в этом шаблоне мы имеем дело с типом данных, для которого
union(X, Y, Z) = union(union(X, Y), Z)
. Достаточно просто можно поддержать и такие типы данных, для которых это не выполняется, реализовав соответствующий алгоритм объединения в этом опорном методе GiST.Функция, реализующая
union
, должна возвращать указатель на память, выделенную вызовомpalloc()
. Она не может просто вернуть то, что получила на вход.compress
Преобразует элемент данных в формат, подходящий для физического хранения в странице индекса.
В SQL эта функция должна объявляться так:
CREATE OR REPLACE FUNCTION my_compress(internal) RETURNS internal AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
И соответствующий код в модуле C должен реализовываться по такому шаблону:
PG_FUNCTION_INFO_V1(my_compress); Datum my_compress(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); GISTENTRY *retval; if (entry->leafkey) { /* заменить entry->key сжатой версией */ compressed_data_type *compressed_data = palloc(sizeof(compressed_data_type)); /* заполнить *compressed_data из entry->key ... */ retval = palloc(sizeof(GISTENTRY)); gistentryinit(*retval, PointerGetDatum(compressed_data), entry->rel, entry->page, entry->offset, FALSE); } else { /* обычно с записями внутренних узлов ничего делать не нужно */ retval = entry; } PG_RETURN_POINTER(retval); }
Разумеется,
compressed_data_type
(тип сжатых данных) нужно привести к нужному типу, при преобразовании в который будут сжиматься узлы на уровне листьев.decompress
Метод, обратный к
compress
. Преобразует представление элемента данных в индексе в формат, с которым могут работать другие методы GiST в классе операторов.В SQL эта функция должна объявляться так:
CREATE OR REPLACE FUNCTION my_decompress(internal) RETURNS internal AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
И соответствующий код в модуле C должен реализовываться по такому шаблону:
PG_FUNCTION_INFO_V1(my_decompress); Datum my_decompress(PG_FUNCTION_ARGS) { PG_RETURN_POINTER(PG_GETARG_POINTER(0)); }
Этот шаблон подходит для случая, когда преобразовывать данные не нужно.
penalty
Возвращает значение, выражающее «стоимость» добавления новой записи в конкретную ветвь дерева. Элементы будут вставляться по тому направлению в дереве, для которого значение
penalty
минимально. Результатыpenalty
должны быть неотрицательными; если возвращается отрицательное значение, оно воспринимается как ноль.В SQL эта функция должна объявляться так:
CREATE OR REPLACE FUNCTION my_penalty(internal, internal, internal) RETURNS internal AS 'MODULE_PATHNAME' LANGUAGE C STRICT; -- в некоторых случаях функции стоимости не должны быть строгими
И соответствующий код в модуле C должен реализовываться по такому шаблону:
PG_FUNCTION_INFO_V1(my_penalty); Datum my_penalty(PG_FUNCTION_ARGS) { GISTENTRY *origentry = (GISTENTRY *) PG_GETARG_POINTER(0); GISTENTRY *newentry = (GISTENTRY *) PG_GETARG_POINTER(1); float *penalty = (float *) PG_GETARG_POINTER(2); data_type *orig = DatumGetDataType(origentry->key); data_type *new = DatumGetDataType(newentry->key); *penalty = my_penalty_implementation(orig, new); PG_RETURN_POINTER(penalty); }
Функция
penalty
важна для хорошей производительности индекса. Она будет вызываться во время добавления записи, чтобы выбрать ветвь для дальнейшего движения, когда в дерево нужно добавить новый элемент. Это имеет значение во время запроса, так как чем более сбалансирован индекс, тем быстрее будет поиск в нём.picksplit
Когда необходимо разделить страницу индекса, эта функция решает, какие записи должны остаться в старой странице, а какие нужно перенести в новую.
В SQL эта функция должна объявляться так:
CREATE OR REPLACE FUNCTION my_picksplit(internal, internal) RETURNS internal AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
И соответствующий код в модуле C должен реализовываться по такому шаблону:
PG_FUNCTION_INFO_V1(my_picksplit); Datum my_picksplit(PG_FUNCTION_ARGS) { GistEntryVector *entryvec = (GistEntryVector *) PG_GETARG_POINTER(0); OffsetNumber maxoff = entryvec->n - 1; GISTENTRY *ent = entryvec->vector; GIST_SPLITVEC *v = (GIST_SPLITVEC *) PG_GETARG_POINTER(1); int i, nbytes; OffsetNumber *left, *right; data_type *tmp_union; data_type *unionL; data_type *unionR; GISTENTRY **raw_entryvec; maxoff = entryvec->n - 1; nbytes = (maxoff + 1) * sizeof(OffsetNumber); v->spl_left = (OffsetNumber *) palloc(nbytes); left = v->spl_left; v->spl_nleft = 0; v->spl_right = (OffsetNumber *) palloc(nbytes); right = v->spl_right; v->spl_nright = 0; unionL = NULL; unionR = NULL; /* Инициализировать чистый вектор записи. */ raw_entryvec = (GISTENTRY **) malloc(entryvec->n * sizeof(void *)); for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i)) raw_entryvec[i] = &(entryvec->vector[i]); for (i = FirstOffsetNumber; i <= maxoff; i = OffsetNumberNext(i)) { int real_index = raw_entryvec[i] - entryvec->vector; tmp_union = DatumGetDataType(entryvec->vector[real_index].key); Assert(tmp_union != NULL); /* * Выбрать, куда помещать записи индекса и изменить unionL и unionR * соответственно. Добавить записи в v->spl_left или * v->spl_right и увеличить счётчики. */ if (my_choice_is_left(unionL, curl, unionR, curr)) { if (unionL == NULL) unionL = tmp_union; else unionL = my_union_implementation(unionL, tmp_union); *left = real_index; ++left; ++(v->spl_nleft); } else { /* * То же самое с правой стороной */ } } v->spl_ldatum = DataTypeGetDatum(unionL); v->spl_rdatum = DataTypeGetDatum(unionR); PG_RETURN_POINTER(v); }
Как и
penalty
, функцияpicksplit
важна для хорошей производительности индекса. Сложность создания быстродействующих индексов GiST заключается как раз в разработке подходящих реализацийpenalty
иpicksplit
.same
Возвращает true, если два элемента индекса равны, и false в противном случае.
В SQL эта функция должна объявляться так:
CREATE OR REPLACE FUNCTION my_same(internal, internal, internal) RETURNS internal AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
И соответствующий код в модуле C должен реализовываться по такому шаблону:
PG_FUNCTION_INFO_V1(my_same); Datum my_same(PG_FUNCTION_ARGS) { prefix_range *v1 = PG_GETARG_PREFIX_RANGE_P(0); prefix_range *v2 = PG_GETARG_PREFIX_RANGE_P(1); bool *result = (bool *) PG_GETARG_POINTER(2); *result = my_eq(v1, v2); PG_RETURN_POINTER(result); }
По историческим причинам функция
same
не возвращает булевский результат; вместо этого она должна сохранить флаг в переменной, на которую указывает третий аргумент.distance
Для переданной записи индекса
p
и значения запросаq
эта функция определяет «дистанцию» от записи индекса до значения в запросе. Эта функция должна быть представлена, если класс операторов содержит какие-либо операторы упорядочивания. Запрос с оператором упорядочивания будет выполняться так, чтобы записи индекса с наименьшей «дистанцией» возвращались первыми, так что результаты должны согласовываться со смысловым значением оператора. Для записи на уровне листьев результат представляет только дистанцию до этой записи, а для внутреннего узла дерева это будет минимальная дистанция, которая может быть получена среди всех его потомков.В SQL эта функция должна объявляться так:
CREATE OR REPLACE FUNCTION my_distance(internal, data_type, smallint, oid) RETURNS float8 AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
И соответствующий код в модуле C должен реализовываться по такому шаблону:
PG_FUNCTION_INFO_V1(my_distance); Datum my_distance(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); data_type *query = PG_GETARG_DATA_TYPE_P(1); StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2); /* Oid subtype = PG_GETARG_OID(3); */ /* bool *recheck = (bool *) PG_GETARG_POINTER(4); */ data_type *key = DatumGetDataType(entry->key); double retval; /* * определить возвращаемое значение как функцию стратегии, ключа и запроса. */ PG_RETURN_FLOAT8(retval); }
Функции
distance
передаются те же аргументы, что и функцииconsistent
.При определении дистанции допускается некоторая неточность, если результат никогда не будет превышать действительную дистанцию до элемента. Так, например, в геометрических приложениях бывает достаточно определить дистанцию до описанного прямоугольника. Для внутреннего узла дерева результат не должен превышать дистанцию до любого из его дочерних узлов. Если возвращаемая дистанция неточная, функция должна установить флаг
*recheck
. (Это необязательно для внутренних узлов дерева; для них результат всегда считается неточным.) В этом случае исполнитель вычислит точную дистанцию, выбрав кортеж из кучи, и переупорядочит кортежи при необходимости.Если функция distance возвращает
*recheck = true
для любого узла на уровне листьев, типом результата исходного оператора упорядочивания должен бытьfloat8
илиfloat4
, и значения результата функции distance должны быть сравнимы с результатами исходного оператора упорядочивания, так как исполнитель будет выполнять сортировку, используя и результаты функции distance, и уточнённые результаты оператора упорядочивания. В противном случае значениями результата distance могут быть любые конечные значенияfloat8
, при условии, что относительный порядок значений результата соответствует порядку, который даёт оператор упорядочивания. (Значения бесконечность и минус бесконечность применяются внутри для особых случаев, например, представления NULL, поэтому возвращать такие значения из функцийdistance
не рекомендуется.)fetch
Преобразует сжатое представление элемента данных в индексе в исходный тип данных, для сканирования только индекса. Возвращаемые данные должны быть точной, не примерной копией изначально проиндексированного значения.
В SQL эта функция должна объявляться так:
CREATE OR REPLACE FUNCTION my_fetch(internal) RETURNS internal AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
В качестве аргумента ей передаётся указатель на структуру
GISTENTRY
. При вызове её поле 'key' содержит данные листа в сжатой форме (не NULL). Возвращаемое значение — ещё одна структураGISTENTRY
, в которой поле 'key' содержит те же данные в исходной, развёрнутой форме. Если функция compress класса операторов не делает с данными листьев ничего, метод fetch может возвратить аргумент без изменений.Соответствующий код в модуле C должен реализовываться по такому шаблону:
PG_FUNCTION_INFO_V1(my_fetch); Datum my_fetch(PG_FUNCTION_ARGS) { GISTENTRY *entry = (GISTENTRY *) PG_GETARG_POINTER(0); input_data_type *in = DatumGetPointer(entry->key); fetched_data_type *fetched_data; GISTENTRY *retval; retval = palloc(sizeof(GISTENTRY)); fetched_data = palloc(sizeof(fetched_data_type)); /* * Преобразовать структуру 'fetched_data' в Datum исходного типа данных. */ /* Заполнить *retval из fetch_data. */ gistentryinit(*retval, PointerGetDatum(converted_datum), entry->rel, entry->page, entry->offset, FALSE); PG_RETURN_POINTER(retval); }
Если метод сжатия является неточным для записей уровня листьев, такой класс операторов не может поддерживать сканирование только индекса и не должен определять функцию fetch.
Все опорные методы GiST обычно вызываются в кратковременных контекстах памяти; то есть, CurrentMemoryContext
сбрасывается после обработки каждого кортежа. Таким образом можно не заботиться об освобождении любых блоков памяти, выделенных функцией palloc. Однако в некоторых случаях для опорного метода полезно кешировать какие-либо данные между вызовами. Для этого нужно разместить долгоживущие данные в контексте fcinfo->flinfo->fn_mcxt
и сохранить указатель на них в fcinfo->flinfo->fn_extra
. Такие данные смогут просуществовать всё время операции с индексом (например, одно сканирование индекса GiST, построение индекса или добавление кортежа в индекс). Не забудьте вызвать pfree для предыдущего значения, заменяя значение в fn_extra
, чтобы не допустить накопления утечек памяти в ходе операции.