SP-GiST es una abreviatura para GiST particionado en el espacio (space-partitioned GiST). SP-GiST soporta árboles de búsqueda particionados, lo que facilita el desarrollo de una amplia variedad de estructuras de datos no balanceadas, como árboles cuadripolares (quad-trees), árboles k-d y árboles de prefijos (radix trees o tries). La característica común de estas estructuras es que dividen repetidamente el espacio de búsqueda en particiones que no necesitan ser de igual tamaño. Las búsquedas que se adaptan bien a la regla de particionado pueden ser muy rápidas.
Estas estructuras de datos populares se desarrollaron originalmente para su uso en memoria física. En la memoria principal, generalmente se diseñan como un conjunto de nodos asignados dinámicamente y enlazados mediante punteros. Esto no es adecuado para el almacenamiento directo en disco, ya que estas cadenas de punteros pueden ser bastante largas, lo que requeriría demasiados accesos a disco. Por el contrario, las estructuras de datos basadas en disco deben tener una alta ramificación (fanout) para minimizar la E/S. El desafío que aborda SP-GiST es mapear los nodos del árbol de búsqueda a páginas de disco de tal manera que una búsqueda solo necesite acceder a unas pocas páginas de disco, incluso si recorre muchos nodos.
Al igual que GiST, SP-GiST está diseñado para permitir el desarrollo de tipos de datos personalizados con sus métodos de acceso apropiados por parte de un experto en el dominio del tipo de datos, en lugar de un experto en bases de datos.
Parte de la información presentada aquí proviene del sitio web del Proyecto de Indexación SP-GiST de la Universidad de Purdue en sitio web. La implementación de SP-GiST en PostgreSQL es mantenida principalmente por Teodor Sigaev y Oleg Bartunov, y hay más información disponible en su sitio web.
La distribución principal de PostgreSQL incluye las clases de operadores de SP-GiST que se muestran en la Table 65.2.
Table 65.2. Clases de operadores SP-GiST integradas
| Nombre | Operadores indexables | Operadores de ordenamiento |
|---|---|---|
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) |
De las dos clases de operadores para el tipo point,
quad_point_ops es la predeterminada. kd_point_ops
soporta los mismos operadores pero utiliza una estructura de datos de índice diferente que
puede ofrecer un mejor rendimiento en algunas aplicaciones.
Las clases de operadores quad_point_ops, kd_point_ops y
poly_ops soportan el operador de ordenamiento <->,
lo que permite la búsqueda de los k vecinos más cercanos (k-NN)
sobre conjuntos de datos indexados de puntos o polígonos.
SP-GiST ofrece una interfaz con un alto nivel de abstracción, requiriendo que el desarrollador del método de acceso implemente únicamente métodos específicos para un tipo de datos dado. El núcleo de SP-GiST es responsable del mapeo eficiente en disco y de la búsqueda en la estructura del árbol. También se encarga de las consideraciones de concurrencia y registro (logging).
Las tuplas hoja de un árbol SP-GiST usualmente contienen valores del mismo tipo de datos que la columna indexada, aunque también es posible que contengan representaciones con pérdida (lossy) de la columna indexada. Las tuplas hoja almacenadas en el nivel raíz representarán directamente el valor de datos indexado original, pero las tuplas hoja en niveles inferiores podrían contener solo un valor parcial, como un sufijo. En ese caso, las funciones de soporte de la clase de operadores deben ser capaces de reconstruir el valor original utilizando la información acumulada de las tuplas internas por las que se pasa para llegar al nivel hoja.
Cuando se crea un índice SP-GiST con columnas
INCLUDE, los valores de esas columnas también se
almacenan en las tuplas hoja. Las columnas INCLUDE no son
de interés para la clase de operadores de SP-GiST, por lo que no
se discuten más aquí.
Las tuplas internas son más complejas, ya que son puntos de ramificación en el árbol de búsqueda. Cada tupla interna contiene un conjunto de uno o más nodos (nodes), que representan grupos de valores hoja similares. Un nodo contiene un enlace descendente (downlink) que conduce a otra tupla interna de nivel inferior, o a una lista corta de tuplas hoja que se encuentran todas en la misma página de índice. Cada nodo normalmente tiene una etiqueta (label) que lo describe; por ejemplo, en un árbol de prefijos la etiqueta del nodo podría ser el siguiente carácter del valor de la cadena. (Alternativamente, una clase de operadores puede omitir las etiquetas de los nodos si trabaja con un conjunto fijo de nodos para todas las tuplas internas; consulta la Section 65.3.4.2.) Opcionalmente, una tupla interna puede tener un valor de prefijo (prefix) que describe a todos sus miembros. En un árbol de prefijos, este podría ser el prefijo común de las cadenas representadas. El valor del prefijo no es necesariamente un prefijo real, sino que puede ser cualquier dato que necesite la clase de operadores; por ejemplo, en un árbol cuadripolar puede almacenar el punto central con respecto al cual se miden los cuatro cuadrantes. Una tupla interna de un árbol cuadripolar contendría entonces también cuatro nodos correspondientes a los cuadrantes alrededor de este punto central.
Algunos algoritmos de árbol requieren conocer el nivel (o profundidad) de la tupla actual, por lo que el núcleo de SP-GiST ofrece la posibilidad de que las clases de operadores gestionen el conteo de niveles mientras descienden por el árbol. También hay soporte para reconstruir incrementalmente el valor representado cuando sea necesario, y para pasar datos adicionales (llamados valores de recorrido o traverse values) durante el descenso del árbol.
El código del núcleo de SP-GiST se encarga de las entradas nulas. Aunque los índices SP-GiST sí almacenan entradas para valores nulos en las columnas indexadas, esto se oculta del código de la clase de operadores del índice: nunca se pasarán entradas de índice nulas ni condiciones de búsqueda nulas a los métodos de la clase de operadores. (Se asume que los operadores de SP-GiST son estrictos y, por lo tanto, no pueden tener éxito para valores nulos). Los valores nulos, por lo tanto, no se discuten más aquí.
Hay cinco métodos definidos por el usuario que una clase de operadores de índice para
SP-GiST debe proporcionar, y dos son opcionales. Los cinco
métodos obligatorios siguen la convención de aceptar dos argumentos de tipo internal,
el primero de los cuales es un puntero a una estructura C que contiene los valores de entrada
para el método de soporte, mientras que el segundo argumento es un puntero a una estructura C
donde se deben colocar los valores de salida. Cuatro de los métodos obligatorios simplemente
devuelven void, ya que todos sus resultados aparecen en la estructura de salida; pero
leaf_consistent devuelve un resultado de tipo boolean.
Los métodos no deben modificar ningún campo de sus estructuras de entrada. En todos
los casos, la estructura de salida se inicializa con ceros antes de llamar al
método definido por el usuario. El sexto método opcional, compress,
acepta un datum a indexar como único argumento y devuelve un valor adecuado
para el almacenamiento físico en una tupla hoja. El séptimo método opcional,
options, acepta un puntero de tipo internal a una estructura C, donde
se deben colocar los parámetros específicos de la clase de operadores, y devuelve void.
Los cinco métodos obligatorios definidos por el usuario son:
configDevuelve información estática sobre la implementación del índice, incluidos los OID de tipo de datos de los tipos de datos del prefijo y de la etiqueta del nodo.
La declaración SQL de la función debe verse así:
CREATE FUNCTION my_config(internal, internal) RETURNS void ...
El primer argumento es un puntero a una estructura C spgConfigIn,
que contiene los datos de entrada para la función.
El segundo argumento es un puntero a una estructura C spgConfigOut,
que la función debe llenar con los datos del resultado.
typedef struct spgConfigIn
{
Oid attType; /* Tipo de datos a indexar */
} spgConfigIn;
typedef struct spgConfigOut
{
Oid prefixType; /* Tipo de datos de los prefijos de tuplas internas */
Oid labelType; /* Tipo de datos de las etiquetas de nodo de tuplas internas */
Oid leafType; /* Tipo de datos de los valores de tuplas hoja */
bool canReturnData; /* La clase de op. puede reconstruir los datos originales */
bool longValuesOK; /* La clase de op. puede lidiar con valores > 1 página */
} spgConfigOut;
attType se pasa para admitir clases de operadores de
índice polimórficas; para clases de operadores de tipo de datos fijo ordinarias,
siempre tendrá el mismo valor y, por lo tanto, puede ignorarse.
Para las clases de operadores que no usan prefijos,
prefixType se puede establecer en VOIDOID.
Del mismo modo, para las clases de operadores que no usan etiquetas de nodo,
labelType se puede establecer en VOIDOID.
canReturnData debe establecerse en true si la clase de operadores
es capaz de reconstruir el valor de índice suministrado originalmente.
longValuesOK debe establecerse en true solo cuando el
attType es de longitud variable y la clase de operadores
es capaz de segmentar valores largos mediante sufijos repetidos
(consulta la Section 65.3.4.1).
leafType debe coincidir con el tipo de almacenamiento del índice
definido por la entrada de catálogo opckeytype de la clase de operadores.
(Ten en cuenta que opckeytype puede ser cero,
lo que implica que el tipo de almacenamiento es el mismo que el tipo de entrada de la clase
de operadores, lo cual es la situación más común).
Por razones de compatibilidad hacia atrás, el método config
puede establecer leafType en algún otro valor,
y ese valor se utilizará; pero esto está desaconsejado ya que los contenidos del índice
se identifican incorrectamente en los catálogos.
Además, está permitido dejar leafType sin inicializar (cero);
eso se interpreta como el tipo de almacenamiento de índice derivado de
opckeytype.
Cuando attType
y leafType son diferentes, se debe proporcionar el método
opcional compress.
El método compress es responsable
de la transformación de los datums a indexar de attType
a leafType.
chooseElige un método para insertar un nuevo valor en una tupla interna.
La declaración SQL de la función debe verse así:
CREATE FUNCTION my_choose(internal, internal) RETURNS void ...
El primer argumento es un puntero a una estructura C spgChooseIn,
que contiene los datos de entrada para la función.
El segundo argumento es un puntero a una estructura C spgChooseOut,
que la función debe llenar con los datos del resultado.
typedef struct spgChooseIn
{
Datum datum; /* datum original a indexar */
Datum leafDatum; /* datum actual a almacenar en la hoja */
int level; /* nivel actual (contando desde cero) */
/* Datos de la tupla interna actual */
bool allTheSame; /* ¿la tupla está marcada como todo-igual? */
bool hasPrefix; /* ¿la tupla tiene un prefijo? */
Datum prefixDatum; /* si es así, el valor del prefijo */
int nNodes; /* número de nodos en la tupla interna */
Datum *nodeLabels; /* valores de etiqueta del nodo (NULL si no hay) */
} spgChooseIn;
typedef enum spgChooseResultType
{
spgMatchNode = 1, /* descender en el nodo existente */
spgAddNode, /* agregar un nodo a la tupla interna */
spgSplitTuple /* dividir tupla interna (cambiar su prefijo) */
} spgChooseResultType;
typedef struct spgChooseOut
{
spgChooseResultType resultType; /* código de acción, ver arriba */
union
{
struct /* resultados para spgMatchNode */
{
int nodeN; /* descender a este nodo (índice desde 0) */
int levelAdd; /* incrementar el nivel por esta cantidad */
Datum restDatum; /* nuevo datum hoja */
} matchNode;
struct /* resultados para spgAddNode */
{
Datum nodeLabel; /* etiqueta del nuevo nodo */
int nodeN; /* dónde insertarlo (índice desde 0) */
} addNode;
struct /* resultados para spgSplitTuple */
{
/* Info para formar nueva tupla interna superior con una tupla hija */
bool prefixHasPrefix; /* ¿la tupla debería tener un prefijo? */
Datum prefixPrefixDatum; /* si es así, su valor */
int prefixNNodes; /* número de nodos */
Datum *prefixNodeLabels; /* sus etiquetas (o NULL si no hay) */
int childNodeN; /* qué nodo obtiene la tupla hija */
/* Info para formar nueva tupla interna inferior con todos los nodos antiguos */
bool postfixHasPrefix; /* ¿la tupla debería tener un prefijo? */
Datum postfixPrefixDatum; /* si es así, su valor */
} splitTuple;
} result;
} spgChooseOut;
datum is the original datum of type
spgConfigIn.attType
that was to be inserted into the index.
leafDatum es un valor de tipo
spgConfigOut.leafType,
que es inicialmente el resultado del método
compress aplicado a datum
cuando se proporciona el método compress, o el mismo valor que
datum en caso contrario.
leafDatum puede cambiar en niveles inferiores del árbol
si los métodos choose o picksplit
lo modifican. Cuando la búsqueda de inserción alcanza una página hoja,
el valor actual de leafDatum es el que se almacenará
en la tupla hoja recién creada.
level es el nivel de la tupla interna actual, comenzando en
cero para el nivel raíz.
allTheSame es true si la tupla interna actual está
marcada como contenedora de múltiples nodos equivalentes
(consulta la Section 65.3.4.3).
hasPrefix es true si la tupla interna actual contiene
un prefijo; si es así,
prefixDatum es su valor.
nNodes es el número de nodos hijos contenidos en la
tupla interna, y
nodeLabels es un arreglo de sus valores de etiqueta, o
NULL si no hay etiquetas.
La función choose puede determinar que el
nuevo valor coincide con uno de los nodos hijos existentes, o que se debe
agregar un nuevo nodo hijo, o que el nuevo valor es inconsistente con
el prefijo de la tupla y, por lo tanto, la tupla interna debe dividirse para crear un
prefijo menos restrictivo.
Si el nuevo valor coincide con uno de los nodos hijos existentes,
establece resultType en spgMatchNode.
Establece nodeN en el índice (desde cero) de ese nodo en
el arreglo de nodos.
Establece levelAdd en el incremento de
level causado por descender a través de ese nodo,
o déjalo en cero si la clase de operadores no utiliza niveles.
Establece restDatum como igual a leafDatum
si la clase de operadores no modifica los datums de un nivel al
siguiente; de lo contrario, establécelo en el valor modificado que se utilizará como
leafDatum en el siguiente nivel.
Si se debe agregar un nuevo nodo hijo,
establece resultType en spgAddNode.
Establece nodeLabel en la etiqueta que se utilizará para el nuevo
nodo, y establece nodeN en el índice (desde cero) en el que
se debe insertar el nodo en el arreglo de nodos.
Después de agregar el nodo, la función choose
se llamará nuevamente con la tupla interna modificada;
esa llamada debería dar como resultado un resultado spgMatchNode.
Si el nuevo valor es inconsistente con el prefijo de la tupla,
establece resultType en spgSplitTuple.
Esta acción mueve todos los nodos existentes a una nueva tupla interna de
nivel inferior, y reemplaza la tupla interna existente con una tupla
que tiene un único enlace descendente que apunta a la nueva tupla interna de nivel inferior.
Establece prefixHasPrefix para indicar si la nueva
tupla superior debe tener un prefijo, y si es así establece
prefixPrefixDatum en el valor del prefijo. Este nuevo
valor de prefijo debe ser lo suficientemente menos restrictivo que el original
como para aceptar el nuevo valor a indexar.
Establece prefixNNodes en el número de nodos necesarios en la
nueva tupla, y establece prefixNodeLabels en un arreglo asignado con palloc
que contenga sus etiquetas, o en NULL si no se requieren etiquetas de nodo.
Ten en cuenta que el tamaño total de la nueva tupla superior no debe ser mayor
que el tamaño total de la tupla que está reemplazando; esto restringe
las longitudes del nuevo prefijo y de las nuevas etiquetas.
Establece childNodeN en el índice (desde cero) del nodo
que enlazará descendentemente a la nueva tupla interna de nivel inferior.
Establece postfixHasPrefix para indicar si la nueva
tupla interna de nivel inferior debe tener un prefijo, y si es así establece
postfixPrefixDatum en el valor del prefijo. La
combinación de estos dos prefijos y la etiqueta del nodo de enlace descendente
(si la hay) debe tener el mismo significado que el prefijo original, porque
no hay oportunidad de alterar las etiquetas de los nodos que se mueven a
la nueva tupla de nivel inferior, ni de cambiar ninguna entrada de índice hija.
Después de dividir el nodo, la función choose
se llamará nuevamente con la tupla interna de reemplazo.
Esa llamada puede devolver un resultado spgAddNode si no se creó ningún
nodo adecuado mediante la acción spgSplitTuple. Eventualmente,
choose debe devolver spgMatchNode para
permitir que la inserción descienda al siguiente nivel.
picksplitDecide cómo crear una nueva tupla interna sobre un conjunto de tuplas hoja.
La declaración SQL de la función debe verse así:
CREATE FUNCTION my_picksplit(internal, internal) RETURNS void ...
El primer argumento es un puntero a una estructura C spgPickSplitIn,
que contiene los datos de entrada para la función.
El segundo argumento es un puntero a una estructura C spgPickSplitOut,
que la función debe llenar con los datos del resultado.
typedef struct spgPickSplitIn
{
int nTuples; /* número de tuplas hoja */
Datum *datums; /* sus datums (arreglo de longitud nTuples) */
int level; /* nivel actual (contando desde cero) */
} spgPickSplitIn;
typedef struct spgPickSplitOut
{
bool hasPrefix; /* ¿la nueva tupla interna debería tener un prefijo? */
Datum prefixDatum; /* si es así, su valor */
int nNodes; /* número de nodos para la nueva tupla interna */
Datum *nodeLabels; /* sus etiquetas (o NULL si no hay etiquetas) */
int *mapTuplesToNodes; /* índice del nodo para cada tupla hoja */
Datum *leafTupleDatums; /* datum a almacenar en cada nueva tupla hoja */
} spgPickSplitOut;
nTuples es el número de tuplas hoja proporcionadas.
datums es un arreglo de sus valores de datum de tipo
spgConfigOut.leafType.
level es el nivel actual que comparten todas las tuplas hoja,
que se convertirá en el nivel de la nueva tupla interna.
Establece hasPrefix para indicar si la nueva tupla interna
debe tener un prefijo, y si es así establece
prefixDatum en el valor del prefijo.
Establece nNodes para indicar el número de nodos que
contendrá la nueva tupla interna, y
establece nodeLabels en un arreglo de sus valores de etiqueta,
o en NULL si no se requieren etiquetas de nodo.
Establece mapTuplesToNodes en un arreglo que proporcione el índice
(desde cero) del nodo al que se debe asignar cada tupla hoja.
Establece leafTupleDatums en un arreglo de los valores a
almacenar en las nuevas tuplas hoja (estos serán los mismos que los
datums de entrada si la clase de operadores no modifica
los datums de un nivel al siguiente).
Ten en cuenta que la función picksplit es
responsable de asignar con palloc los arreglos
nodeLabels, mapTuplesToNodes y
leafTupleDatums.
Si se suministra más de una tupla hoja, se espera que la
función picksplit las clasifique en más de
un nodo; de lo contrario, no es posible dividir las tuplas hoja
en varias páginas, lo cual es el propósito final de esta
operación. Por lo tanto, si la función picksplit
termina colocando todas las tuplas hoja en el mismo nodo, el código central
de SP-GiST anulará esa decisión y generará una tupla interna
en la que las tuplas hoja se asignan al azar a varios
nodos con etiquetas idénticas. Dicha tupla se marca como
allTheSame para indicar que esto ha sucedido. Las
funciones choose e inner_consistent
deben tener el cuidado adecuado con tales tuplas internas.
Consulta la Section 65.3.4.3 para más información.
picksplit se puede aplicar a una sola tupla hoja únicamente
en el caso de que la función config haya establecido
longValuesOK en true y se haya suministrado un valor de entrada
mayor que una página. En este caso, el objetivo de la operación es
eliminar un prefijo y producir un nuevo valor de datum hoja más corto.
La llamada se repetirá hasta que se haya producido un datum hoja lo suficientemente corto
como para caber en una página. Consulta la Section 65.3.4.1 para
más información.
inner_consistentDevuelve el conjunto de nodos (ramas) a seguir durante la búsqueda en el árbol.
La declaración SQL de la función debe verse así:
CREATE FUNCTION my_inner_consistent(internal, internal) RETURNS void ...
El primer argumento es un puntero a una estructura C spgInnerConsistentIn,
que contiene los datos de entrada para la función.
El segundo argumento es un puntero a una estructura C spgInnerConsistentOut,
que la función debe llenar con los datos del resultado.
typedef struct spgInnerConsistentIn
{
ScanKey scankeys; /* arreglo de operadores y valores de comparación */
ScanKey orderbys; /* arreglo de operadores de ordenamiento y valores
* de comparación */
int nkeys; /* longitud del arreglo scankeys */
int norderbys; /* longitud del arreglo orderbys */
Datum reconstructedValue; /* valor reconstruido en el padre */
void *traversalValue; /* valor de recorrido específico de la opclass */
MemoryContext traversalMemoryContext; /* colocar nuevos valores de recorrido aquí */
int level; /* nivel actual (contando desde cero) */
bool returnData; /* ¿se deben devolver los datos originales? */
/* Datos de la tupla interna actual */
bool allTheSame; /* ¿la tupla está marcada como todo-igual? */
bool hasPrefix; /* ¿la tupla tiene un prefijo? */
Datum prefixDatum; /* si es así, el valor del prefijo */
int nNodes; /* número de nodos en la tupla interna */
Datum *nodeLabels; /* valores de etiqueta del nodo (NULL si no hay) */
} spgInnerConsistentIn;
typedef struct spgInnerConsistentOut
{
int nNodes; /* número de nodos hijos a visitar */
int *nodeNumbers; /* sus índices en el arreglo de nodos */
int *levelAdds; /* incrementar el nivel por esta cantidad para cada uno */
Datum *reconstructedValues; /* valores reconstruidos asociados */
void **traversalValues; /* valores de recorrido específicos de la opclass */
double **distances; /* distancias asociadas */
} spgInnerConsistentOut;
El arreglo scankeys, de longitud nkeys,
describe la(s) condición(es) de búsqueda del índice. Estas condiciones se
combinan con AND — solo interesan las entradas de índice que satisfagan todas
ellas. (Ten en cuenta que nkeys = 0 implies
que todas las entradas del índice satisfacen la consulta). Por lo general, la función
consistente solo se preocupa por los campos sk_strategy y
sk_argument de cada entrada del arreglo, los cuales
dan respectivamente el operador indexable y el valor de comparación.
En particular, no es necesario comprobar sk_flags para
ver si el valor de comparación es NULL, porque el código central de SP-GiST
filtrará tales condiciones.
El arreglo orderbys, de longitud norderbys,
describe los operadores de ordenamiento (si los hay) de la misma manera.
reconstructedValue is the value reconstructed for the
parent tuple; it is (Datum) 0 at the root level or if the
inner_consistent function did not provide a value at the
parent level.
traversalValue is a pointer to any traverse data
passed down from the previous call of inner_consistent
on the parent index tuple, or NULL at the root level.
traversalMemoryContext es el contexto de memoria en el cual
almacenar los valores de recorrido de salida (ver más abajo).
level es el nivel de la tupla interna actual, comenzando en
cero para el nivel raíz.
returnData es true si se requieren datos reconstruidos
para esta consulta; esto solo será así si la
función config afirmó canReturnData.
allTheSame es true si la tupla interna actual está
marcada como “all-the-same”; en este caso todos los nodos tienen la
misma etiqueta (si la hay) y, por lo tanto, todos o ninguno de ellos coinciden con la consulta
(consulta la Section 65.3.4.3).
hasPrefix es true si la tupla interna actual contiene
un prefijo; si es así,
prefixDatum es su valor.
nNodes es el número de nodos hijos contenidos en la
tupla interna, y
nodeLabels es un arreglo de sus valores de etiqueta, o
NULL si los nodos no tienen etiquetas.
nNodes debe establecerse en el número de nodos hijos que
deben ser visitados por la búsqueda, y
nodeNumbers debe establecerse en un arreglo de sus índices.
Si la clase de operadores realiza un seguimiento de los niveles, establece
levelAdds en un arreglo de los incrementos de nivel
requeridos al descender a cada nodo a visitar. (A menudo estos
incrementos serán los mismos para todos los nodos, pero eso no es
necesariamente así, por lo que se utiliza un arreglo).
Si se necesita la reconstrucción de valores, establece
reconstructedValues en un arreglo de los valores
reconstruidos para cada nodo hijo a visitar; de lo contrario, deja
reconstructedValues como NULL.
Se asume que los valores reconstruidos son de tipo
spgConfigOut.leafType.
(Sin embargo, dado que el sistema central no hará nada con ellos excepto
posiblemente copiarlos, es suficiente que tengan las
mismas propiedades typlen y typbyval
que leafType).
Si se realiza una búsqueda ordenada, establece distances
en un arreglo de valores de distancia de acuerdo con el arreglo orderbys
(los nodos con las distancias más bajas se procesarán primero). Déjalo
como NULL en caso contrario.
Si se desea transmitir información adicional fuera de banda
(“valores de recorrido”) a niveles inferiores de la búsqueda del árbol,
establece traversalValues en un arreglo de los valores de
recorrido apropiados, uno para cada nodo hijo a visitar; de lo contrario,
deja traversalValues como NULL.
Ten en cuenta que la función inner_consistent es
responsable de asignar con palloc los arreglos
nodeNumbers, levelAdds,
distances,
reconstructedValues y
traversalValues en el contexto de memoria actual.
Sin embargo, cualquier valor de recorrido de salida apuntado por
el arreglo traversalValues debe asignarse
en traversalMemoryContext.
Cada valor de recorrido debe ser un único bloque asignado con palloc.
leaf_consistentDevuelve true si una tupla hoja satisface una consulta.
La declaración SQL de la función debe verse así:
CREATE FUNCTION my_leaf_consistent(internal, internal) RETURNS bool ...
El primer argumento es un puntero a una estructura C spgLeafConsistentIn,
que contiene los datos de entrada para la función.
El segundo argumento es un puntero a una estructura C spgLeafConsistentOut,
que la función debe llenar con los datos del resultado.
typedef struct spgLeafConsistentIn
{
ScanKey scankeys; /* arreglo de operadores y valores de comparación */
ScanKey orderbys; /* arreglo de operadores de ordenamiento y valores
* de comparación */
int nkeys; /* longitud del arreglo scankeys */
int norderbys; /* longitud del arreglo orderbys */
Datum reconstructedValue; /* valor reconstruido en el padre */
void *traversalValue; /* valor de recorrido específico de la opclass */
int level; /* nivel actual (contando desde cero) */
bool returnData; /* ¿se deben devolver los datos originales? */
Datum leafDatum; /* datum en la tupla hoja */
} spgLeafConsistentIn;
typedef struct spgLeafConsistentOut
{
Datum leafValue; /* datos originales reconstruidos, si los hay */
bool recheck; /* establecer en true si el operador debe ser recheckeado */
bool recheckDistances; /* establecer en true si las distancias deben ser recheckeadas */
double *distances; /* distancias asociadas */
} spgLeafConsistentOut;
El arreglo scankeys, de longitud nkeys,
describe la(s) condición(es) de búsqueda del índice. Estas condiciones se
combinan con AND — solo las entradas de índice que satisfagan todas
ellas satisfacen la consulta. (Ten en cuenta que nkeys = 0 implica
que todas las entradas del índice satisfacen la consulta). Por lo general, la función
consistente solo se preocupa por los campos sk_strategy y
sk_argument de cada entrada del arreglo, los cuales
dan respectivamente el operador indexable y el valor de comparación.
En particular, no es necesario comprobar sk_flags para
ver si el valor de comparación es NULL, porque el código central de SP-GiST
filtrará tales condiciones.
El arreglo orderbys, de longitud norderbys,
describe los operadores de ordenamiento de la misma manera.
reconstructedValue es el valor reconstruido para la
tupla padre; es (Datum) 0 en el nivel raíz o si la
función inner_consistent no proporcionó un valor en el
nivel padre.
traversalValue is a pointer to any traverse data
passed down from the previous call of inner_consistent
on the parent index tuple, or NULL at the root level.
level es el nivel de la tupla hoja actual, comenzando en
cero para el nivel raíz.
returnData es true si se requieren datos reconstruidos
para esta consulta; esto solo será así si la
función config afirmó canReturnData.
leafDatum es el valor clave de tipo
spgConfigOut.leafType
almacenado en la tupla hoja actual.
La función debe devolver true si la tupla hoja coincide con la
consulta, o false si no. En el caso true,
si returnData is true, then
leafValue debe establecerse en el valor (de tipo
spgConfigIn.attType)
suministrado originalmente para ser indexado para esta tupla hoja. Además,
recheck se puede establecer en true si la coincidencia
es incierta y, por lo tanto, el/los operador(es) deben volver a aplicarse a la tupla
de montón (heap) real para verificar la coincidencia.
Si se realiza una búsqueda ordenada, establece distances
en un arreglo de valores de distancia de acuerdo con el arreglo orderbys.
Déjalo como NULL en caso contrario. Si al menos una de las distancias devueltas
no es exacta, establece recheckDistances en true.
En este caso, el ejecutor calculará las distancias exactas después de
obtener la tupla del montón y reordenará las tuplas si es necesario.
Los métodos opcionales definidos por el usuario son:
Datum compress(Datum in)
Convierte un elemento de datos en un formato adecuado para el almacenamiento físico en
una tupla hoja del índice. Acepta un valor de tipo
spgConfigIn.attType
y devuelve un valor de tipo
spgConfigOut.leafType.
El valor de salida no debe contener un puntero TOAST fuera de línea.
Nota: el método compress solo se aplica a los
valores a almacenar. Los métodos consistentes reciben las
scankeys de la consulta sin cambios, sin transformación
mediante compress.
optionsDefine un conjunto de parámetros visibles para el usuario que controlan el comportamiento de la clase de operadores.
La declaración SQL de la función debe verse así:
CREATE OR REPLACE FUNCTION my_options(internal) RETURNS void AS 'MODULE_PATHNAME' LANGUAGE C STRICT;
A la función se le pasa un puntero a una estructura local_relopts,
que debe llenarse con un conjunto de opciones específicas de la clase de operadores.
Se puede acceder a las opciones desde otras funciones de soporte utilizando las macros
PG_HAS_OPCLASS_OPTIONS() y
PG_GET_OPCLASS_OPTIONS().
Dado que la representación de la clave en SP-GiST es flexible, puede depender de parámetros especificados por el usuario.
Todos los métodos de soporte de SP-GiST normalmente se llaman en un contexto de memoria
de vida corta; es decir, CurrentMemoryContext se restablecerá
después del procesamiento de cada tupla. Por lo tanto, no es muy importante
preocuparse por liberar con pfree todo lo que asignes con palloc. (El método config
es una excepción: debe intentar evitar fugas de memoria. Pero
por lo general, el método config no necesita hacer nada más que asignar
constantes en la estructura de parámetros pasada).
Si la columna indexada es de un tipo de datos ordenable (collatable), la colación del índice
se pasará a todos los métodos de soporte, utilizando el mecanismo estándar
PG_GET_COLLATION().
Esta sección cubre detalles de implementación y otros trucos que es útil que conozcan los implementadores de clases de operadores SP-GiST.
Las tuplas hoja individuales y las tuplas internas deben caber en una sola página de índice
(8 kB por defecto). Por lo tanto, al indexar valores de tipos de datos de longitud
variable, los valores largos solo pueden ser soportados por métodos como los
árboles de prefijos, en los cuales cada nivel del árbol incluye un prefijo que es lo suficientemente
corto como para caber en una página, y el nivel hoja final incluye un sufijo que también es lo suficientemente
corto como para caber en una página. La clase de operadores debe establecer
longValuesOK en true solo si está preparada para disponer las cosas para que
esto suceda. De lo contrario, el núcleo de SP-GiST
rechazará cualquier solicitud para indexar un valor que sea demasiado grande para caber
en una página de índice.
Asimismo, es responsabilidad de la clase de operadores que las tuplas internas no crezcan demasiado para caber en una página de índice; esto limita el número de nodos hijos que se pueden usar en una tupla interna, así como el tamaño máximo de un valor de prefijo.
Otra limitación 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.
(Esta es una decisión de diseño para reducir las búsquedas y ahorrar espacio en los
enlaces que encadenan tales tuplas). Si el conjunto de tuplas hoja
crece demasiado para una página, se realiza una división y se inserta una tupla
interna intermedia. Para que esto solucione el problema, la nueva tupla
interna debe dividir el conjunto de valores hoja en más de un
grupo de nodos. Si la función picksplit de la clase de operadores
falla al hacer eso, el núcleo de SP-GiST recurre a las
medidas extraordinarias descritas en la Section 65.3.4.3.
Cuando longValuesOK es true, se espera
que los niveles sucesivos del árbol SP-GiST
absorban cada vez más información en los prefijos y etiquetas de nodo de
las tuplas internas, haciendo que el datum hoja requerido sea cada vez más pequeño,
de modo que eventualmente quepa en una página.
Para evitar que los errores en las clases de operadores provoquen bucles infinitos
de inserción, el núcleo de SP-GiST generará un error si el
datum hoja no se vuelve más pequeño en diez ciclos
de llamadas al método choose.
Algunos algoritmos de árbol utilizan un conjunto fijo de nodos para cada tupla interna;
por ejemplo, en un árbol cuadripolar siempre hay exactamente cuatro nodos
correspondientes a los cuatro cuadrantes alrededor del punto centroide de la tupla interna.
En tal caso, el código normalmente trabaja con los nodos por su
número, y no hay necesidad de etiquetas de nodo explícitas. Para suprimir
las etiquetas de nodo (y así ahorrar algo de espacio), la función picksplit
puede devolver NULL para el arreglo nodeLabels,
y del mismo modo la función choose puede devolver NULL para
el arreglo prefixNodeLabels durante
una acción spgSplitTuple.
Esto a su vez dará como resultado que nodeLabels sea NULL durante
las llamadas subsiguientes a choose y inner_consistent.
En principio, las etiquetas de nodo se pueden usar para algunas tuplas internas y omitirse
para otras en el mismo índice.
Al trabajar con una tupla interna que tiene nodos sin etiquetar, es un error
que choose devuelva spgAddNode, ya que se supone
que el conjunto de nodos es fijo en tales casos.
El núcleo de SP-GiST puede anular los resultados de la
función picksplit de la clase de operadores cuando
picksplit no logra dividir los valores hoja suministrados en
al menos dos categorías de nodos. Cuando esto sucede, la nueva tupla interna
se crea con múltiples nodos que tienen cada uno la misma etiqueta (si la hay)
que picksplit le dio al único nodo que sí usó, y los
valores hoja se dividen al azar entre estos nodos equivalentes.
La bandera allTheSame se establece en la tupla interna para advertir a las
funciones choose e inner_consistent que la
tupla no tiene el conjunto de nodos que de otro modo podrían esperar.
Cuando se trata de una tupla allTheSame, un resultado de
choose de tipo spgMatchNode se interpreta en el sentido de que el nuevo
valor se puede asignar a cualquiera de los nodos equivalentes; el código central
ignorará el valor de nodeN suministrado y descenderá a uno
de los nodos al azar (para mantener el árbol balanceado). Es un
error que choose devuelva spgAddNode, ya que
eso haría que los nodos no fueran todos equivalentes; se debe utilizar la
acción spgSplitTuple si el valor a insertar
no coincide con los nodos existentes.
Cuando se trata de una tupla allTheSame, la
función inner_consistent debe devolver todos o ninguno
de los nodos como objetivos para continuar la búsqueda del índice, ya que son
todos equivalentes. Esto puede o no requerir código de caso especial,
dependiendo de cuánto asuma normalmente la función inner_consistent
sobre el significado de los nodos.
La distribución de fuentes de PostgreSQL incluye
varios ejemplos de clases de operadores de índice para SP-GiST,
como se describe en la Table 65.2. Consulta
src/backend/access/spgist/
y src/backend/utils/adt/ para ver el código.