36.16. Acoplamiento de extensiones a índices #

36.16.1. Métodos de indexación y clases de operadores
36.16.2. Estrategias de los métodos de indexación
36.16.3. Rutinas de soporte de los métodos de indexación
36.16.4. Un ejemplo
36.16.5. Clases de operadores y familias de operadores
36.16.6. Dependencias del sistema respecto a las clases de operadores
36.16.7. Operadores de ordenación
36.16.8. Características especiales de las clases de operadores

Los procedimientos descritos hasta ahora te permiten definir nuevos tipos, nuevas funciones y nuevos operadores. Sin embargo, todavía no podemos definir un índice en una columna de un nuevo tipo de datos. Para hacer esto, debemos definir una clase de operadores (operator class) para el nuevo tipo de datos. Más adelante en esta sección, ilustraremos este concepto con un ejemplo: una nueva clase de operadores para el método de indexación B-tree que almacena y ordena números complejos en orden ascendente de su valor absoluto.

Las clases de operadores se pueden agrupar en familias de operadores (operator families) para mostrar las relaciones entre clases semánticamente compatibles. Cuando solo interviene un único tipo de datos, una clase de operadores es suficiente, por lo que nos centraremos primero en ese caso y luego volveremos a las familias de operadores.

36.16.1. Métodos de indexación y clases de operadores #

Las clases de operadores están asociadas con un método de acceso a índices, como B-Tree o GIN. Se pueden definir métodos de acceso a índices personalizados con CREATE ACCESS METHOD. Consulta Chapter 63 para obtener más detalles.

Las rutinas de un método de indexación no saben directamente nada sobre los tipos de datos sobre los que operará el método de indexación. En su lugar, una clase de operadores identifica el conjunto de operaciones que el método de indexación necesita usar para trabajar con un tipo de datos particular. Las clases de operadores se llaman así porque una de las cosas que especifican es el conjunto de operadores de la cláusula WHERE que se pueden usar con un índice (es decir, que se pueden convertir en una condición de escaneo de índice). Una clase de operadores también puede especificar algunas funciones de soporte (support functions) que son necesarias para las operaciones internas del método de indexación, pero que no corresponden directamente a ningún operador de la cláusula WHERE que se pueda usar con el índice.

Es posible definir múltiples clases de operadores para el mismo tipo de datos y método de indexación. Al hacer esto, se pueden definir múltiples conjuntos de semánticas de indexación para un solo tipo de datos. Por ejemplo, un índice B-tree requiere que se defina un orden de clasificación para cada tipo de datos con el que trabaja. Podría ser útil para un tipo de datos de números complejos tener una clase de operadores B-tree que ordene los datos por el valor absoluto del número complejo, otra que los ordene por la parte real, y así sucesivamente. Típicamente, una de las clases de operadores se considerará la más útil y se marcará como la clase de operadores por defecto para ese tipo de datos y método de indexación.

Se puede usar el mismo nombre de clase de operadores para varios métodos de indexación diferentes (por ejemplo, tanto los métodos de indexación B-tree como los de hash tienen clases de operadores llamadas int4_ops), pero cada una de esas clases es una entidad independiente y debe definirse por separado.

36.16.2. Estrategias de los métodos de indexación #

Los operadores asociados con una clase de operadores se identifican mediante números de estrategia (strategy numbers), que sirven para identificar la semántica de cada operador dentro del contexto de su clase de operadores. Por ejemplo, los B-trees imponen un orden estricto en las claves, de menor a mayor, y por lo tanto los operadores como menor que y mayor o igual que son interesantes con respecto a un B-tree. Dado que PostgreSQL permite al usuario definir operadores, PostgreSQL no puede mirar el nombre de un operador (por ejemplo, < o >=) y saber qué tipo de comparación es. En su lugar, el método de indexación define un conjunto de estrategias, que pueden pensarse como operadores generalizados. Cada clase de operadores especifica qué operador real corresponde a cada estrategia para un tipo de datos particular y una interpretación de la semántica del índice.

El método de indexación B-tree define cinco estrategias, que se muestran en la tabla Table 36.3.

Table 36.3. Estrategias de B-Tree

OperaciónNúmero de estrategia
menor que1
menor o igual que2
igual3
mayor o igual que4
mayor que5

Los índices hash solo admiten comparaciones de igualdad, por lo que utilizan solo una estrategia, que se muestra en la tabla Table 36.4.

Table 36.4. Estrategias de Hash

OperaciónNúmero de estrategia
igual1

Los índices GiST son más flexibles: no tienen un conjunto fijo de estrategias en absoluto. En su lugar, la rutina de soporte de consistencia (consistency) de cada clase de operadores GiST particular interpreta los números de estrategia como quiera. Como ejemplo, varias de las clases de operadores de índice GiST integradas indexan objetos geométricos bidimensionales, proporcionando las estrategias de R-tree que se muestran en la tabla Table 36.5. Cuatro de estas son pruebas bidimensionales reales (se superpone, igual, contiene, contenido por); cuatro de ellas consideran solo la dirección X; y las otras cuatro proporcionan las mismas pruebas en la dirección Y.

Table 36.5. Estrategias de R-tree bidimensionales de GiST

OperaciónNúmero de estrategia
estrictamente a la izquierda de1
no se extiende a la derecha de2
se superpone3
no se extiende a la izquierda de4
estrictamente a la derecha de5
igual6
contiene7
contenido por8
no se extiende por encima de9
estrictamente por debajo de10
estrictamente por encima de11
no se extiende por debajo de12

Los índices SP-GiST son similares a los índices GiST en flexibilidad: no tienen un conjunto fijo de estrategias. En su lugar, las rutinas de soporte de cada clase de operadores interpretan los números de estrategia de acuerdo con la definición de la clase de operadores. Como ejemplo, los números de estrategia utilizados por las clases de operadores integradas para puntos se muestran en la tabla Table 36.6.

Table 36.6. Estrategias de puntos de SP-GiST

OperaciónNúmero de estrategia
estrictamente a la izquierda de1
estrictamente a la derecha de5
igual6
contenido por8
estrictamente por debajo de10
estrictamente por encima de11

Los índices GIN son similares a los índices GiST y SP-GiST, en el sentido de que tampoco tienen un conjunto fijo de estrategias. En su lugar, las rutinas de soporte de cada clase de operadores interpretan los números de estrategia de acuerdo con la definición de la clase de operadores. Como ejemplo, los números de estrategia utilizados por la clase de operadores integrada para arrays se muestran en la tabla Table 36.7.

Table 36.7. Estrategias de arrays de GIN

OperaciónNúmero de estrategia
se superpone1
contiene2
está contenido por3
igual4

Los índices BRIN son similares a los índices GiST, SP-GiST y GIN en que tampoco tienen un conjunto fijo de estrategias. En su lugar, las rutinas de soporte de cada clase de operadores interpretan los números de estrategia de acuerdo con la definición de la clase de operadores. Como ejemplo, los números de estrategia utilizados por las clases de operadores integradas Minmax se muestran en la tabla Table 36.8.

Table 36.8. Estrategias Minmax de BRIN

OperaciónNúmero de estrategia
menor que1
menor o igual que2
igual3
mayor o igual que4
mayor que5

Ten en cuenta que todos los operadores enumerados anteriormente devuelven valores booleanos. En la práctica, todos los operadores definidos como operadores de búsqueda del método de indexación deben devolver el tipo boolean, ya que deben aparecer en el nivel superior de una cláusula WHERE para ser utilizados con un índice. (Algunos métodos de acceso a índices también admiten operadores de ordenación [ordering operators], que típicamente no devuelven valores booleanos; esa característica se analiza en la sección Section 36.16.7).

36.16.3. Rutinas de soporte de los métodos de indexación #

Las estrategias no suelen ser suficiente información para que el sistema averigüe cómo usar un índice. En la práctica, los métodos de indexación requieren rutinas de soporte adicionales para funcionar. Por ejemplo, el método de indexación B-tree debe ser capaz de comparar dos claves y determinar si una es mayor, igual o menor que la otra. Del mismo modo, el método de indexación hash debe ser capaz de calcular códigos hash para los valores de las claves. Estas operaciones no corresponden a los operadores utilizados en las condiciones de los comandos SQL; son rutinas administrativas utilizadas internamente por los métodos de indexación.

Al igual que con las estrategias, la clase de operadores identifica qué funciones específicas deben desempeñar cada uno de estos roles para un tipo de datos y una interpretación semántica dados. El método de indexación define el conjunto de funciones que necesita, y la clase de operadores identifica las funciones correctas a usar asignándolas a los números de función de soporte especificados por el método de indexación.

Además, algunas clases de operadores permiten a los usuarios especificar parámetros que controlan su comportamiento. Cada método de acceso a índices integrado tiene una función de soporte opcional options, que define un conjunto de parámetros específicos de la clase de operadores.

Los B-trees requieren una función de soporte de comparación, y permiten suministrar cuatro funciones de soporte adicionales a opción del autor de la clase de operadores, como se muestra en la tabla Table 36.9. Los requisitos para estas funciones de soporte se explican con más detalle en la sección Section 65.1.3.

Table 36.9. Funciones de soporte de B-Tree

FunciónNúmero de soporte
Comparar dos claves y devolver un entero menor que cero, cero o mayor que cero, indicando si la primera clave es menor, igual o mayor que la segunda 1
Devolver las direcciones de las funciones de soporte de ordenación llamables desde C (opcional) 2
Comparar un valor de prueba con un valor base más/menos un desplazamiento, y devolver true o false según el resultado de la comparación (opcional) 3
Determinar si es seguro para los índices que usan la clase de operadores aplicar la optimización de deduplicación de btree (opcional) 4
Definir opciones que son específicas de esta clase de operadores (opcional) 5
Devolver las direcciones de las funciones de soporte de salto (skip) llamables desde C (opcional) 6

Los índices hash requieren una función de soporte y permiten suministrar dos adicionales a opción del autor de la clase de operadores, como se muestra en la tabla Table 36.10.

Table 36.10. Funciones de soporte de Hash

FunciónNúmero de soporte
Calcular el valor hash de 32 bits para una clave1
Calcular el valor hash de 64 bits para una clave dada una sal de 64 bits; si la sal es 0, los 32 bits bajos del resultado deben coincidir con el valor que habría sido calculado por la función 1 (opcional) 2
Definir opciones que son específicas de esta clase de operadores (opcional) 3

Los índices GiST tienen doce funciones de soporte, siete de las cuales son opcionales, como se muestra en la tabla Table 36.11. (Para más información consulta la sección Section 65.2).

Table 36.11. Funciones de soporte de GiST

FunciónDescripciónNúmero de soporte
consistentdeterminar si la clave satisface la condición de la consulta1
unioncalcular la unión de un conjunto de claves2
compresscalcular una representación comprimida de una clave o valor a indexar (opcional)3
decompresscalcular una representación descomprimida de una clave comprimida (opcional)4
penaltycalcular la penalización por insertar una nueva clave en el subárbol con la clave del subárbol dado5
picksplitdeterminar qué entradas de una página se deben mover a la nueva página y calcular las claves de unión para las páginas resultantes6
samecomparar dos claves y devolver true si son iguales7
distancedeterminar la distancia desde la clave al valor de la consulta (opcional)8
fetchcalcular la representación original de una clave comprimida para escaneos de solo índice (index-only scans) (opcional)9
optionsdefinir opciones que son específicas de esta clase de operadores (opcional)10
sortsupportproporcionar un comparador de ordenación para ser utilizado en construcciones rápidas de índices (opcional)11
translate_cmptypetraducir los tipos de comparación a los números de estrategia utilizados por la clase de operadores (opcional)12

Los índices SP-GiST tienen seis funciones de soporte, una de las cuales es opcional, como se muestra en la tabla Table 36.12. (Para más información consulta la sección Section 65.3).

Table 36.12. Funciones de soporte de SP-GiST

FunciónDescripciónNúmero de soporte
configproporcionar información básica sobre la clase de operadores1
choosedeterminar cómo insertar un nuevo valor en una tupla interna2
picksplitdeterminar cómo particionar un conjunto de valores3
inner_consistentdeterminar qué subparticiones deben buscarse para una consulta4
leaf_consistentdeterminar si la clave satisface la condición de la consulta5
optionsdefinir opciones que son específicas de esta clase de operadores (opcional)6

Los índices GIN tienen siete funciones de soporte, cuatro de las cuales son opcionales, como se muestra en la tabla Table 36.13. (Para más información consulta la sección Section 65.4).

Table 36.13. Funciones de soporte de GIN

FunciónDescripciónNúmero de soporte
compare comparar dos claves y devolver un entero menor que cero, cero, o mayor que cero, indicando si la primera clave es menor, igual o mayor que la segunda 1
extractValueextraer claves de un valor a indexar2
extractQueryextraer claves de una condición de consulta3
consistent determinar si el valor coincide con la condición de la consulta (variante booleana) (opcional si la función de soporte 6 está presente) 4
comparePartial comparar la clave parcial de la consulta y la clave del índice, y devolver un entero menor que cero, cero, o mayor que cero, indicando si GIN debe ignorar esta entrada de índice, tratar la entrada como una coincidencia o detener el escaneo de índice (opcional) 5
triConsistent determinar si el valor coincide con la condición de la consulta (variante ternaria) (opcional si la función de soporte 4 está presente) 6
options definir opciones que son específicas de esta clase de operadores (opcional) 7

Los índices BRIN tienen cinco funciones de soporte básicas, una de las cuales es opcional, como se muestra en la tabla Table 36.14. Algunas versiones de las funciones básicas requieren que se proporcionen funciones de soporte adicionales. (Para más información consulta la sección Section 65.5.3).

Table 36.14. Funciones de soporte de BRIN

FunciónDescripciónNúmero de soporte
opcInfo devolver información interna que describe los datos de resumen de las columnas indexadas 1
add_valueañadir un nuevo valor a una tupla de índice de resumen existente2
consistentdeterminar si el valor coincide con la condición de la consulta3
union calcular la unión de dos tuplas de resumen 4
options definir opciones que son específicas de esta clase de operadores (opcional) 5

A diferencia de los operadores de búsqueda, las funciones de soporte devuelven el tipo de datos que el método de indexación particular espera; por ejemplo, en el caso de la función de comparación para B-trees, un entero con signo. El número y los tipos de los argumentos de cada función de soporte son igualmente dependientes del método de indexación. Para B-tree y hash, las funciones de soporte de comparación y hashing toman los mismos tipos de datos de entrada que los operadores incluidos en la clase de operadores, pero este no es el caso para la mayoría de las funciones de soporte de GiST, SP-GiST, GIN y BRIN.

36.16.4. Un ejemplo #

Ahora que hemos visto las ideas, aquí tienes el ejemplo prometido de creación de una nueva clase de operadores. (Puedes encontrar una copia funcional de este ejemplo en src/tutorial/complex.c y src/tutorial/complex.sql en la distribución de las fuentes). La clase de operadores encapsula operadores que ordenan números complejos en orden de su valor absoluto, por lo que elegimos el nombre complex_abs_ops. Primero, necesitamos un conjunto de operadores. El procedimiento para definir operadores se analizó en la sección Section 36.14. Para una clase de operadores en B-trees, los operadores que requerimos son:

  • valor absoluto menor que (estrategia 1)
  • valor absoluto menor o igual que (estrategia 2)
  • valor absoluto igual (estrategia 3)
  • valor absoluto mayor o igual que (estrategia 4)
  • valor absoluto mayor que (estrategia 5)

La forma menos propensa a errores de definir un conjunto relacionado de operadores de comparación es escribir primero la función de soporte de comparación de B-tree y luego escribir las otras funciones como envoltorios de una sola línea alrededor de la función de soporte. Esto reduce las probabilidades de obtener resultados inconsistentes para casos límite. Siguiendo este enfoque, primero escribimos:

#define Mag(c)  ((c)->x*(c)->x + (c)->y*(c)->y)

static int
complex_abs_cmp_internal(Complex *a, Complex *b)
{
    double      amag = Mag(a),
                bmag = Mag(b);

    if (amag < bmag)
        return -1;
    if (amag > bmag)
        return 1;
    return 0;
}

Ahora la función menor que se ve así:

PG_FUNCTION_INFO_V1(complex_abs_lt);

Datum
complex_abs_lt(PG_FUNCTION_ARGS)
{
    Complex    *a = (Complex *) PG_GETARG_POINTER(0);
    Complex    *b = (Complex *) PG_GETARG_POINTER(1);

    PG_RETURN_BOOL(complex_abs_cmp_internal(a, b) < 0);
}

Las otras cuatro funciones difieren solo en cómo comparan el resultado de la función interna con cero.

A continuación declaramos las funciones y los operadores basados en las funciones a SQL:

CREATE FUNCTION complex_abs_lt(complex, complex) RETURNS bool
    AS 'filename', 'complex_abs_lt'
    LANGUAGE C IMMUTABLE STRICT;

CREATE OPERATOR < (
   leftarg = complex, rightarg = complex, procedure = complex_abs_lt,
   commutator = > , negator = >= ,
   restrict = scalarltsel, join = scalarltjoinsel
);

Es importante especificar los operadores conmutador y negador correctos, así como las funciones de selectividad de restricción y de unión adecuadas, de lo contrario el optimizador no podrá hacer un uso eficaz del índice.

Hay otras cosas que vale la pena señalar que están sucediendo aquí:

  • Solo puede haber un operador llamado, por ejemplo, = y que tome el tipo complex para ambos operandos. En este caso no tenemos ningún otro operador = para complex, pero si estuviéramos construyendo un tipo de datos práctico, probablemente querríamos que = fuera la operación de igualdad ordinaria para números complejos (y no la igualdad de los valores absolutos). En ese caso, necesitaríamos usar algún otro nombre de operador para complex_abs_eq.

  • Aunque PostgreSQL puede lidiar con funciones que tienen el mismo nombre SQL siempre que tengan diferentes tipos de datos de argumentos, C solo puede lidiar con una función global que tenga un nombre dado. Por lo tanto, no deberíamos nombrar la función C con algo simple como abs_eq. Por lo general, es una buena práctica incluir el nombre del tipo de datos en el nombre de la función C, para no entrar en conflicto con funciones para otros tipos de datos.

  • Podríamos haber hecho que el nombre SQL de la función fuera abs_eq, confiando en que PostgreSQL la distinguiera por los tipos de datos de los argumentos de cualquier otra función SQL del mismo nombre. Para mantener el ejemplo simple, hacemos que la función tenga los mismos nombres a nivel de C y a nivel de SQL.

El siguiente paso es el registro de la rutina de soporte requerida por los B-trees. El código C de ejemplo que implementa esto está en el mismo archivo que contiene las funciones del operador. Así es como declaramos la función:

CREATE FUNCTION complex_abs_cmp(complex, complex)
    RETURNS integer
    AS 'filename'
    LANGUAGE C IMMUTABLE STRICT;

Ahora que tenemos los operadores y la rutina de soporte requeridos, finalmente podemos crear la clase de operadores:

CREATE OPERATOR CLASS complex_abs_ops
    DEFAULT FOR TYPE complex USING btree AS
        OPERATOR        1       < ,
        OPERATOR        2       <= ,
        OPERATOR        3       = ,
        OPERATOR        4       >= ,
        OPERATOR        5       > ,
        FUNCTION        1       complex_abs_cmp(complex, complex);

¡Y ya hemos terminado! Ahora debería ser posible crear y usar índices B-tree en columnas de tipo complex.

Podríamos haber escrito las entradas de los operadores de forma más detallada, como en:

        OPERATOR        1       < (complex, complex) ,

pero no es necesario hacerlo cuando los operadores toman el mismo tipo de datos para el que estamos definiendo la clase de operadores.

El ejemplo anterior asume que deseas hacer de esta nueva clase de operadores la clase de operadores B-tree por defecto para el tipo de datos complex. Si no es así, simplemente omite la palabra DEFAULT.

36.16.5. Clases de operadores y familias de operadores #

Hasta ahora hemos asumido implícitamente que una clase de operadores trata con un solo tipo de datos. Si bien ciertamente solo puede haber un tipo de datos en una columna de índice particular, a menudo es útil indexar operaciones que comparan una columna indexada con un valor de un tipo de datos diferente. Además, si hay uso para un operador de tipos de datos cruzados en conexión con una clase de operadores, a menudo ocurre que el otro tipo de datos tiene una clase de operadores relacionada propia. Es útil hacer explícitas las conexiones entre clases relacionadas, porque esto puede ayudar al planificador a optimizar las consultas SQL (particularmente para las clases de operadores B-tree, ya que el planificador contiene una gran cantidad de conocimiento sobre cómo trabajar con ellas).

Para manejar estas necesidades, PostgreSQL utiliza el concepto de una familia de operadores. Una familia de operadores contiene una o más clases de operadores, y también puede contener operadores indexables y las funciones de soporte correspondientes que pertenecen a la familia en su conjunto pero no a ninguna clase individual dentro de la familia. Decimos que tales operadores y funciones están sueltos dentro de la familia, a diferencia de estar vinculados a una clase específica. Típicamente, cada clase de operadores contiene operadores de un solo tipo de datos, mientras que los operadores de tipos de datos cruzados están sueltos en la familia.

Todos los operadores y funciones en una familia de operadores deben tener semánticas compatibles, donde los requisitos de compatibilidad son establecidos por el método de indexación. Por lo tanto, te preguntarás por qué molestarse en señalar subconjuntos particulares de la familia como clases de operadores; y de hecho, para muchos propósitos, las divisiones de clases son irrelevantes y la familia es la única agrupación interesante. La razón para definir clases de operadores es que especifican qué parte de la familia se necesita para soportar cualquier índice en particular. Si hay un índice que usa una clase de operadores, entonces esa clase de operadores no se puede eliminar sin eliminar el índice — pero otras partes de la familia de operadores, es decir, otras clases de operadores y operadores sueltos, podrían eliminarse. Por lo tanto, se debe especificar una clase de operadores para que contenga el conjunto mínimo de operadores y funciones que son razonablemente necesarios para trabajar con un índice en un tipo de datos específico, y luego los operadores relacionados pero no esenciales se pueden agregar como miembros sueltos de la familia de operadores.

Como ejemplo, PostgreSQL tiene una familia de operadores B-tree integrada llamada integer_ops, que incluye las clases de operadores int8_ops, int4_ops e int2_ops para índices en columnas de tipo bigint (int8), integer (int4) y smallint (int2) respectivamente. La familia también contiene operadores de comparación de tipos de datos cruzados que permiten comparar cualquiera de estos dos tipos, de modo que un índice en uno de estos tipos se pueda buscar utilizando un valor de comparación de otro tipo. La familia podría duplicarse mediante estas definiciones:

CREATE OPERATOR FAMILY integer_ops USING btree;

CREATE OPERATOR CLASS int8_ops
DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
  -- comparaciones int8 estándar
  OPERATOR 1 < ,
  OPERATOR 2 <= ,
  OPERATOR 3 = ,
  OPERATOR 4 >= ,
  OPERATOR 5 > ,
  FUNCTION 1 btint8cmp(int8, int8) ,
  FUNCTION 2 btint8sortsupport(internal) ,
  FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
  FUNCTION 4 btequalimage(oid) ,
  FUNCTION 6 btint8skipsupport(internal) ;

CREATE OPERATOR CLASS int4_ops
DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
  -- comparaciones int4 estándar
  OPERATOR 1 < ,
  OPERATOR 2 <= ,
  OPERATOR 3 = ,
  OPERATOR 4 >= ,
  OPERATOR 5 > ,
  FUNCTION 1 btint4cmp(int4, int4) ,
  FUNCTION 2 btint4sortsupport(internal) ,
  FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
  FUNCTION 4 btequalimage(oid) ,
  FUNCTION 6 btint4skipsupport(internal) ;

CREATE OPERATOR CLASS int2_ops
DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
  -- comparaciones int2 estándar
  OPERATOR 1 < ,
  OPERATOR 2 <= ,
  OPERATOR 3 = ,
  OPERATOR 4 >= ,
  OPERATOR 5 > ,
  FUNCTION 1 btint2cmp(int2, int2) ,
  FUNCTION 2 btint2sortsupport(internal) ,
  FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
  FUNCTION 4 btequalimage(oid) ,
  FUNCTION 6 btint2skipsupport(internal) ;

ALTER OPERATOR FAMILY integer_ops USING btree ADD
  -- comparaciones de tipos cruzados int8 vs int2
  OPERATOR 1 < (int8, int2) ,
  OPERATOR 2 <= (int8, int2) ,
  OPERATOR 3 = (int8, int2) ,
  OPERATOR 4 >= (int8, int2) ,
  OPERATOR 5 > (int8, int2) ,
  FUNCTION 1 btint82cmp(int8, int2) ,

  -- comparaciones de tipos cruzados int8 vs int4
  OPERATOR 1 < (int8, int4) ,
  OPERATOR 2 <= (int8, int4) ,
  OPERATOR 3 = (int8, int4) ,
  OPERATOR 4 >= (int8, int4) ,
  OPERATOR 5 > (int8, int4) ,
  FUNCTION 1 btint84cmp(int8, int4) ,

  -- comparaciones de tipos cruzados int4 vs int2
  OPERATOR 1 < (int4, int2) ,
  OPERATOR 2 <= (int4, int2) ,
  OPERATOR 3 = (int4, int2) ,
  OPERATOR 4 >= (int4, int2) ,
  OPERATOR 5 > (int4, int2) ,
  FUNCTION 1 btint42cmp(int4, int2) ,

  -- comparaciones de tipos cruzados int4 vs int8
  OPERATOR 1 < (int4, int8) ,
  OPERATOR 2 <= (int4, int8) ,
  OPERATOR 3 = (int4, int8) ,
  OPERATOR 4 >= (int4, int8) ,
  OPERATOR 5 > (int4, int8) ,
  FUNCTION 1 btint48cmp(int4, int8) ,

  -- comparaciones de tipos cruzados int2 vs int8
  OPERATOR 1 < (int2, int8) ,
  OPERATOR 2 <= (int2, int8) ,
  OPERATOR 3 = (int2, int8) ,
  OPERATOR 4 >= (int2, int8) ,
  OPERATOR 5 > (int2, int8) ,
  FUNCTION 1 btint28cmp(int2, int8) ,

  -- comparaciones de tipos cruzados int2 vs int4
  OPERATOR 1 < (int2, int4) ,
  OPERATOR 2 <= (int2, int4) ,
  OPERATOR 3 = (int2, int4) ,
  OPERATOR 4 >= (int2, int4) ,
  OPERATOR 5 > (int2, int4) ,
  FUNCTION 1 btint24cmp(int2, int4) ,

  -- funciones in_range de tipos cruzados
  FUNCTION 3 in_range(int4, int4, int8, boolean, boolean) ,
  FUNCTION 3 in_range(int4, int4, int2, boolean, boolean) ,
  FUNCTION 3 in_range(int2, int2, int8, boolean, boolean) ,
  FUNCTION 3 in_range(int2, int2, int4, boolean, boolean) ;

Ten en cuenta que esta definición sobrecarga los números de estrategia de operador y de función de soporte: cada número ocurre varias veces dentro de la familia. Esto está permitido siempre que cada instancia de un número particular tenga tipos de datos de entrada distintos. Las instancias que tienen ambos tipos de entrada iguales al tipo de entrada de una clase de operadores son los operadores y funciones de soporte primarios para esa clase de operadores, y en la mayoría de los casos deben declararse como parte de la clase de operadores en lugar de como miembros sueltos de la familia.

En una familia de operadores B-tree, todos los operadores de la familia deben ordenar de forma compatible, como se especifica en detalle en la sección Section 65.1.2. Para cada operator en la familia debe haber una función de soporte que tenga los mismos dos tipos de datos de entrada que el operador. Se recomienda que una familia sea completa, es decir, que para cada combinación de tipos de datos se incluyan todos los operadores. Cada clase de operadores debe incluir solo los operadores que no son de tipos cruzados y la función de soporte para su tipo de datos.

Para construir una familia de operadores hash de múltiples tipos de datos, se deben crear funciones de soporte hash compatibles para cada tipo de datos admitido por la familia. Aquí, compatibilidad significa que se garantiza que las funciones devolverán el mismo código hash para cualquier par de valores que se consideren iguales por los operadores de igualdad de la familia, incluso cuando los valores sean de diferentes tipos. Esto suele ser difícil de lograr cuando los tipos tienen representaciones físicas diferentes, pero se puede hacer en algunos casos. Además, la conversión de un valor de un tipo de datos representado en la familia de operadores a otro tipo de datos también representado en la familia de operadores mediante un cast de coerción implícito o binario no debe cambiar el valor hash calculado. Ten en cuenta que solo hay una función de soporte por tipo de datos, no una por operador de igualdad. Se recomienda que una familia esté completa, es decir, que proporcione un operador de igualdad para cada combinación de tipos de datos. Cada clase de operadores debe incluir solo el operador de igualdad que no es de tipos cruzados y la función de soporte para su tipo de datos.

Los índices GiST, SP-GiST y GIN no tienen ninguna noción explícita de operaciones de tipos de datos cruzados. El conjunto de operadores admitidos es simplemente lo que las funciones de soporte primarias para una clase de operadores dada pueden manejar.

En BRIN, los requisitos dependen del marco que proporciona las clases de operadores. Para las clases de operadores basadas en minmax, el comportamiento requerido es el mismo que para las familias de operadores B-tree: all the operators in the family must sort compatibly, and casts must todos los operadores de la familia deben ordenar de forma compatible, y los casts no deben cambiar el orden de clasificación asociado.

Note

Antes de PostgreSQL 8.3, no existía el concepto de familias de operadores, por lo que cualquier operador de tipos de datos cruzados que se quisiera utilizar con un índice tenía que vincularse directamente a la clase de operadores del índice. Si bien este enfoque sigue funcionando, está obsoleto porque hace que las dependencias de un índice sean demasiado amplias, y porque el planificador puede manejar las comparaciones de tipos de datos cruzados de manera más efectiva cuando ambos tipos de datos tienen operadores en la misma familia de operadores.

36.16.6. Dependencias del sistema respecto a las clases de operadores #

PostgreSQL utiliza clases de operadores para inferir las propiedades de los operadores en más formas que solo si se pueden usar con índices. Por lo tanto, es posible que desees crear clases de operadores incluso si no tienes intención de indexar ninguna columna de tu tipo de datos.

En particular, existen características de SQL como ORDER BY y DISTINCT que requieren la comparación y ordenación de valores. Para implementar estas características en un tipo de datos definido por el usuario, PostgreSQL busca la clase de operadores B-tree por defecto para el tipo de datos. El miembro igual de esta clase de operadores define la noción de igualdad de valores del sistema para GROUP BY y DISTINCT, y el orden de clasificación impuesto por la clase de operadores define el orden predeterminado de ORDER BY.

Si no hay una clase de operadores B-tree por defecto para un tipo de datos, el sistema buscará una clase de operadores hash por defecto. Pero dado que ese tipo de clase de operadores solo proporciona igualdad, solo es capaz de admitir la agrupación, no la ordenación.

Cuando no hay una clase de operadores por defecto para un tipo de datos, obtendrás errores como could not identify an ordering operator si intentas usar estas características de SQL con el tipo de datos.

Note

En las versiones de PostgreSQL anteriores a 7.4, las operaciones de ordenación y agrupación utilizaban implícitamente operadores llamados =, < y >. El nuevo comportamiento de confiar en las clases de operadores predeterminadas evita tener que hacer cualquier suposición sobre el comportamiento de los operadores con nombres particulares.

Ordenar mediante una clase de operadores B-tree no predeterminada es posible especificando el operador menor que de la clase en una opción USING, por ejemplo

SELECT * FROM mytable ORDER BY somecol USING ~<~;

Alternativamente, especificar el operador mayor que de la clase en USING selecciona una ordenación en sentido descendente.

La comparación de arrays de un tipo definido por el usuario también depende de la semántica definida por la clase de operadores B-tree por defecto del tipo. Si no hay una clase de operadores B-tree por defecto, pero hay una clase de operadores hash por defecto, entonces se admite la igualdad de arrays, pero no las comparaciones de ordenación.

Otra característica de SQL que requiere aún más conocimiento específico del tipo de datos es la opción de encuadre (framing option) RANGE offset PRECEDING/FOLLOWING para funciones de ventana (ver la sección Section 4.2.8). Para una consulta como

SELECT sum(x) OVER (ORDER BY x RANGE BETWEEN 5 PRECEDING AND 10 FOLLOWING)
  FROM mytable;

no es suficiente saber cómo ordenar por x; la base de datos también debe comprender cómo restar 5 o sumar 10 al valor de x de la fila actual para identificar los límites del marco de la ventana actual. Comparar los límites resultantes con los valores de x de otras filas es posible utilizando los operadores de comparación proporcionados por la clase de operadores B-tree que define el orden de ORDER BY — pero los operadores de suma y resta no forman parte de la clase de operadores, así que ¿cuáles deberían usarse? Vincular esa elección de forma fija sería indeseable, porque diferentes órdenes de clasificación (diferentes clases de operadores B-tree) podrían necesitar comportamientos diferentes. Por lo tanto, una clase de operadores B-tree puede especificar una función de soporte in_range que encapsule los comportamientos de suma y resta que tengan sentido para su orden de clasificación. Incluso puede proporcionar más de una función de soporte in_range, en caso de que haya más de un tipo de datos que tenga sentido usar como el desplazamiento en las cláusulas RANGE. Si la clase de operadores B-tree asociada con la cláusula ORDER BY de la ventana no tiene una función de soporte in_range coincidente, la opción RANGE offset PRECEDING/FOLLOWING no es compatible.

Otro punto importante es que un operador de igualdad que aparece en una familia de operadores hash es un candidato para uniones por hash, agregación por hash y optimizaciones relacionadas. La familia de operadores hash es esencial aquí ya que identifica las funciones hash a usar.

36.16.7. Operadores de ordenación #

Algunos métodos de acceso a índices (actualmente, solo GiST y SP-GiST) admiten el concepto de operadores de ordenación. Lo que hemos estado discutiendo hasta ahora son operadores de búsqueda (search operators). Un operador de búsqueda es aquel para el cual se puede buscar en el índice para encontrar todas las filas que satisfacen la condición WHERE indexed_column operator constant. Ten en cuenta que no se promete nada sobre el orden en el que se devolverán las filas coincidentes. Por el contrario, un operador de ordenación no restringe el conjunto de filas que se pueden devolver, sino que determina su orden. Un operador de ordenación es aquel para el cual se puede escanear el índice para devolver filas en el orden representado por ORDER BY indexed_column operator constant. La razón para definir los operadores de ordenación de esa manera es que admite búsquedas del vecino más cercano (nearest-neighbor), si el operador es uno que mide la distancia. Por ejemplo, una consulta como

SELECT * FROM places ORDER BY location <-> point '(101,456)' LIMIT 10;

encuentra los diez lugares más cercanos a un punto objetivo dado. Un índice GiST en la columna location puede hacer esto de manera eficiente porque <-> es un operador de ordenación.

Mientras que los operadores de búsqueda deben devolver resultados booleanos, los operadores de ordenación suelen devolver algún otro tipo, como float o numeric para las distancias. Este tipo normalmente no es el mismo que el tipo de datos que se está indexando. Para evitar suposiciones fijas sobre el comportamiento de diferentes tipos de datos, se requiere que la definición de un operador de ordenación nombre a una familia de operadores B-tree que especifique el orden de clasificación del tipo de datos resultante. Como se indicó en la sección anterior, las familias de operadores B-tree definen la noción de ordenación de PostgreSQL, por lo que esta es una representación natural. Dado que el operador de punto <-> devuelve el tipo float8, podría especificarse en un comando de creación de clase de operadores como este:

OPERATOR 15    <-> (point, point) FOR ORDER BY float_ops

donde float_ops es la familia de operadores integrada que incluye operaciones en float8. Esta declaración establece que el índice es capaz de devolver filas en orden de valores crecientes del operador <->.

36.16.8. Características especiales de las clases de operadores #

Hay dos características especiales de las clases de operadores que aún no hemos discutido, principalmente porque no son útiles con los métodos de indexación más utilizados.

Normalmente, declarar un operador como miembro de una clase de operadores (o familia) significa que el método de indexación puede recuperar exactamente el conjunto de filas que satisfacen una condición WHERE utilizando el operador. Por ejemplo:

SELECT * FROM table WHERE integer_column < 4;

puede ser satisfecho exactamente por un índice B-tree en la columna entera. Pero hay casos en los que un índice es útil como una guía inexacta para las filas coincidentes. Por ejemplo, si un índice GiST almacena solo cajas delimitadoras (bounding boxes) para objetos geométricos, entonces no puede satisfacer exactamente una condición WHERE que pruebe la superposición entre objetos no rectangulares como polígonos. Sin embargo, podríamos usar el índice para encontrar objetos cuyas cajas delimitadoras se superponen con la caja delimitadora del objeto objetivo, y luego hacer la prueba de superposición exacta solo en los objetos encontrados por el índice. Si se aplica este escenario, se dice que el índice es con pérdida (lossy) para el operador. Las búsquedas de índice con pérdida se implementan haciendo que el método de indexación devuelva una bandera de recomprobación (recheck) cuando una fila podría o no satisfacer realmente la condición de la consulta. El sistema central probará entonces la condición de la consulta original en la fila recuperada para ver si debe devolverse como una coincidencia válida. Este enfoque funciona si se garantiza que el índice devolverá todas las filas requeridas, más quizás algunas filas adicionales, que pueden eliminarse realizando la invocación del operador original. Los métodos de indexación que admiten búsquedas con pérdida (actualmente, GiST, SP-GiST y GIN) permiten que las funciones de soporte de las clases de operadores individuales establezcan la bandera de recomprobación, por lo que esta es esencialmente una característica de la clase de operadores.

Considera de nuevo la situación en la que estamos almacenando en el índice solo la caja delimitadora de un objeto complejo como un polígono. En este caso, no tiene mucho valor almacenar todo el polígono en la entrada del índice — es mejor almacenar solo un objeto más simple de tipo box. Esta situación se expresa mediante la opción STORAGE en CREATE OPERATOR CLASS: escribiríamos algo como:

CREATE OPERATOR CLASS polygon_ops
    DEFAULT FOR TYPE polygon USING gist AS
        ...
        STORAGE box;

En la actualidad, solo los métodos de indexación GiST, SP-GiST, GIN y BRIN admiten un tipo STORAGE que es diferente del tipo de datos de la columna. Las rutinas de soporte GiST compress y decompress deben encargarse de la conversión de tipos de datos cuando se utiliza STORAGE. SP-GiST requiere igualmente una función de soporte compress para convertir al tipo de almacenamiento, cuando este es diferente; si una clase de operadores SP-GiST también admite la recuperación de datos, la conversión inversa debe ser manejada por la función consistent. En GIN, el tipo STORAGE identifica el tipo de los valores de la clave (key), que normalmente es diferente del tipo de la columna indexada — por ejemplo, una clase de operadores para columnas de arrays de enteros podría tener claves que son simplemente enteros. Las rutinas de soporte GIN extractValue y extractQuery son responsables de extraer claves de los valores indexados. BRIN es similar a GIN: el tipo STORAGE identifica el tipo de los valores de resumen almacenados, y los procedimientos de soporte de las clases de operadores son responsables de interpretar correctamente los valores de resumen.