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.
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.
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ón | Número de estrategia |
|---|---|
| menor que | 1 |
| menor o igual que | 2 |
| igual | 3 |
| mayor o igual que | 4 |
| mayor que | 5 |
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ón | Número de estrategia |
|---|---|
| igual | 1 |
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ón | Número de estrategia |
|---|---|
| estrictamente a la izquierda de | 1 |
| no se extiende a la derecha de | 2 |
| se superpone | 3 |
| no se extiende a la izquierda de | 4 |
| estrictamente a la derecha de | 5 |
| igual | 6 |
| contiene | 7 |
| contenido por | 8 |
| no se extiende por encima de | 9 |
| estrictamente por debajo de | 10 |
| estrictamente por encima de | 11 |
| no se extiende por debajo de | 12 |
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ón | Número de estrategia |
|---|---|
| estrictamente a la izquierda de | 1 |
| estrictamente a la derecha de | 5 |
| igual | 6 |
| contenido por | 8 |
| estrictamente por debajo de | 10 |
| estrictamente por encima de | 11 |
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ón | Número de estrategia |
|---|---|
| se superpone | 1 |
| contiene | 2 |
| está contenido por | 3 |
| igual | 4 |
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ón | Número de estrategia |
|---|---|
| menor que | 1 |
| menor o igual que | 2 |
| igual | 3 |
| mayor o igual que | 4 |
| mayor que | 5 |
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).
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ón | Nú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ón | Número de soporte |
|---|---|
| Calcular el valor hash de 32 bits para una clave | 1 |
| 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ón | Descripción | Número de soporte |
|---|---|---|
consistent | determinar si la clave satisface la condición de la consulta | 1 |
union | calcular la unión de un conjunto de claves | 2 |
compress | calcular una representación comprimida de una clave o valor a indexar (opcional) | 3 |
decompress | calcular una representación descomprimida de una clave comprimida (opcional) | 4 |
penalty | calcular la penalización por insertar una nueva clave en el subárbol con la clave del subárbol dado | 5 |
picksplit | determinar 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 resultantes | 6 |
same | comparar dos claves y devolver true si son iguales | 7 |
distance | determinar la distancia desde la clave al valor de la consulta (opcional) | 8 |
fetch | calcular la representación original de una clave comprimida para escaneos de solo índice (index-only scans) (opcional) | 9 |
options | definir opciones que son específicas de esta clase de operadores (opcional) | 10 |
sortsupport | proporcionar un comparador de ordenación para ser utilizado en construcciones rápidas de índices (opcional) | 11 |
translate_cmptype | traducir 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ón | Descripción | Número de soporte |
|---|---|---|
config | proporcionar información básica sobre la clase de operadores | 1 |
choose | determinar cómo insertar un nuevo valor en una tupla interna | 2 |
picksplit | determinar cómo particionar un conjunto de valores | 3 |
inner_consistent | determinar qué subparticiones deben buscarse para una consulta | 4 |
leaf_consistent | determinar si la clave satisface la condición de la consulta | 5 |
options | definir 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ón | Descripción | Nú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 |
extractValue | extraer claves de un valor a indexar | 2 |
extractQuery | extraer claves de una condición de consulta | 3 |
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ón | Descripción | Número de soporte |
|---|---|---|
opcInfo | devolver información interna que describe los datos de resumen de las columnas indexadas | 1 |
add_value | añadir un nuevo valor a una tupla de índice de resumen existente | 2 |
consistent | determinar si el valor coincide con la condición de la consulta | 3 |
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.
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:
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.
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.
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.
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.
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.
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 <->.
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.