64.3. Индексы SP-GiST #
64.3.1. Введение #
Аббревиатура SP-GiST расшифровывается как «Space-Partitioned GiST» (GiST с секционированием пространства). SP-GiST поддерживает деревья поиска с секционированием, что облегчает разработку широкого спектра различных несбалансированных структур данных, в том числе деревьев квадрантов, а также k-мерных и префиксных деревьев. Общей характеристикой этих структур является то, что они последовательно разбивают пространство поиска на сегменты, которые не обязательно должны быть равного размера. При этом поиск, хорошо соответствующий правилу секционирования, с таким индексом может быть очень быстрым.
Эти популярные структуры данных изначально конструировались для работы в памяти. При таком применении они обычно представляются в виде набора динамически выделяемых узлов, связываемых указателями. Однако подобную схему нельзя в таком виде перенести на диск, так как цепочки указателей могут быть довольно длинными, и поэтому потребуется слишком много обращений к диску. Структуры данных для хранения на диске, напротив, должны иметь большую разветвлённость для минимизации объёма ввода-вывода. Для решения этой задачи SP-GiST сопоставляет узлы дерева поиска со страницами на диске так, чтобы при поиске требовалось обращаться только к нескольким страницам на диске, даже если при этом нужно просмотреть множество узлов.
Как и GiST, SP-GiST призван дать возможность разрабатывать дополнительные типы данных с соответствующими методами доступа экспертам в предметной области типа данных, а не специалистам по СУБД.
Представленная здесь информация частично позаимствована с сайта Проекта индексации SP-GiST Университета Пердью. Сопровождением реализации SP-GiST в PostgreSQL в основном занимаются Фёдор Сигаев и Олег Бартунов; дополнительные сведения можно получить на их сайте.
64.3.2. Встроенные классы операторов #
В базовый дистрибутив PostgreSQL включены классы операторов SP-GiST, перечисленные в Таблице 64.2.
Таблица 64.2. Встроенные классы операторов SP-GiST
| Имя | Индексируемые операторы | Операторы упорядочивания |
|---|---|---|
box_ops | << (box,box) | <-> (box,point) |
&< (box,box) | ||
&> (box,box) | ||
>> (box,box) | ||
<@ (box,box) | ||
@> (box,box) | ||
~= (box,box) | ||
&& (box,box) | ||
<<| (box,box) | ||
&<| (box,box) | ||
|&> (box,box) | ||
|>> (box,box) | ||
inet_ops | << (inet,inet) | |
<<= (inet,inet) | ||
>> (inet,inet) | ||
>>= (inet,inet) | ||
= (inet,inet) | ||
<> (inet,inet) | ||
< (inet,inet) | ||
<= (inet,inet) | ||
> (inet,inet) | ||
>= (inet,inet) | ||
&& (inet,inet) | ||
kd_point_ops | |>> (point,point) | <-> (point,point) |
<< (point,point) | ||
>> (point,point) | ||
<<| (point,point) | ||
~= (point,point) | ||
<@ (point,box) | ||
poly_ops | << (polygon,polygon) | <-> (polygon,point) |
&< (polygon,polygon) | ||
&> (polygon,polygon) | ||
>> (polygon,polygon) | ||
<@ (polygon,polygon) | ||
@> (polygon,polygon) | ||
~= (polygon,polygon) | ||
&& (polygon,polygon) | ||
<<| (polygon,polygon) | ||
&<| (polygon,polygon) | ||
|>> (polygon,polygon) | ||
|&> (polygon,polygon) | ||
quad_point_ops | |>> (point,point) | <-> (point,point) |
<< (point,point) | ||
>> (point,point) | ||
<<| (point,point) | ||
~= (point,point) | ||
<@ (point,box) | ||
range_ops | = (anyrange,anyrange) | |
&& (anyrange,anyrange) | ||
@> (anyrange,anyelement) | ||
@> (anyrange,anyrange) | ||
<@ (anyrange,anyrange) | ||
<< (anyrange,anyrange) | ||
>> (anyrange,anyrange) | ||
&< (anyrange,anyrange) | ||
&> (anyrange,anyrange) | ||
-|- (anyrange,anyrange) | ||
text_ops | = (text,text) | |
< (text,text) | ||
<= (text,text) | ||
> (text,text) | ||
>= (text,text) | ||
~<~ (text,text) | ||
~<=~ (text,text) | ||
~>=~ (text,text) | ||
~>~ (text,text) | ||
^@ (text,text) |
Из двух классов операторов для типа point классом по умолчанию является quad_point_ops. Класс kd_point_ops поддерживает те же операторы, но использует другую структуру данных индекса, которая может дать выигрыш в скорости для некоторых приложений.
Классы операторов quad_point_ops, kd_point_ops и poly_ops поддерживают оператор упорядочивания <->, позволяющий выполнить поиск k ближайших соседей (k-NN) по индексированному набору точек или многоугольников.
64.3.3. Расширяемость #
SP-GiST предлагает интерфейс с высоким уровнем абстракции и таким образом требует от разработчика метода доступа реализовать только методы, специфичные для конкретного типа данных. Ядро SP-GiST отвечает за эффективную схему обращений к диску и поиск в структуре дерева, а также берёт на себя заботу о параллельном доступе и поддержке журнала.
Кортежи в листьях дерева SP-GiST обычно содержат значения того же типа данных, что и индексируемый столбец, но могут содержать и неточное представление индексируемого столбца. На верхнем уровне эти кортежи содержат исходное индексируемое значение данных, но на более нижних могут содержать только часть значения, например, суффикс. В этом случае опорные функции класса операторов должны уметь восстанавливать исходное значение, собирая его из внутренних кортежей, которые нужно пройти для достижения уровня конкретного листа.
Когда создаётся индекс SP-GiST с неключевыми столбцами (INCLUDE), значения этих столбцов также будут храниться в кортежах уровня листьев. Класс операторов SP-GiST не обращает внимания на неключевые столбцы, поэтому здесь они рассматриваться не будут.
Внутренние кортежи устроены сложнее, так как они представляют собой точки разветвления в дереве поиска. Каждый внутренний кортеж содержит набор из одного или нескольких узлов, представляющих группы сходных значений листьев. Узел содержит ответвление, приводящее либо к другому, внутреннему кортежу нижнего уровня, либо к короткому списку кортежей в листьях, лежащих в одной странице индекса. Для каждого узла обычно задаётся метка, описывающая его; например, в префиксном дереве меткой может быть очередной символ в строковом значении. (С другой стороны, класс операторов может опускать метки узлов, если он имеет дело с фиксированным набором узлов во всех внутренних кортежах; см. Подраздел 64.3.4.2.) Дополнительно внутренний кортеж может хранить префикс, описывающий все его члены. В префиксном дереве это может быть общий префикс всех представленных ниже строк. Значением префикса не обязательно должен быть префикс, а могут быть любые данные, требующиеся классу операторов; например, в дереве квадрантов это может быть центральная точка, от которой отмеряются четыре квадранта. В этом случае внутренний кортеж дерева квадрантов будет также содержать четыре узла, соответствующие квадрантам вокруг этой центральной точки.
Некоторые алгоритмы деревьев требует знания уровня (или глубины) текущего кортежа, так что ядро SP-GiST даёт возможность классам операторов контролировать число уровней при спуске по дереву. Также имеется поддержка пошагового восстановления представленного значения, когда это требуется, и передачи вниз дополнительных данных (так называемых переходящих значений) при спуске.
Примечание
Ядро SP-GiST берёт на себя заботу о значениях NULL. Хотя в индексах SP-GiST не хранятся записи для NULL в индексируемых столбцах, это скрыто от кода класса операторов; записи индексов или условия поиска с NULL никогда не передаются методам класса операторов. (Предполагается, что операторы SP-GiST строгие и не могут возвращать положительный результат для значений NULL.) Поэтому значения NULL здесь больше обсуждаться не будут.
Класс операторов индекса для SP-GiST должен предоставить пять методов и может дополнительно предоставить ещё два. Все пять обязательных методов должны по единому соглашению принимать два аргумента internal, первым из которых будет указатель на структуру C, содержащую входные значения для опорного метода, а вторым — указатель на структуру C, в которую должны помещаться выходные значения. Четыре из этих методов должны возвращать просто void, так как их результаты помещаются в выходную структуру; однако leaf_consistent возвращает результат boolean. Эти методы не должны менять никакие поля в их входных структурах. Выходная структура всегда обнуляется перед вызовом пользовательского метода. Необязательный шестой метод compress принимает в единственном аргументе значение datum, подлежащее индексированию, и возвращает значение, подходящее для физического хранения в кортеже уровня листьев. Необязательный седьмой метод options принимает указатель типа internal на структуру C, в которую должны помещаться параметры для класса операторов, и возвращает void.
Пользователь должен определить следующие пять обязательных методов:
configВозвращает статическую информацию о реализации индекса, включая OID типов данных префикса и метки узла.
В SQL эта функция должна объявляться так:
CREATE FUNCTION my_config(internal, internal) RETURNS void ...
В первом аргументе передаётся указатель на структуру
spgConfigInязыка C, содержащие входные данные для функции. Во втором аргументе передаётся указатель на структуруspgConfigOutязыка C, в которую функция должна поместить результат.typedef struct spgConfigIn { Oid attType; /* Индексируемый тип данных */ } spgConfigIn; typedef struct spgConfigOut { Oid prefixType; /* Тип данных префикса во внутренних кортежах */ Oid labelType; /* Тип данных метки узла во внутренних кортежах */ Oid leafType; /* Тип данных в кортежах уровня листьев */ bool canReturnData; /* Класс операторов может восстановить исходные данные */ bool longValuesOK; /* Класс может принимать значения, не умещающиеся на одной странице */ } spgConfigOut;Поле
attTypeпередаётся для поддержки полиморфных классов операторов; для обычных классов операторов с фиксированным типом оно будет всегда содержать одно значение и поэтому его можно просто игнорировать.Для классов операторов, не использующих префиксы, в
prefixTypeможно установитьVOIDOID. Подобным образом, для классов операторов, не использующих метки узлов, вlabelTypeтоже можно установитьVOIDOID. ПризнакcanReturnDataследует установить, если класс операторов может восстановить изначально переданное в индекс значение. ПризнакlongValuesOKдолжен устанавливаться, только еслиattTypeпеременной длины и класс операторов может фрагментировать длинные значения, повторяя суффиксы (см. Подраздел 64.3.4.1).Значение
leafTypeдолжно соответствовать типу хранения индекса, заданному в полеopckeytypeзаписи, которая описывает этот класс операторов в каталоге. (Обратите внимание, чтоopckeytypeможет быть нулевым, тогда тип хранения определяется входным типом класса операторов, как бывает чаще всего.) Для обеспечения обратной совместимости методуconfigразрешается задатьleafTypeдругое значение (которое и будет использоваться); в результате содержимое индекса нельзя будет правильно идентифицировать в каталогах, поэтому данная возможность считается устаревшей. Кроме того, допускается оставитьleafTypeнеинициализированным (нулевым), что будет означать, что тип хранения индекса определяется значениемopckeytype.Когда
attTypeиleafTypeразличаются, должен предоставляться необязательный методcompress. Методcompressотвечает за преобразование данных, подлежащих индексации, из типаattTypeв типleafType.chooseВыбирает метод для добавления нового значения во внутренний кортеж.
В SQL эта функция должна объявляться так:
CREATE FUNCTION my_choose(internal, internal) RETURNS void ...
В первом аргументе передаётся указатель на структуру
spgChooseInязыка C, содержащую входные данные для функции. Во втором аргументе передаётся указатель на структуруspgChooseOut, в которую функция должна поместить результат.typedef struct spgChooseIn { Datum datum; /* исходное значение, которое должно индексироваться */ Datum leafDatum; /* текущее значение, которое должно сохраниться в листе */ int level; /* текущий уровень (начиная с нуля) */ /* Данные из текущего внутреннего кортежа */ bool allTheSame; /* кортеж с признаком все-равны? */ bool hasPrefix; /* у кортежа есть префикс? */ Datum prefixDatum; /* если да, то это значение префикса */ int nNodes; /* число узлов во внутреннем кортеже */ Datum *nodeLabels; /* значения меток узлов (NULL, если их нет) */ } spgChooseIn; typedef enum spgChooseResultType { spgMatchNode = 1, /* спуститься в существующий узел */ spgAddNode, /* добавить узел во внутренний кортеж */ spgSplitTuple /* разделить внутренний кортеж (изменить его префикс) */ } spgChooseResultType; typedef struct spgChooseOut { spgChooseResultType resultType; /* код действия, см. выше */ union { struct /* результаты для spgMatchNode */ { int nodeN; /* спуститься к этому узлу (нумерация с 0) */ int levelAdd; /* шаг увеличения уровня */ Datum restDatum; /* новое значение листа */ } matchNode; struct /* результаты для spgAddNode */ { Datum nodeLabel; /* метка нового узла */ int nodeN; /* куда вставлять её (нумерация с 0) */ } addNode; struct /* результаты для spgSplitTuple */ { /* Информация для формирования нового внутреннего кортежа верхнего уровня с одним дочерним кортежем */ bool prefixHasPrefix; /* кортеж должен иметь префикс? */ Datum prefixPrefixDatum; /* если да, его значение */ int prefixNNodes; /* число узлов */ Datum *prefixNodeLabels; /* их метки (или NULL, если * меток нет) */ int childNodeN; /* узел, который получит дочерний кортеж */ /* Информация для формирования нового внутреннего кортежа нижнего уровня со всеми старыми узлами */ bool postfixHasPrefix; /* кортеж должен иметь префикс? */ Datum postfixPrefixDatum; /* если да, его значение */ } splitTuple; } result; } spgChooseOut;В
datumпередаётся исходное значение типаspgConfigIn.attType, которое должно быть вставлено в индекс. ВleafDatumсодержится значение типаspgConfigOut.leafType, изначально представляющее собой результат методаcompress, применённого кdatum, если методcompressреализован, а иначе — собственно значениеdatum.leafDatumможет быть другим на нижних уровнях дерева, если его изменят функцииchooseилиpicksplit. Когда поиск места добавления достигает страницы уровня листа, в создаваемом кортеже листа сохраняется текущее значениеleafDatum. Вlevelзадаётся текущий уровень внутреннего кортежа, начиная с нуля для уровня корня. ПризнакallTheSameустанавливается, если текущий внутренний кортеж содержит несколько равнозначных узлов (см. Подраздел 64.3.4.3). ПризнакhasPrefixустанавливается, если текущий внутренний кортеж содержит префикс; в этом случае вprefixDatumзадаётся его значение. ПолеnNodesзадаёт число дочерних узлов, содержащихся во внутреннем кортеже, аnodeLabelsпредставляет массив их меток или NULL, если меток у них нет.Функция
chooseможет определить, соответствует ли новое значение одному из существующих дочерних узлов, или что нужно добавить новый дочерний узел, или что новое значение не согласуется с префиксом кортежа и внутренний кортеж нужно разделить, чтобы получить менее ограничивающий префикс.Если новое значение соответствует одному из существующих дочерних узлов, установите в
resultTypeзначениеspgMatchNode. Установите вnodeNномер этого узла в массиве узлов (нумерация начинается с нуля). Установите вlevelAddзначение, на которое должен увеличиваться уровень (level) при спуске через этот узел, либо оставьте его нулевым, если класс операторов не отслеживает уровни. УстановитеrestDatum, равнымleafDatum, если класс операторов не меняет значения данных от уровня к уровню, а в противном случае запишите в него изменённое значение, которое должно использоваться в качествеleafDatumна следующем уровне.Если нужно добавить новый дочерний узел, установите в
resultTypeзначениеspgAddNode. ВnodeLabelзадайте метку для нового узла, а вnodeNпозицию (отсчитываемую от нуля), в которую должен вставляться узел в массиве узлов. После того как узел будет добавлен, функцияchooseвызывается снова с изменённым внутренним кортежем; в результате этого вызова должен быть получен результатspgMatchNode.Если новое значение не согласуется с префиксом кортежа, установите в
resultTypeзначениеspgSplitTuple. Это действие приводит к перемещению всех существующих узлов в новый внутренний кортеж нижнего уровня и замене существующего внутреннего кортежа кортежем, содержащим одну ссылку вниз на новый внутренний кортеж. Установите признакprefixHasPrefix, чтобы указать, должен ли новый верхний кортеж иметь префикс, и если да, задайте вprefixPrefixDatumзначение префикса. Это новое значение префикса должно быть в достаточной мере менее ограничивающим, чем исходное, чтобы в индекс было принято новое значение. Запишите вprefixNNodesчисло требующихся узлов в новом кортеже, а вprefixNodeLabels— указатель на выделенный через palloc массив с их метками или NULL, если метки узлов не нужны. Заметьте, что общий размер нового кортежа верхнего уровня не должен превышать общий размер кортежа, который он замещает; это ограничивает длины нового префикса и новых меток. Установите вchildNodeNиндекс (начиная с нуля) узла, который будет ссылаться на новый внутренний кортеж нижнего уровня. Установите признакpostfixHasPrefix, чтобы указать, должен ли новый внутренний кортеж нижнего уровня иметь префикс, и если да, задайте вpostfixPrefixDatumзначение префикса. Сочетание этих двух префиксов и метки узла, ссылающегося вниз, (если она есть) должно иметь то же значение, что и исходный префикс, так как нет возможности ни изменить метки узлов, перемещённых в новый кортеж нижнего уровня, ни изменить какие-либо нижние записи индекса. После того как узел разделён, функцияchooseбудет вызвана снова с заменяемым внутренним кортежем. При этом вызове может быть возвращён результатspgAddNode, если подходящий узел не был создан действиемspgSplitTuple. В конце концовchooseдолжна вернутьspgMatchNode, чтобы операция добавления могла перейти на следующий уровень.picksplitВыбирает, как создать новый внутренний кортеж по набору кортежей в листьях.
В SQL эта функция должна объявляться так:
CREATE FUNCTION my_picksplit(internal, internal) RETURNS void ...
В первом аргументе передаётся указатель на структуру
spgPickSplitInязыка C, содержащую входные данные для функции. Во втором аргументе передаётся указатель на структуруspgPickSplitOutязыка C, в которую функция должна поместить результат.typedef struct spgPickSplitIn { int nTuples; /* число кортежей в листьях */ Datum *datums; /* их значения (массив длины nTuples) */ int level; /* текущий уровень (отсчитывая от 0) */ } spgPickSplitIn; typedef struct spgPickSplitOut { bool hasPrefix; /* новый внутренний кортеж должен иметь префикс? */ Datum prefixDatum; /* если да, его значение */ int nNodes; /* число узлов для нового внутреннего кортежа */ Datum *nodeLabels; /* их метки (или NULL, если их нет) */ int *mapTuplesToNodes; /* номер узла для каждого кортежа в листе */ Datum *leafTupleDatums; /* значения, помещаемые в каждый новый кортеж */ } spgPickSplitOut;В
nTuplesзадаётся число предоставленных кортежей уровня листьев, а вdatums— массив их значений типаspgConfigOut.leafType. Вlevelуказывается текущий уровень, который должны разделять все кортежи листьев, и который станет уровнем нового внутреннего кортежа.Установите признак
hasPrefix, чтобы указать, должен ли новый внутренний кортеж иметь префикс, и если да, задайте вprefixDatumзначение префикса. Установите вnNodesколичество узлов, которые будут содержаться во внутреннем кортеже, а вnodeLabels— массив значений их меток либо NULL, если узлам не нужны метки. Поместите вmapTuplesToNodesуказатель на массив, назначающий номера узлов (начиная с нуля) каждому кортежу листа. ВleafTupleDatumsпередайте массив значений, которые должны быть сохранены в новых кортежах листьев (они будут совпадать со входными значениями (datums), если класс операторов не изменяет значения от уровня к следующему). Заметьте, что функцияpicksplitсама должна выделить память, используя palloc, для массивовnodeLabels,mapTuplesToNodesиleafTupleDatums.Если передаётся несколько кортежей листьев, ожидается, что функция
picksplitклассифицирует их и разделит на несколько узлов; иначе нельзя будет разнести кортежи листьев по разным страницам, что является конечной целью этой операции. Таким образом, еслиpicksplitв итоге помещает все кортежи листьев в один узел, ядро SP-GiST меняет это решение и создаёт внутренний кортеж, в котором кортежи листьев связываются случайным образом с несколькими узлами с одинаковыми метками. Такой кортеж помечается флагомallTheSame, показывающим, что все узлы равны. Функцииchooseиinner_consistentдолжны работать с такими внутренними кортежами особым образом. За дополнительными сведениями обратитесь к Подразделу 64.3.4.3.picksplitможет применяться к одному кортежу на уровне листьев, только когда функцияconfigустановила вlongValuesOKзначение true и было передано входное значение, большее страницы. В этом случае цель операции — отделить префикс и получить новое, более короткое значение для листа. Этот вызов будет повторяться, пока значение уровня листа не уменьшится настолько, чтобы уместиться в странице. За дополнительными сведениями обратитесь к Подразделу 64.3.4.1.inner_consistentВозвращает набор узлов (ветвей), по которым надо продолжать поиск.
В SQL эта функция должна объявляться так:
CREATE FUNCTION my_inner_consistent(internal, internal) RETURNS void ...
В первом аргументе передаётся указатель на структуру
spgInnerConsistentInязыка C, содержащую входные данные для функции. Во втором аргументе передаётся указатель на структуруspgInnerConsistentOutязыка C, в которую функция должна поместить результат.typedef struct spgInnerConsistentIn { ScanKey scankeys; /* массив операторов и искомых значений */ ScanKey orderbys; /* массив операторов упорядочивания и * сравниваемых значений */ int nkeys; /* длина массива scankeys */ int norderbys; /* длина массива orderbys */ Datum reconstructedValue; /* значение, восстановленное для родителя */ void *traversalValue; /* переходящее значение, специфичное для класса операторов */ MemoryContext traversalMemoryContext; /* переходящие значения нужно помещать сюда */ int level; /* текущий уровень (отсчитывается от нуля) */ bool returnData; /* нужно ли возвращать исходные данные? */ /* Данные из текущего внутреннего кортежа */ bool allTheSame; /* кортеж с признаком все-равны? */ bool hasPrefix; /* у кортежа есть префикс? */ Datum prefixDatum; /* если да, то это значение префикса */ int nNodes; /* число узлов во внутреннем кортеже */ Datum *nodeLabels; /* значения меток узлов (NULL, если их нет) */ } spgInnerConsistentIn; typedef struct spgInnerConsistentOut { int nNodes; /* число дочерних узлов, которые нужно посетить */ int *nodeNumbers; /* их номера в массиве узлов */ int *levelAdds; /* шаги увеличения уровня для этих узлов */ Datum *reconstructedValues; /* связанные восстановленные значения */ void **traversalValues; /* переходящие значения, специфичные для класса операторов */ double **distances; /* связанные расстояния */ } spgInnerConsistentOut;Массив
scankeysдлиныnkeysописывает условия поиска по индексу. Эти условия объединяются операцией И — найдены должны быть только те записи, которые удовлетворяют всем условиям. (Заметьте, что сnkeys= 0 подразумевается, что запросу удовлетворяют все записи в индексе.) Обычно эту функцию интересуют только поляsk_strategyиsk_argumentв каждой записи массива, в которых определяется соответственно индексируемый оператор и искомое значение. В частности, нет необходимости проверятьsk_flags, чтобы распознать NULL в искомом значении, так как ядро SP-GiST отфильтрует такие условия. Массивorderbysдлиныnorderbysподобным образом описывает упорядочивающие операторы (если они есть). ВreconstructedValueпередаётся значение, восстановленное для родительского кортежа; это может быть(Datum) 0на уровне корня или если функцияinner_consistentне установила значение на предыдущем уровне. ВtraversalValueпередаётся указатель на переходящие данные, полученные из предыдущего вызоваinner_consistentдля родительского кортежа индекса, либо NULL на уровне корня. ПолеtraversalMemoryContextуказывает на контекст памяти, в котором нужно сохранить выходные переходящие данные (см. ниже). Вlevelпередаётся уровень текущего внутреннего кортежа (уровень корня считается нулевым). ФлагreturnDataустанавливается, когда для этого запроса нужно получить восстановленные данные; это возможно, только если функцияconfigустановила признакcanReturnData. ПризнакallTheSameустанавливается, если текущий внутренний кортеж имеет пометку «все-равны»; в этом случае все узлы имеют одну метку (если имеют) и значит, либо все они, либо никакой не соответствует запросу (см. Подраздел 64.3.4.3). ПризнакhasPrefixустанавливается, если текущий внутренний кортеж содержит префикс; в этом случае вprefixDatumнаходится его значение. ВnNodesзадаётся число дочерних узлов, содержащихся во внутреннем кортеже, а вnodeLabels— массив их меток либо NULL, если они не имеют меток.В
nNodesнужно записать число дочерних узлов, которые потребуется посетить при поиске, а вnodeNumbers— массив их индексов. Если класс операторов отслеживает уровни, вlevelAddsнужно передать массив с шагами увеличения уровня при посещении каждого узла. (Часто шаг будет одним для всех узлов, но может быть и по-другому, поэтому применяется массив.) Если потребовалось восстановить значения, поместите вreconstructedValuesмассив значений, восстановленных для каждого дочернего узла, который нужно посетить; в противном случае оставьтеreconstructedValuesравным NULL. Предполагается, что восстановленные значения имеют типspgConfigOut.leafType. (Однако ядро системы ничего не будет делать с ними, кроме как, возможно, копировать, поэтому они должны иметь такие жеtyplenиtypbyval, что иleafType.) Если выполняется поиск с упорядочиванием, поместите вdistancesмассив расстояний в соответствии с массивомorderbys(узлы с меньшими расстояниями будут обрабатываться первыми). В противном случае оставьте в этом поле NULL. Если желательно передать дополнительные данные («переходящие значения») на нижние уровни при поиске по дереву, поместите вtraversalValuesуказатель на массив соответствующих переходящих значений, по одному для каждого дочернего узла, который нужно посетить; в противном случае оставьте вtraversalValuesзначение NULL. Заметьте, что функцияinner_consistentсама должна выделять память, используя palloc, для массивовnodeNumbers,levelAdds,distances,reconstructedValuesиtraversalValuesв текущем контексте памяти. Однако выходные переходящие значения, на которые указывает массивtraversalValues, должны размещаться в контекстеtraversalMemoryContext. При этом каждое переходящее значения должно располагаться в отдельном блоке памяти palloc.leaf_consistentВозвращает true, если кортеж листа удовлетворяет запросу.
В SQL эта функция должна объявляться так:
CREATE FUNCTION my_leaf_consistent(internal, internal) RETURNS bool ...
В первом аргументе передаётся указатель на структуру
spgLeafConsistentInязыка C, содержащую входные данные для функции. Во втором аргументе передаётся указатель на структуруspgLeafConsistentOutязыка C, в которую функция должна поместить результат.typedef struct spgLeafConsistentIn { ScanKey scankeys; /* массив операторов и искомых значений */ ScanKey orderbys; /* массив операторов упорядочивания и * сравниваемых значений */ int nkeys; /* длина массива scankeys */ int norderbys; /* длина массива orderbys */ Datum reconstructedValue; /* значение, восстановленное для родителя */ void *traversalValue; /* переходящее значение, специфичное для класса операторов */ int level; /* текущий уровень (отсчитывая от нуля) */ bool returnData; /* нужно ли возвращать исходные данные? */ Datum leafDatum; /* значение в кортеже листа */ } spgLeafConsistentIn; typedef struct spgLeafConsistentOut { Datum leafValue; /* восстановленные исходные данные, при наличии */ bool recheck; /* true, если оператор нужно перепроверить */ bool recheckDistances; /* true, если расстояния нужно перепроверить */ double *distances; /* связанные расстояния */ } spgLeafConsistentOut;Массив
scankeysдлиныnkeysописывает условия поиска по индексу. Эти условия объединяются операцией И — запросу удовлетворяют только те записи в индексе, которые удовлетворяют всем этим условиям. (Заметьте, что сnkeys= 0 подразумевается, что запросу удовлетворяют все записи в индексе.) Обычно эту функцию интересуют только поляsk_strategyиsk_argumentв каждой записи массива, в которых определяются соответственно индексируемый оператор и искомое значение. В частности, нет необходимости проверятьsk_flags, чтобы распознать NULL в искомом значении, так как ядро SP-GiST отфильтрует такие условия. Массивorderbysдлиныnorderbysподобным образом описывает упорядочивающие операторы. ВreconstructedValueпередаётся значение, восстановленное для родительского кортежа; это может быть(Datum) 0на уровне корня или если функцияinner_consistentне установила значение на предыдущем уровне. ВtraversalValueпередаётся указатель на переходящие данные, полученные из предыдущего вызоваinner_consistentдля родительского кортежа индекса, либо NULL на уровне корня. Вlevelпередаётся уровень текущего внутреннего кортежа (уровень корня считается нулевым). ФлагreturnDataустанавливается, когда для этого запроса нужно получить восстановленные данные; это возможно, только если функцияconfigустановила признакcanReturnData. ВleafDatumпередаётся значение ключа, записанное в текущем кортеже листа.Эта функция должна вернуть
true, если кортеж листа соответствует запросу, илиfalseв противном случае. В случае положительного результата, если в полеreturnDataпереданоtrue, нужно поместить вleafValueзначение (типаspgConfigIn.attType), изначально переданное для индексации в этот кортеж. Кроме того, флагуrecheckможно присвоитьtrue, если соответствие неточное, так что для установления точного результата проверки нужно повторно применить оператор(ы) к собственно кортежу данных. Если выполняется упорядочивающий поиск, поместите вdistancesмассив со значениями расстояния, соответствующими массивуorderbys. В противном случае оставьте в этом поле NULL. Если хотя бы одно из возвращаемых расстояний определено неточно, присвойте true полюrecheckDistances. В этом случае исполнитель вычислит точные расстояния после получения кортежа из кучи и переупорядочит кортежи, если потребуется.
Дополнительно пользователь может определить методы:
Datum compress(Datum in)Преобразует элемент данных в формат, подходящий для физического хранения в кортеже уровня листьев в индексе. Эта функция принимает значение типа
spgConfigIn.attTypeи возвращает значение типаspgConfigOut.leafType. Возвращаемое значение не должно содержать указатель на внешние TOAST-данные.Обратите внимание, что метод
compressприменяется только к сохраняемым значениям. Методы, оценивающие согласованность, получают сканируемые в запросе ключи (scankeys) неизменёнными, без обработки функциейcompress.optionsОпределяет набор видимых пользователю параметров, управляющих поведением класса операторов.
В SQL эта функция должна объявляться так:
CREATE OR REPLACE FUNCTION my_options(internal) RETURNS void AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
Этой функции передаётся указатель на структуру
local_relopts, в которую нужно внести набор параметров, относящихся к классу операторов. Обращаться к этим параметрам из других опорных функций можно с помощью макросовPG_HAS_OPCLASS_OPTIONS()иPG_GET_OPCLASS_OPTIONS().Так как в SP-GiST представление ключа допускает гибкость, могут быть полезны параметры для настройки этого индекса.
Все опорные методы SP-GiST обычно вызываются в кратковременных контекстах памяти; то есть CurrentMemoryContext сбрасывается после обработки каждого кортежа. Таким образом, можно не заботиться об освобождении любых блоков памяти, выделенных функцией palloc. (Метод config является исключением: в нём нужно не допускать утечек памяти. Но обычно метод config не делает ничего, кроме как присваивает константы переданной структуре параметров.)
Если индексируемый столбец имеет сортируемый тип данных, правило сортировки индекса будет передаваться всем опорным методам, используя стандартный механизм PG_GET_COLLATION().
64.3.4. Реализация #
В этом разделе освещаются тонкости реализации и особенности, о которых полезно знать тем, кто будет реализовывать классы операторов SP-GiST.
64.3.4.1. Ограничения SP-GiST #
Отдельные кортежи листьев и внутренние кортежи должны умещаться в одной странице индекса (по умолчанию её размер 8 Кбайт). Таким образом при индексировании значений типов данных переменной длины большие значения могут поддерживаться только такими схемами, как префиксные деревья, в которых каждый уровень дерева включает префикс, достаточно короткий для помещения в страницу, и на конечном уровне листьев содержится суффикс, который также достаточно мал, чтобы поместиться в странице. Класс операторов должен устанавливать признак longValuesOK, только если он готов организовывать такую структуру. Если этот признак не установлен, ядро SP-GiST не примет запрос на индексацию значения, которое слишком велико для одной страницы индекса.
Также класс операторов должен отвечать за то, чтобы внутренние кортежи при расширении не выходили за пределы страницы индекса; это ограничивает число дочерних узлов, которые могут принадлежать одному внутреннему кортежу, а также максимальный размер значения префикса.
Ещё одно ограничение состоит в том, что когда узел внутреннего кортежа указывает на набор кортежей листьев, все эти кортежи должны находиться в одной странице индекса. (Это конструктивное ограничение введено для оптимизации позиционирования и экономии места на ссылках, связывающих такие кортежи вместе.) Если набор кортежей листьев оказывается слишком большим для одной страницы, выполняется разделение и вставляется промежуточный внутренний кортеж. Чтобы устранить возникшую проблему, новый внутренний кортеж должен разделять набор значений в листе на несколько групп узлов. Если функция picksplit класса операторов не может сделать это, ядро SP-GiST переходит к чрезвычайным мерам, описанным в Подразделе 64.3.4.3.
Когда longValuesOK имеет значение true, ожидается, что последующие уровни дерева SP-GiST будут включать всё больше и больше информации в префиксы и метки узлов внутренних кортежей, постепенно уменьшая значение, которое нужно сохранить на уровне листьев, чтобы оно в конце концов уместилось на странице. Чтобы в случае ошибки в классах операторах исключить бесконечные циклы при добавлении записи, ядро SP-GiST выдаст ошибку, если значение для листа не уменьшится за 10 вызовов метода choose.
64.3.4.2. SP-GiST без меток узлов #
В некоторых древовидных схемах каждый внутренний кортеж содержит фиксированный набор узлов; например, в дереве квадрантов это всегда четыре узла, соответствующие четырём квадрантам вокруг центральной точки внутреннего кортежа. В таком случае код обычно работает с узлами по номерам и необходимости в явных метках узлов нет. Чтобы убрать метки узлов (и таким образом сэкономить место), функция picksplit может возвратить NULL вместо массива nodeLabels, а функция choose аналогично может возвратить NULL вместо массива prefixNodeLabels во время действия spgSplitTuple. В результате при последующих вызовах функций choose и inner_consistent им вместо nodeLabels будет передаваться NULL. В принципе метки узлов могут применяться для одних внутренних кортежей и отсутствовать у других в том же индексе.
Когда внутренний кортеж содержит узлы без меток, функция choose не может выбрать действие spgAddNode, так как в этом случае предполагается, что набор узлов фиксированный.
64.3.4.3. Внутренние кортежи «все-равны» #
Ядро SP-GiST может переопределить результаты функции picksplit класса операторов, когда эта функция не может разделить поступившие значения листьев на минимум две категории узлов. Когда это происходит, создаётся новый внутренний кортеж с несколькими узлами, каждый из которых имеет одну метку (если имеет), которую picksplit дала одному узлу, а значения листьев распределяются случайно между этими равнозначными узлами. Для этого внутреннего кортежа устанавливается флаг allTheSame, который предупреждает функции choose и inner_consistent, что кортеж не содержит набор узлов, который они обычно ожидают.
Когда обрабатывается кортеж с флагом allTheSame, выбранное функцией choose действие spgMatchNode воспринимается как указание, что новое значение можно присвоить одному из равнозначных узлов; код ядра будет игнорировать полученное значение nodeN и спустится в один из узлов, выбранный случайно (чтобы дерево было сбалансированным). Будет считаться ошибкой, если choose выберет действие spgAddNode, так как при этом не все узлы окажутся равны; если добавляемое значение не соответствует существующим узлам, должно выбираться действие spgSplitTuple.
Также, когда обрабатывается кортеж с флагом allTheSame, функция inner_consistent должна вернуть все или не возвращать никакие узлы для продолжения поиска по индексу, так как все узлы равнозначны. Для этого может потребоваться, а может и не потребоваться код обработки особого случая, в зависимости от того, как inner_consistent обычно воспринимает узлы.
64.3.5. Примеры #
Дистрибутив исходного кода PostgreSQL содержит несколько примеров классов операторов индекса SP-GiST, перечисленных в Таблице 64.2. Код их реализации вы можете найти в src/backend/access/spgist/ и src/backend/utils/adt/.
64.3. SP-GiST Indexes #
64.3.1. Introduction #
SP-GiST is an abbreviation for space-partitioned GiST. SP-GiST supports partitioned search trees, which facilitate development of a wide range of different non-balanced data structures, such as quad-trees, k-d trees, and radix trees (tries). The common feature of these structures is that they repeatedly divide the search space into partitions that need not be of equal size. Searches that are well matched to the partitioning rule can be very fast.
These popular data structures were originally developed for in-memory usage. In main memory, they are usually designed as a set of dynamically allocated nodes linked by pointers. This is not suitable for direct storing on disk, since these chains of pointers can be rather long which would require too many disk accesses. In contrast, disk-based data structures should have a high fanout to minimize I/O. The challenge addressed by SP-GiST is to map search tree nodes to disk pages in such a way that a search need access only a few disk pages, even if it traverses many nodes.
Like GiST, SP-GiST is meant to allow the development of custom data types with the appropriate access methods, by an expert in the domain of the data type, rather than a database expert.
Some of the information here is derived from Purdue University's SP-GiST Indexing Project web site. The SP-GiST implementation in PostgreSQL is primarily maintained by Teodor Sigaev and Oleg Bartunov, and there is more information on their web site.
64.3.2. Built-in Operator Classes #
The core PostgreSQL distribution includes the SP-GiST operator classes shown in Table 64.2.
Table 64.2. Built-in SP-GiST Operator Classes
| Name | Indexable Operators | Ordering Operators |
|---|---|---|
box_ops | << (box,box) | <-> (box,point) |
&< (box,box) | ||
&> (box,box) | ||
>> (box,box) | ||
<@ (box,box) | ||
@> (box,box) | ||
~= (box,box) | ||
&& (box,box) | ||
<<| (box,box) | ||
&<| (box,box) | ||
|&> (box,box) | ||
|>> (box,box) | ||
inet_ops | << (inet,inet) | |
<<= (inet,inet) | ||
>> (inet,inet) | ||
>>= (inet,inet) | ||
= (inet,inet) | ||
<> (inet,inet) | ||
< (inet,inet) | ||
<= (inet,inet) | ||
> (inet,inet) | ||
>= (inet,inet) | ||
&& (inet,inet) | ||
kd_point_ops | |>> (point,point) | <-> (point,point) |
<< (point,point) | ||
>> (point,point) | ||
<<| (point,point) | ||
~= (point,point) | ||
<@ (point,box) | ||
poly_ops | << (polygon,polygon) | <-> (polygon,point) |
&< (polygon,polygon) | ||
&> (polygon,polygon) | ||
>> (polygon,polygon) | ||
<@ (polygon,polygon) | ||
@> (polygon,polygon) | ||
~= (polygon,polygon) | ||
&& (polygon,polygon) | ||
<<| (polygon,polygon) | ||
&<| (polygon,polygon) | ||
|>> (polygon,polygon) | ||
|&> (polygon,polygon) | ||
quad_point_ops | |>> (point,point) | <-> (point,point) |
<< (point,point) | ||
>> (point,point) | ||
<<| (point,point) | ||
~= (point,point) | ||
<@ (point,box) | ||
range_ops | = (anyrange,anyrange) | |
&& (anyrange,anyrange) | ||
@> (anyrange,anyelement) | ||
@> (anyrange,anyrange) | ||
<@ (anyrange,anyrange) | ||
<< (anyrange,anyrange) | ||
>> (anyrange,anyrange) | ||
&< (anyrange,anyrange) | ||
&> (anyrange,anyrange) | ||
-|- (anyrange,anyrange) | ||
text_ops | = (text,text) | |
< (text,text) | ||
<= (text,text) | ||
> (text,text) | ||
>= (text,text) | ||
~<~ (text,text) | ||
~<=~ (text,text) | ||
~>=~ (text,text) | ||
~>~ (text,text) | ||
^@ (text,text) |
Of the two operator classes for type point, quad_point_ops is the default. kd_point_ops supports the same operators but uses a different index data structure that may offer better performance in some applications.
The quad_point_ops, kd_point_ops and poly_ops operator classes support the <-> ordering operator, which enables the k-nearest neighbor (k-NN) search over indexed point or polygon data sets.
64.3.3. Extensibility #
SP-GiST offers an interface with a high level of abstraction, requiring the access method developer to implement only methods specific to a given data type. The SP-GiST core is responsible for efficient disk mapping and searching the tree structure. It also takes care of concurrency and logging considerations.
Leaf tuples of an SP-GiST tree usually contain values of the same data type as the indexed column, although it is also possible for them to contain lossy representations of the indexed column. Leaf tuples stored at the root level will directly represent the original indexed data value, but leaf tuples at lower levels might contain only a partial value, such as a suffix. In that case the operator class support functions must be able to reconstruct the original value using information accumulated from the inner tuples that are passed through to reach the leaf level.
When an SP-GiST index is created with INCLUDE columns, the values of those columns are also stored in leaf tuples. The INCLUDE columns are of no concern to the SP-GiST operator class, so they are not discussed further here.
Inner tuples are more complex, since they are branching points in the search tree. Each inner tuple contains a set of one or more nodes, which represent groups of similar leaf values. A node contains a downlink that leads either to another, lower-level inner tuple, or to a short list of leaf tuples that all lie on the same index page. Each node normally has a label that describes it; for example, in a radix tree the node label could be the next character of the string value. (Alternatively, an operator class can omit the node labels, if it works with a fixed set of nodes for all inner tuples; see Section 64.3.4.2.) Optionally, an inner tuple can have a prefix value that describes all its members. In a radix tree this could be the common prefix of the represented strings. The prefix value is not necessarily really a prefix, but can be any data needed by the operator class; for example, in a quad-tree it can store the central point that the four quadrants are measured with respect to. A quad-tree inner tuple would then also contain four nodes corresponding to the quadrants around this central point.
Some tree algorithms require knowledge of level (or depth) of the current tuple, so the SP-GiST core provides the possibility for operator classes to manage level counting while descending the tree. There is also support for incrementally reconstructing the represented value when that is needed, and for passing down additional data (called traverse values) during a tree descent.
Note
The SP-GiST core code takes care of null entries. Although SP-GiST indexes do store entries for nulls in indexed columns, this is hidden from the index operator class code: no null index entries or search conditions will ever be passed to the operator class methods. (It is assumed that SP-GiST operators are strict and so cannot succeed for null values.) Null values are therefore not discussed further here.
There are five user-defined methods that an index operator class for SP-GiST must provide, and two are optional. All five mandatory methods follow the convention of accepting two internal arguments, the first of which is a pointer to a C struct containing input values for the support method, while the second argument is a pointer to a C struct where output values must be placed. Four of the mandatory methods just return void, since all their results appear in the output struct; but leaf_consistent returns a boolean result. The methods must not modify any fields of their input structs. In all cases, the output struct is initialized to zeroes before calling the user-defined method. The optional sixth method compress accepts a datum to be indexed as the only argument and returns a value suitable for physical storage in a leaf tuple. The optional seventh method options accepts an internal pointer to a C struct, where opclass-specific parameters should be placed, and returns void.
The five mandatory user-defined methods are:
configReturns static information about the index implementation, including the data type OIDs of the prefix and node label data types.
The SQL declaration of the function must look like this:
CREATE FUNCTION my_config(internal, internal) RETURNS void ...
The first argument is a pointer to a
spgConfigInC struct, containing input data for the function. The second argument is a pointer to aspgConfigOutC struct, which the function must fill with result data.typedef struct spgConfigIn { Oid attType; /* Data type to be indexed */ } spgConfigIn; typedef struct spgConfigOut { Oid prefixType; /* Data type of inner-tuple prefixes */ Oid labelType; /* Data type of inner-tuple node labels */ Oid leafType; /* Data type of leaf-tuple values */ bool canReturnData; /* Opclass can reconstruct original data */ bool longValuesOK; /* Opclass can cope with values > 1 page */ } spgConfigOut;attTypeis passed in order to support polymorphic index operator classes; for ordinary fixed-data-type operator classes, it will always have the same value and so can be ignored.For operator classes that do not use prefixes,
prefixTypecan be set toVOIDOID. Likewise, for operator classes that do not use node labels,labelTypecan be set toVOIDOID.canReturnDatashould be set true if the operator class is capable of reconstructing the originally-supplied index value.longValuesOKshould be set true only when theattTypeis of variable length and the operator class is capable of segmenting long values by repeated suffixing (see Section 64.3.4.1).leafTypeshould match the index storage type defined by the operator class'sopckeytypecatalog entry. (Note thatopckeytypecan be zero, implying the storage type is the same as the operator class's input type, which is the most common situation.) For reasons of backward compatibility, theconfigmethod can setleafTypeto some other value, and that value will be used; but this is deprecated since the index contents are then incorrectly identified in the catalogs. Also, it's permissible to leaveleafTypeuninitialized (zero); that is interpreted as meaning the index storage type derived fromopckeytype.When
attTypeandleafTypeare different, the optional methodcompressmust be provided. Methodcompressis responsible for transformation of datums to be indexed fromattTypetoleafType.chooseChooses a method for inserting a new value into an inner tuple.
The SQL declaration of the function must look like this:
CREATE FUNCTION my_choose(internal, internal) RETURNS void ...
The first argument is a pointer to a
spgChooseInC struct, containing input data for the function. The second argument is a pointer to aspgChooseOutC struct, which the function must fill with result data.typedef struct spgChooseIn { Datum datum; /* original datum to be indexed */ Datum leafDatum; /* current datum to be stored at leaf */ int level; /* current level (counting from zero) */ /* Data from current inner tuple */ bool allTheSame; /* tuple is marked all-the-same? */ bool hasPrefix; /* tuple has a prefix? */ Datum prefixDatum; /* if so, the prefix value */ int nNodes; /* number of nodes in the inner tuple */ Datum *nodeLabels; /* node label values (NULL if none) */ } spgChooseIn; typedef enum spgChooseResultType { spgMatchNode = 1, /* descend into existing node */ spgAddNode, /* add a node to the inner tuple */ spgSplitTuple /* split inner tuple (change its prefix) */ } spgChooseResultType; typedef struct spgChooseOut { spgChooseResultType resultType; /* action code, see above */ union { struct /* results for spgMatchNode */ { int nodeN; /* descend to this node (index from 0) */ int levelAdd; /* increment level by this much */ Datum restDatum; /* new leaf datum */ } matchNode; struct /* results for spgAddNode */ { Datum nodeLabel; /* new node's label */ int nodeN; /* where to insert it (index from 0) */ } addNode; struct /* results for spgSplitTuple */ { /* Info to form new upper-level inner tuple with one child tuple */ bool prefixHasPrefix; /* tuple should have a prefix? */ Datum prefixPrefixDatum; /* if so, its value */ int prefixNNodes; /* number of nodes */ Datum *prefixNodeLabels; /* their labels (or NULL for * no labels) */ int childNodeN; /* which node gets child tuple */ /* Info to form new lower-level inner tuple with all old nodes */ bool postfixHasPrefix; /* tuple should have a prefix? */ Datum postfixPrefixDatum; /* if so, its value */ } splitTuple; } result; } spgChooseOut;datumis the original datum ofspgConfigIn.attTypetype that was to be inserted into the index.leafDatumis a value ofspgConfigOut.leafTypetype, which is initially a result of methodcompressapplied todatumwhen methodcompressis provided, or the same value asdatumotherwise.leafDatumcan change at lower levels of the tree if thechooseorpicksplitmethods change it. When the insertion search reaches a leaf page, the current value ofleafDatumis what will be stored in the newly created leaf tuple.levelis the current inner tuple's level, starting at zero for the root level.allTheSameis true if the current inner tuple is marked as containing multiple equivalent nodes (see Section 64.3.4.3).hasPrefixis true if the current inner tuple contains a prefix; if so,prefixDatumis its value.nNodesis the number of child nodes contained in the inner tuple, andnodeLabelsis an array of their label values, or NULL if there are no labels.The
choosefunction can determine either that the new value matches one of the existing child nodes, or that a new child node must be added, or that the new value is inconsistent with the tuple prefix and so the inner tuple must be split to create a less restrictive prefix.If the new value matches one of the existing child nodes, set
resultTypetospgMatchNode. SetnodeNto the index (from zero) of that node in the node array. SetlevelAddto the increment inlevelcaused by descending through that node, or leave it as zero if the operator class does not use levels. SetrestDatumto equalleafDatumif the operator class does not modify datums from one level to the next, or otherwise set it to the modified value to be used asleafDatumat the next level.If a new child node must be added, set
resultTypetospgAddNode. SetnodeLabelto the label to be used for the new node, and setnodeNto the index (from zero) at which to insert the node in the node array. After the node has been added, thechoosefunction will be called again with the modified inner tuple; that call should result in anspgMatchNoderesult.If the new value is inconsistent with the tuple prefix, set
resultTypetospgSplitTuple. This action moves all the existing nodes into a new lower-level inner tuple, and replaces the existing inner tuple with a tuple having a single downlink pointing to the new lower-level inner tuple. SetprefixHasPrefixto indicate whether the new upper tuple should have a prefix, and if so setprefixPrefixDatumto the prefix value. This new prefix value must be sufficiently less restrictive than the original to accept the new value to be indexed. SetprefixNNodesto the number of nodes needed in the new tuple, and setprefixNodeLabelsto a palloc'd array holding their labels, or to NULL if node labels are not required. Note that the total size of the new upper tuple must be no more than the total size of the tuple it is replacing; this constrains the lengths of the new prefix and new labels. SetchildNodeNto the index (from zero) of the node that will downlink to the new lower-level inner tuple. SetpostfixHasPrefixto indicate whether the new lower-level inner tuple should have a prefix, and if so setpostfixPrefixDatumto the prefix value. The combination of these two prefixes and the downlink node's label (if any) must have the same meaning as the original prefix, because there is no opportunity to alter the node labels that are moved to the new lower-level tuple, nor to change any child index entries. After the node has been split, thechoosefunction will be called again with the replacement inner tuple. That call may return anspgAddNoderesult, if no suitable node was created by thespgSplitTupleaction. Eventuallychoosemust returnspgMatchNodeto allow the insertion to descend to the next level.picksplitDecides how to create a new inner tuple over a set of leaf tuples.
The SQL declaration of the function must look like this:
CREATE FUNCTION my_picksplit(internal, internal) RETURNS void ...
The first argument is a pointer to a
spgPickSplitInC struct, containing input data for the function. The second argument is a pointer to aspgPickSplitOutC struct, which the function must fill with result data.typedef struct spgPickSplitIn { int nTuples; /* number of leaf tuples */ Datum *datums; /* their datums (array of length nTuples) */ int level; /* current level (counting from zero) */ } spgPickSplitIn; typedef struct spgPickSplitOut { bool hasPrefix; /* new inner tuple should have a prefix? */ Datum prefixDatum; /* if so, its value */ int nNodes; /* number of nodes for new inner tuple */ Datum *nodeLabels; /* their labels (or NULL for no labels) */ int *mapTuplesToNodes; /* node index for each leaf tuple */ Datum *leafTupleDatums; /* datum to store in each new leaf tuple */ } spgPickSplitOut;nTuplesis the number of leaf tuples provided.datumsis an array of their datum values ofspgConfigOut.leafTypetype.levelis the current level that all the leaf tuples share, which will become the level of the new inner tuple.Set
hasPrefixto indicate whether the new inner tuple should have a prefix, and if so setprefixDatumto the prefix value. SetnNodesto indicate the number of nodes that the new inner tuple will contain, and setnodeLabelsto an array of their label values, or to NULL if node labels are not required. SetmapTuplesToNodesto an array that gives the index (from zero) of the node that each leaf tuple should be assigned to. SetleafTupleDatumsto an array of the values to be stored in the new leaf tuples (these will be the same as the inputdatumsif the operator class does not modify datums from one level to the next). Note that thepicksplitfunction is responsible for palloc'ing thenodeLabels,mapTuplesToNodesandleafTupleDatumsarrays.If more than one leaf tuple is supplied, it is expected that the
picksplitfunction will classify them into more than one node; otherwise it is not possible to split the leaf tuples across multiple pages, which is the ultimate purpose of this operation. Therefore, if thepicksplitfunction ends up placing all the leaf tuples in the same node, the core SP-GiST code will override that decision and generate an inner tuple in which the leaf tuples are assigned at random to several identically-labeled nodes. Such a tuple is markedallTheSameto signify that this has happened. Thechooseandinner_consistentfunctions must take suitable care with such inner tuples. See Section 64.3.4.3 for more information.picksplitcan be applied to a single leaf tuple only in the case that theconfigfunction setlongValuesOKto true and a larger-than-a-page input value has been supplied. In this case the point of the operation is to strip off a prefix and produce a new, shorter leaf datum value. The call will be repeated until a leaf datum short enough to fit on a page has been produced. See Section 64.3.4.1 for more information.inner_consistentReturns set of nodes (branches) to follow during tree search.
The SQL declaration of the function must look like this:
CREATE FUNCTION my_inner_consistent(internal, internal) RETURNS void ...
The first argument is a pointer to a
spgInnerConsistentInC struct, containing input data for the function. The second argument is a pointer to aspgInnerConsistentOutC struct, which the function must fill with result data.typedef struct spgInnerConsistentIn { ScanKey scankeys; /* array of operators and comparison values */ ScanKey orderbys; /* array of ordering operators and comparison * values */ int nkeys; /* length of scankeys array */ int norderbys; /* length of orderbys array */ Datum reconstructedValue; /* value reconstructed at parent */ void *traversalValue; /* opclass-specific traverse value */ MemoryContext traversalMemoryContext; /* put new traverse values here */ int level; /* current level (counting from zero) */ bool returnData; /* original data must be returned? */ /* Data from current inner tuple */ bool allTheSame; /* tuple is marked all-the-same? */ bool hasPrefix; /* tuple has a prefix? */ Datum prefixDatum; /* if so, the prefix value */ int nNodes; /* number of nodes in the inner tuple */ Datum *nodeLabels; /* node label values (NULL if none) */ } spgInnerConsistentIn; typedef struct spgInnerConsistentOut { int nNodes; /* number of child nodes to be visited */ int *nodeNumbers; /* their indexes in the node array */ int *levelAdds; /* increment level by this much for each */ Datum *reconstructedValues; /* associated reconstructed values */ void **traversalValues; /* opclass-specific traverse values */ double **distances; /* associated distances */ } spgInnerConsistentOut;The array
scankeys, of lengthnkeys, describes the index search condition(s). These conditions are combined with AND — only index entries that satisfy all of them are interesting. (Note thatnkeys= 0 implies that all index entries satisfy the query.) Usually the consistent function only cares about thesk_strategyandsk_argumentfields of each array entry, which respectively give the indexable operator and comparison value. In particular it is not necessary to checksk_flagsto see if the comparison value is NULL, because the SP-GiST core code will filter out such conditions. The arrayorderbys, of lengthnorderbys, describes ordering operators (if any) in the same manner.reconstructedValueis the value reconstructed for the parent tuple; it is(Datum) 0at the root level or if theinner_consistentfunction did not provide a value at the parent level.traversalValueis a pointer to any traverse data passed down from the previous call ofinner_consistenton the parent index tuple, or NULL at the root level.traversalMemoryContextis the memory context in which to store output traverse values (see below).levelis the current inner tuple's level, starting at zero for the root level.returnDataistrueif reconstructed data is required for this query; this will only be so if theconfigfunction assertedcanReturnData.allTheSameis true if the current inner tuple is marked “all-the-same”; in this case all the nodes have the same label (if any) and so either all or none of them match the query (see Section 64.3.4.3).hasPrefixis true if the current inner tuple contains a prefix; if so,prefixDatumis its value.nNodesis the number of child nodes contained in the inner tuple, andnodeLabelsis an array of their label values, or NULL if the nodes do not have labels.nNodesmust be set to the number of child nodes that need to be visited by the search, andnodeNumbersmust be set to an array of their indexes. If the operator class keeps track of levels, setlevelAddsto an array of the level increments required when descending to each node to be visited. (Often these increments will be the same for all the nodes, but that's not necessarily so, so an array is used.) If value reconstruction is needed, setreconstructedValuesto an array of the values reconstructed for each child node to be visited; otherwise, leavereconstructedValuesas NULL. The reconstructed values are assumed to be of typespgConfigOut.leafType. (However, since the core system will do nothing with them except possibly copy them, it is sufficient for them to have the sametyplenandtypbyvalproperties asleafType.) If ordered search is performed, setdistancesto an array of distance values according toorderbysarray (nodes with lowest distances will be processed first). Leave it NULL otherwise. If it is desired to pass down additional out-of-band information (“traverse values”) to lower levels of the tree search, settraversalValuesto an array of the appropriate traverse values, one for each child node to be visited; otherwise, leavetraversalValuesas NULL. Note that theinner_consistentfunction is responsible for palloc'ing thenodeNumbers,levelAdds,distances,reconstructedValues, andtraversalValuesarrays in the current memory context. However, any output traverse values pointed to by thetraversalValuesarray should be allocated intraversalMemoryContext. Each traverse value must be a single palloc'd chunk.leaf_consistentReturns true if a leaf tuple satisfies a query.
The SQL declaration of the function must look like this:
CREATE FUNCTION my_leaf_consistent(internal, internal) RETURNS bool ...
The first argument is a pointer to a
spgLeafConsistentInC struct, containing input data for the function. The second argument is a pointer to aspgLeafConsistentOutC struct, which the function must fill with result data.typedef struct spgLeafConsistentIn { ScanKey scankeys; /* array of operators and comparison values */ ScanKey orderbys; /* array of ordering operators and comparison * values */ int nkeys; /* length of scankeys array */ int norderbys; /* length of orderbys array */ Datum reconstructedValue; /* value reconstructed at parent */ void *traversalValue; /* opclass-specific traverse value */ int level; /* current level (counting from zero) */ bool returnData; /* original data must be returned? */ Datum leafDatum; /* datum in leaf tuple */ } spgLeafConsistentIn; typedef struct spgLeafConsistentOut { Datum leafValue; /* reconstructed original data, if any */ bool recheck; /* set true if operator must be rechecked */ bool recheckDistances; /* set true if distances must be rechecked */ double *distances; /* associated distances */ } spgLeafConsistentOut;The array
scankeys, of lengthnkeys, describes the index search condition(s). These conditions are combined with AND — only index entries that satisfy all of them satisfy the query. (Note thatnkeys= 0 implies that all index entries satisfy the query.) Usually the consistent function only cares about thesk_strategyandsk_argumentfields of each array entry, which respectively give the indexable operator and comparison value. In particular it is not necessary to checksk_flagsto see if the comparison value is NULL, because the SP-GiST core code will filter out such conditions. The arrayorderbys, of lengthnorderbys, describes the ordering operators in the same manner.reconstructedValueis the value reconstructed for the parent tuple; it is(Datum) 0at the root level or if theinner_consistentfunction did not provide a value at the parent level.traversalValueis a pointer to any traverse data passed down from the previous call ofinner_consistenton the parent index tuple, or NULL at the root level.levelis the current leaf tuple's level, starting at zero for the root level.returnDataistrueif reconstructed data is required for this query; this will only be so if theconfigfunction assertedcanReturnData.leafDatumis the key value ofspgConfigOut.leafTypestored in the current leaf tuple.The function must return
trueif the leaf tuple matches the query, orfalseif not. In thetruecase, ifreturnDataistruethenleafValuemust be set to the value (of typespgConfigIn.attType) originally supplied to be indexed for this leaf tuple. Also,recheckmay be set totrueif the match is uncertain and so the operator(s) must be re-applied to the actual heap tuple to verify the match. If ordered search is performed, setdistancesto an array of distance values according toorderbysarray. Leave it NULL otherwise. If at least one of returned distances is not exact, setrecheckDistancesto true. In this case, the executor will calculate the exact distances after fetching the tuple from the heap, and will reorder the tuples if needed.
The optional user-defined methods are:
Datum compress(Datum in)Converts a data item into a format suitable for physical storage in a leaf tuple of the index. It accepts a value of type
spgConfigIn.attTypeand returns a value of typespgConfigOut.leafType. The output value must not contain an out-of-line TOAST pointer.Note: the
compressmethod is only applied to values to be stored. The consistent methods receive queryscankeysunchanged, without transformation usingcompress.optionsDefines a set of user-visible parameters that control operator class behavior.
The SQL declaration of the function must look like this:
CREATE OR REPLACE FUNCTION my_options(internal) RETURNS void AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
The function is passed a pointer to a
local_reloptsstruct, which needs to be filled with a set of operator class specific options. The options can be accessed from other support functions using thePG_HAS_OPCLASS_OPTIONS()andPG_GET_OPCLASS_OPTIONS()macros.Since the representation of the key in SP-GiST is flexible, it may depend on user-specified parameters.
All the SP-GiST support methods are normally called in a short-lived memory context; that is, CurrentMemoryContext will be reset after processing of each tuple. It is therefore not very important to worry about pfree'ing everything you palloc. (The config method is an exception: it should try to avoid leaking memory. But usually the config method need do nothing but assign constants into the passed parameter struct.)
If the indexed column is of a collatable data type, the index collation will be passed to all the support methods, using the standard PG_GET_COLLATION() mechanism.
64.3.4. Implementation #
This section covers implementation details and other tricks that are useful for implementers of SP-GiST operator classes to know.
64.3.4.1. SP-GiST Limits #
Individual leaf tuples and inner tuples must fit on a single index page (8kB by default). Therefore, when indexing values of variable-length data types, long values can only be supported by methods such as radix trees, in which each level of the tree includes a prefix that is short enough to fit on a page, and the final leaf level includes a suffix also short enough to fit on a page. The operator class should set longValuesOK to true only if it is prepared to arrange for this to happen. Otherwise, the SP-GiST core will reject any request to index a value that is too large to fit on an index page.
Likewise, it is the operator class's responsibility that inner tuples do not grow too large to fit on an index page; this limits the number of child nodes that can be used in one inner tuple, as well as the maximum size of a prefix value.
Another limitation is that when an inner tuple's node points to a set of leaf tuples, those tuples must all be in the same index page. (This is a design decision to reduce seeking and save space in the links that chain such tuples together.) If the set of leaf tuples grows too large for a page, a split is performed and an intermediate inner tuple is inserted. For this to fix the problem, the new inner tuple must divide the set of leaf values into more than one node group. If the operator class's picksplit function fails to do that, the SP-GiST core resorts to extraordinary measures described in Section 64.3.4.3.
When longValuesOK is true, it is expected that successive levels of the SP-GiST tree will absorb more and more information into the prefixes and node labels of the inner tuples, making the required leaf datum smaller and smaller, so that eventually it will fit on a page. To prevent bugs in operator classes from causing infinite insertion loops, the SP-GiST core will raise an error if the leaf datum does not become any smaller within ten cycles of choose method calls.
64.3.4.2. SP-GiST Without Node Labels #
Some tree algorithms use a fixed set of nodes for each inner tuple; for example, in a quad-tree there are always exactly four nodes corresponding to the four quadrants around the inner tuple's centroid point. In such a case the code typically works with the nodes by number, and there is no need for explicit node labels. To suppress node labels (and thereby save some space), the picksplit function can return NULL for the nodeLabels array, and likewise the choose function can return NULL for the prefixNodeLabels array during a spgSplitTuple action. This will in turn result in nodeLabels being NULL during subsequent calls to choose and inner_consistent. In principle, node labels could be used for some inner tuples and omitted for others in the same index.
When working with an inner tuple having unlabeled nodes, it is an error for choose to return spgAddNode, since the set of nodes is supposed to be fixed in such cases.
64.3.4.3. “All-the-Same” Inner Tuples #
The SP-GiST core can override the results of the operator class's picksplit function when picksplit fails to divide the supplied leaf values into at least two node categories. When this happens, the new inner tuple is created with multiple nodes that each have the same label (if any) that picksplit gave to the one node it did use, and the leaf values are divided at random among these equivalent nodes. The allTheSame flag is set on the inner tuple to warn the choose and inner_consistent functions that the tuple does not have the node set that they might otherwise expect.
When dealing with an allTheSame tuple, a choose result of spgMatchNode is interpreted to mean that the new value can be assigned to any of the equivalent nodes; the core code will ignore the supplied nodeN value and descend into one of the nodes at random (so as to keep the tree balanced). It is an error for choose to return spgAddNode, since that would make the nodes not all equivalent; the spgSplitTuple action must be used if the value to be inserted doesn't match the existing nodes.
When dealing with an allTheSame tuple, the inner_consistent function should return either all or none of the nodes as targets for continuing the index search, since they are all equivalent. This may or may not require any special-case code, depending on how much the inner_consistent function normally assumes about the meaning of the nodes.
64.3.5. Examples #
The PostgreSQL source distribution includes several examples of index operator classes for SP-GiST, as described in Table 64.2. Look into src/backend/access/spgist/ and src/backend/utils/adt/ to see the code.