36.15. Información de optimización de operadores #

36.15.1. COMMUTATOR
36.15.2. NEGATOR
36.15.3. RESTRICT
36.15.4. JOIN
36.15.5. HASHES
36.15.6. MERGES

La definición de un operador de PostgreSQL puede incluir varias cláusulas opcionales que le indican al sistema cosas útiles sobre cómo se comporta el operador. Estas cláusulas deben proporcionarse siempre que sea apropiado, porque pueden lograr aceleraciones considerables en la ejecución de las consultas que utilizan el operador. ¡Pero si las proporcionas, debes estar seguro de que son correctas! El uso incorrecto de una cláusula de optimización puede dar como resultado consultas lentas, salidas sutilmente erróneas u otras cosas malas (Bad Things). Siempre puedes omitir una cláusula de optimización si no estás seguro de ella; la única consecuencia es que las consultas podrían ejecutarse más lentamente de lo necesario.

Se podrían agregar cláusulas de optimización adicionales en futuras versiones de PostgreSQL. Las que se describen aquí son todas las que la versión 18.4 entiende.

También es posible asociar una función de soporte del planificador (planner support function) a la función subyacente de un operador, lo que proporciona otra forma de informarle al sistema sobre el comportamiento del operador. Consulta la sección Section 36.11 para obtener más información.

36.15.1. COMMUTATOR #

La cláusula COMMUTATOR, si se proporciona, nombra a un operador que es el conmutador del operador que se está definiendo. Decimos que el operador A es el conmutador del operador B si (x A y) es igual a (y B x) para todos los posibles valores de entrada x, y. Ten en cuenta que B también es el conmutador de A. Por ejemplo, los operadores < y > para un tipo de datos particular suelen ser conmutadores el uno del otro, y el operador + suele ser conmutativo consigo mismo. Pero el operador - no suele ser conmutativo con nada.

El tipo del operando izquierdo de un operador conmutable es el mismo que el tipo del operando derecho de su conmutador, y viceversa. Por lo tanto, el nombre de el operador conmutador es todo lo que PostgreSQL necesita recibir para buscar el conmutador, y eso es todo lo que necesita ser proporcionado en la cláusula COMMUTATOR.

Es fundamental proporcionar información del conmutador para los operadores que se utilizarán en índices y cláusulas de unión (join), porque esto permite al optimizador de consultas dar la vuelta a dicha cláusula a las formas necesarias para diferentes tipos de planes. Por ejemplo, considera una consulta con una cláusula WHERE como tab1.x = tab2.y, donde tab1.x y tab2.y son de un tipo definido por el usuario, y supón que tab2.y está indexado. El optimizador no puede generar un escaneo de índice a menos que pueda determinar cómo dar la vuelta a la cláusula a tab2.y = tab1.x, porque el mecanismo de escaneo de índice espera ver la columna indexada a la izquierda del operador que recibe. PostgreSQL no asumirá simplemente que esta es una transformación válida — el creador del operador = debe especificar que es válida, marcando el operador con la información del conmutador.

36.15.2. NEGATOR #

La cláusula NEGATOR, si se proporciona, nombra a un operador que es el negador del operador que se está definiendo. Decimos que el operador A es el negador del operador B si ambos devuelven resultados booleanos y (x A y) es igual a NOT (x B y) para todas las entradas posibles x, y. Ten en cuenta que B también es el negador de A. Por ejemplo, < y >= son un par negador para la mayoría de los tipos de datos. Un operador nunca puede ser válidamente su propio negador.

A diferencia de los conmutadores, un par de operadores unarios podría marcarse válidamente como negadores el uno del otro; eso significaría que (A x) es igual a NOT (B x) para todo x.

El negador de un operador debe tener los mismos tipos de operando izquierdo y/o derecho que el operador a definir, por lo que, al igual que con COMMUTATOR, solo se necesita dar el nombre del operador en la cláusula NEGATOR.

Proporcionar un negador es muy útil para el optimizador de consultas, ya que permite simplificar expresiones como NOT (x = y) en x <> y. Esto ocurre con más frecuencia de lo que se podría pensar, porque las operaciones NOT pueden insertarse como consecuencia de otros reordenamientos.

36.15.3. RESTRICT #

La cláusula RESTRICT, si se proporciona, nombra a una función de estimación de selectividad de restricción para el operador. (Ten en cuenta que este es el nombre de una función, no el nombre de un operador). Las cláusulas RESTRICT solo tienen sentido para operadores binarios que devuelven boolean. La idea detrás de un estimador de selectividad de restricción es adivinar qué fracción de las filas de una tabla satisfará una condición de la cláusula WHERE de la forma:

column OP constant

para el operador actual y un valor constante particular. Esto ayuda al optimizador al darle una idea de cuántas filas serán eliminadas por las cláusulas WHERE que tienen esta forma. (¿Qué pasa si la constante está a la izquierda?, te estarás preguntando. Bueno, esa es una de las cosas para las que sirve COMMUTATOR...)

Escribir nuevas funciones de estimación de selectividad de restricción está muy fuera del alcance de este capítulo, pero afortunadamente por lo general puedes usar uno de los estimadores estándar del sistema para muchos de tus propios operadores. Estos son los estimadores de restricción estándar:

eqsel para =
neqsel para <>
scalarltsel para <
scalarlesel para <=
scalargtsel para >
scalargesel para >=

Con frecuencia puedes salirte con la tuya usando eqsel o neqsel para operadores que tienen una selectividad muy alta o muy baja, incluso si no son realmente de igualdad o desigualdad. Por ejemplo, los operadores geométricos de igualdad aproximada usan eqsel bajo la suposición de que por lo general solo coincidirán con una pequeña fracción de las entradas de una tabla.

Puedes utilizar scalarltsel, scalarlesel, scalargtsel y scalargesel para comparaciones en tipos de datos que tengan algún medio sensato de ser convertidos en escalares numéricos para comparaciones de rango. Si es posible, añade el tipo de datos a los entendidos por la función convert_to_scalar() en src/backend/utils/adt/selfuncs.c. (Eventualmente, esta función debería ser reemplazada por funciones por tipo de datos identificadas a través de una columna del catálogo del sistema pg_type; pero eso aún no ha sucedido). Si no haces esto, las cosas seguirán funcionando, pero las estimaciones del optimizador no serán tan buenas como podrían ser.

Otra función de estimación de selectividad incorporada útil es matchingsel, que funcionará para casi cualquier operador binario, si se recopilan estadísticas estándar de MCV y/o histograma para los tipos de datos de entrada. Su estimación por defecto se establece en el doble de la estimación por defecto utilizada en eqsel, lo que la hace más adecuada para operadores de comparación que son algo menos estrictos que la igualdad. (O podrías llamar a la función subyacente generic_restriction_selectivity, proporcionando una estimación por defecto diferente).

Hay funciones de estimación de selectividad adicionales diseñadas para operadores geométricos en src/backend/utils/adt/geo_selfuncs.c: areasel, positionsel, y contsel. En el momento de escribir esto, son solo esqueletos (stubs), pero de todos modos es posible que desees utilizarlas (o mejor aún, mejorarlas).

36.15.4. JOIN #

La cláusula JOIN, si se proporciona, nombra a una función de estimación de selectividad de unión (join) para el operador. (Ten en cuenta que este es el nombre de una función, no el nombre de un operador). Las cláusulas JOIN solo tienen sentido para operadores binarios que devuelven boolean. La idea detrás de un estimador de selectividad de unión es adivinar qué fracción de las filas en un par de tablas satisfará una condición de la cláusula WHERE de la forma:

table1.column1 OP table2.column2

para el operador actual. Al igual que con la cláusula RESTRICT, esto ayuda al optimizador de manera muy sustancial al permitirle averiguar cuál de las diversas secuencias de unión posibles es probable que requiera el menor trabajo.

Como antes, este capítulo no intentará explicar cómo escribir una función de estimación de selectividad de unión, sino que simplemente sugerirá que utilices uno de los estimadores estándar si alguno es aplicable:

eqjoinsel para =
neqjoinsel para <>
scalarltjoinsel para <
scalarlejoinsel para <=
scalargtjoinsel para >
scalargejoinsel para >=
matchingjoinsel para operadores de coincidencia genéricos
areajoinsel para comparaciones basadas en áreas en 2D
positionjoinsel para comparaciones basadas en la posición en 2D
contjoinsel para comparaciones basadas en la contención en 2D

36.15.5. HASHES #

La cláusula HASHES, si está presente, le indica al sistema que es permisible usar el método de unión por hash (hash join) para una unión basada en este operador. HASHES solo tiene sentido para un operador binario que devuelve boolean, y en la práctica el operador debe representar la igualdad para algún tipo de datos o par de tipos de datos.

La suposición subyacente a la unión por hash es que el operador de unión solo puede devolver true para pares de valores izquierdos y derechos que se asocian al mismo código hash. Si dos valores se colocan en diferentes buckets de hash, la unión nunca los comparará en absoluto, asumiendo implícitamente que el resultado del operador de unión debe ser false. Por lo tanto, nunca tiene sentido especificar HASHES para operadores que no representan alguna forma de igualdad. En la mayoría de los casos, solo es práctico admitir el hashing para operadores que toman el mismo tipo de datos en ambos lados. Sin embargo, a veces es posible diseñar funciones hash compatibles para dos o más tipos de datos; es decir, funciones que generarán los mismos códigos hash para valores iguales, a pesar de que los valores tengan diferentes representaciones. Por ejemplo, es bastante simple organizar esta propiedad al aplicar hash a enteros de diferentes anchos.

Para marcarse como HASHES, el operador de unión debe aparecer en una familia de operadores de índice hash. Esto no se impone cuando creas el operador, ya que, por supuesto, la familia de operadores de referencia no podría existir todavía. Pero los intentos de usar el operador en uniones por hash fallarán en tiempo de ejecución si no existe tal familia de operadores. El sistema necesita la familia de operadores para encontrar las funciones hash específicas del tipo de datos para los tipos de datos de entrada del operador. Por supuesto, también debes crear funciones hash adecuadas antes de poder crear la familia de operadores.

Se debe tener cuidado al preparar una función hash, porque hay formas dependientes de la máquina en las que podría no hacer lo correcto. Por ejemplo, si tu tipo de datos es una estructura en la que podría haber bits de relleno (pad bits) sin interés, no puedes simplemente pasar la estructura completa a hash_any. (A menos que escribas tus otros operadores y funciones para asegurar que los bits no utilizados sean siempre cero, que es la estrategia recomendada). Otro ejemplo es que en las máquinas que cumplen con el estándar de coma flotante IEEE, el cero negativo y el cero positivo son valores diferentes (diferentes patrones de bits) pero están definidos para compararse como iguales. Si un valor float pudiera contener cero negativo, se necesitan pasos adicionales para asegurar que genere el mismo valor hash que el cero positivo.

Un operador utilizable en uniones por hash debe tener un conmutador (él mismo si los dos tipos de datos de los operandos son iguales, o un operador de igualdad relacionado si son diferentes) que aparezca en la misma familia de operadores. Si este no es el caso, podrían ocurrir errores del planificador cuando se utiliza el operador. Además, es una buena idea (pero no estrictamente necesario) que una familia de operadores hash que admita múltiples tipos de datos proporcione operadores de igualdad para cada combinación de los tipos de datos; esto permite una mejor optimización.

Note

La función subyacente a un operador utilizable en uniones por hash debe estar marcada como immutable o stable. Si es volatile, el sistema nunca intentará usar el operador para una unión por hash.

Note

Si un operador utilizable en uniones por hash tiene una función subyacente que está marcada como strict, la función también debe ser completa: es decir, debe devolver true o false, nunca null, para cualquier par de entradas no nulas. Si no se sigue esta regla, la optimización por hash de las operaciones IN podría generar resultados incorrectos. (Específicamente, IN podría devolver false donde la respuesta correcta según el estándar sería null; o podría producir un error quejándose de que no estaba preparado para un resultado nulo).

36.15.6. MERGES #

La cláusula MERGES, si está presente, le indica al sistema que es permisible usar el método de unión por mezcla (merge join) para una unión basada en este operador. MERGES solo tiene sentido para un operador binario que devuelve boolean, y en la práctica el operador debe representar la igualdad para algún tipo de datos o par de tipos de datos.

La unión por mezcla se basa en la idea de ordenar las tablas izquierda y derecha y luego escanearlas en paralelo. Por lo tanto, ambos tipos de datos deben ser capaces de ordenarse por completo, y el operador de unión debe ser uno que solo pueda tener éxito para pares de valores que caen en el mismo lugar en el orden de clasificación. En la práctica, esto significa que el operador de unión debe comportarse como la igualdad. Pero es posible unir por mezcla dos tipos de datos distintos siempre que sean lógicamente compatibles. Por ejemplo, el operador de igualdad de smallint contra integer es utilizable en uniones por mezcla. Solo necesitamos operadores de ordenación que lleven ambos tipos de datos a una secuencia lógicamente compatible.

Para marcarse como MERGES, el operador de unión debe aparecer como un miembro de igualdad de una familia de operadores de índice btree. Esto no se impone cuando creas el operador, ya que, por supuesto, la familia de operadores de referencia no podría existir todavía. Pero el operador no se utilizará realmente para uniones por mezcla a menos que se pueda encontrar una familia de operadores coincidente. El flag MERGES actúa así como una sugerencia al planificador de que vale la pena buscar una familia de operadores coincidente.

Un operador utilizable en uniones por mezcla debe tener un conmutador (él mismo si los dos tipos de datos de los operandos son el mismo, o un operador de igualdad relacionado si son diferentes) que aparezca en la misma familia de operadores. Si este no es el caso, podrían ocurrir errores del planificador cuando se utiliza el operador. Además, es una buena idea (pero no estrictamente necesario) que una familia de operadores btree que admita múltiples tipos de datos proporcione operadores de igualdad para cada combinación de los tipos de datos; esto permite una mejor optimización.

Note

La función subyacente a un operador utilizable en uniones por mezcla debe estar marcada como immutable o stable. Si es volatile, el sistema nunca intentará usar el operador para una unión por mezcla.