La tarea del planificador/optimizador (planner/optimizer) es crear un plan de ejecución óptimo. Una consulta SQL dada (y, por lo tanto, un árbol de consulta) se puede ejecutar en realidad en una amplia variedad de formas diferentes, cada una de las cuales producirá el mismo conjunto de resultados. Si es computacionalmente factible, el optimizador de consultas examinará cada uno de estos posibles planes de ejecución, seleccionando finalmente el plan de ejecución que se espera que se ejecute más rápido.
En algunas situaciones, examinar cada forma posible en que se puede ejecutar una consulta requeriría una cantidad excesiva de tiempo y memoria. En particular, esto ocurre al ejecutar consultas que involucran un gran número de operaciones de unión. Con el fin de determinar un plan de consulta razonable (not necesariamente óptimo) en una cantidad de tiempo razonable, PostgreSQL utiliza un optimizador genético de consultas (Genetic Query Optimizer) (ver Chapter 61) cuando el número de uniones supera un umbral (ver geqo_threshold).
El procedimiento de búsqueda del planificador funciona en realidad con estructuras de datos llamadas rutas (paths), que son simplemente representaciones recortadas de los planes que contienen únicamente la información que el planificador necesita para tomar sus decisiones. Una vez determinada la ruta más barata, se construye un árbol de plan (plan tree) completo para pasarlo al ejecutor. Esto representa el plan de ejecución deseado con suficiente detalle para que el ejecutor lo ejecute. En el resto de esta sección ignoraremos la distinción entre rutas y planes.
El planificador/optimizador comienza generando planes para escanear cada relación
individual (tabla) utilizada en la consulta. Los planes posibles están determinados
por los índices disponibles en cada relación. Siempre existe la posibilidad de
realizar un escaneo secuencial en una relación, por lo que siempre se crea un plan
de escaneo secuencial. Supón que se define un índice en una relación (por ejemplo,
un índice B-tree) y una consulta contiene la restricción
relation.attribute OPR constant. Si
relation.attribute coincide con la clave del índice B-tree
y OPR es uno de los operadores enumerados en la
clase de operador del índice, se crea otro plan utilizando
el índice B-tree para escanear la relación. Si hay más índices presentes y las
restricciones de la consulta coinciden con la clave de un índice, se considerarán
otros planes. Los planes de escaneo de índices también se generan para los índices
que tienen un orden que puede coincidir con la cláusula ORDER BY
de la consulta (si la hubiera), o un orden que podría ser útil para la unión por
mezcla (merge join) (ver más abajo).
Si la consulta requiere unir dos o más relaciones, los planes para unir relaciones se consideran después de que se hayan encontrado todos los planes factibles para escanear relaciones individuales. Las tres estrategias de unión disponibles son:
nested loop join (unión de bucle anidado): la relación derecha se escanea una vez por cada fila encontrada en la relación izquierda. Esta estrategia es fácil de implementar pero puede requerir mucho tiempo. (Sin embargo, si la relación derecha se puede escanear con un escaneo de índice, esta puede ser una buena estrategia. Es posible utilizar valores de la fila actual de la relación izquierda como claves para el escaneo de índice de la derecha).
merge join (unión por mezcla): cada relación se ordena según los atributos de la unión antes de que comience la unión. Luego, las dos relaciones se escanean en paralelo y las filas coincidentes se combinan para formar las filas de la unión. Este tipo de unión es atractivo porque cada relación tiene que ser escaneada una sola vez. El ordenamiento requerido se puede lograr mediante un paso de ordenamiento explícito o escaneando la relación en el orden adecuado utilizando un índice en la clave de la unión.
hash join (unión por hash): la relación derecha se escanea primero y se carga en una tabla hash, utilizando sus atributos de unión como claves hash. A continuación, se escanea la relación izquierda y los valores apropiados de cada fila encontrada se utilizan como claves hash para localizar las filas coincidentes en la tabla.
Cuando la consulta involucra más de dos relaciones, el resultado final debe construirse mediante un árbol de pasos de unión, cada uno con dos entradas. El planificador examina diferentes secuencias de unión posibles para encontrar la más barata.
Si la consulta utiliza menos de geqo_threshold relaciones, se
realiza una búsqueda casi exhaustiva para encontrar la mejor secuencia de unión. El
planificador considera preferentemente las uniones entre cualquier par de relaciones para
las cuales exista una cláusula de unión correspondiente en la condición WHERE
(es decir, para las cuales exista una restricción como where rel1.attr1=rel2.attr2).
Los pares de unión sin cláusula de unión se consideran únicamente cuando no hay otra
opción, es decir, cuando una relación particular no tiene cláusulas de unión disponibles
para ninguna otra relación. Se generan todos los planes posibles para cada par de unión
considerado por el planificador, y se elige el que se (estima que es) el más barato.
Cuando se supera geqo_threshold, las secuencias de unión
consideradas se determinan mediante heurística, como se describe en el Chapter 61. De lo contrario, el proceso es el mismo.
El árbol de plan finalizado consiste en escaneos secuenciales o de índices de las
relaciones base, más nodos de unión nested-loop, merge o hash según sea necesario,
más cualquier paso auxiliar necesario, como nodos de ordenamiento o nodos de cálculo
de funciones de agregación. La mayoría de estos tipos de nodos de plan tienen la
capacidad adicional de realizar selección (descartar las filas
que no cumplen una condición booleana especificada) y proyección
(cálculo de un conjunto de columnas derivadas basadas en valores de columna dados,
es decir, evaluación de expresiones escalares donde sea necesario). Una de las
responsabilidades del planificador es adjuntar las condiciones de selección de la
cláusula WHERE y el cálculo de las expresiones de salida requeridas
a los nodos más apropiados del árbol de plan.