65.3. Índices SP-GiST #

65.3.1. Introducción
65.3.2. Clases de operadores integradas
65.3.3. Extensibilidad
65.3.4. Implementación
65.3.5. Ejemplos

65.3.1. Introducción #

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.

65.3.2. Clases de operadores integradas #

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

NombreOperadores indexablesOperadores 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.

65.3.3. Extensibilidad #

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.

Note

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:

config

Devuelve 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.

choose

Elige 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.

picksplit

Decide 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_consistent

Devuelve 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_consistent

Devuelve 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.

options

Define 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().

65.3.4. Implementación #

Esta sección cubre detalles de implementación y otros trucos que es útil que conozcan los implementadores de clases de operadores SP-GiST.

65.3.4.1. Límites de 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.

65.3.4.2. SP-GiST sin etiquetas de nodo #

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.

65.3.4.3. Tuplas internas de tipo todo-igual (all-the-same) #

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.

65.3.5. Ejemplos #

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.