58.4. Planificación de consultas en adaptadores de datos externos #

Las funciones de retrollamada de FDW GetForeignRelSize, GetForeignPaths, GetForeignPlan, PlanForeignModify, GetForeignJoinPaths, GetForeignUpperPaths y PlanDirectModify deben encajar en el funcionamiento del planificador de PostgreSQL. A continuación se presentan algunas notas sobre lo que deben hacer.

La información en root y baserel se puede utilizar para reducir la cantidad de información que se tiene que recuperar de la tabla externa (y por lo tanto reducir el costo). baserel->baserestrictinfo es particularmente interesante, ya que contiene las condiciones de restricción (cláusulas WHERE) que deben utilizarse para filtrar las filas a recuperar. (El propio FDW no está obligado a imponer estas condiciones, ya que el ejecutor principal las puede comprobar en su lugar). baserel->reltarget->exprs se puede utilizar para determinar qué columnas deben recuperarse; pero ten en cuenta que solo enumera las columnas que deben ser emitidas por el nodo de plan ForeignScan, no las columnas que se utilizan en la evaluación de condiciones de restricción pero que no son de salida de la consulta.

Hay varios campos privados disponibles para que las funciones de planificación de FDW guarden información. Por lo general, cualquier cosa que guardes en los campos privados de FDW debe asignarse con palloc, de modo que se libere al final de la planificación.

baserel->fdw_private es un puntero void que está disponible para que las funciones de planificación de FDW guarden información relevante para la tabla externa en particular. El planificador principal no lo toca excepto para inicializarlo a NULL cuando se crea el nodo RelOptInfo. Es útil para pasar información de GetForeignRelSize a GetForeignPaths y/o de GetForeignPaths a GetForeignPlan, evitando así recalcularla.

GetForeignPaths puede identificar el significado de diferentes rutas de acceso guardando información privada en el campo fdw_private de los nodos ForeignPath. fdw_private se declara como un puntero List, pero en realidad podría contener cualquier cosa ya que el planificador principal no lo toca. Sin embargo, la mejor práctica es usar una representación que sea descriptiva mediante nodeToString, para su uso con el soporte de depuración disponible en el backend.

GetForeignPlan puede examinar el campo fdw_private del nodo ForeignPath seleccionado, y puede generar listas fdw_exprs y fdw_private para colocarlas en el nodo de plan ForeignScan, donde estarán disponibles en el momento de la ejecución. Ambas listas deben representarse en una forma que copyObject sepa cómo copiar. La lista fdw_private no tiene otras restricciones y no es interpretada por el backend principal de ninguna manera. Se espera que la lista fdw_exprs, si no es NIL, contenga árboles de expresiones que se pretenden ejecutar en tiempo de ejecución. Estos árboles se someterán a un procesamiento posterior por parte del planificador para hacerlos completamente ejecutables.

En GetForeignPlan, generalmente la lista de objetivos pasada se puede copiar en el nodo de plan tal cual. La lista scan_clauses pasada contiene las mismas cláusulas que baserel->baserestrictinfo, pero puede ser reordenada para una mejor eficiencia de ejecución. En casos simples, el FDW puede simplemente extraer los nodos RestrictInfo de la lista scan_clauses (usando extract_actual_clauses) y colocar todas las cláusulas en la lista qual del nodo de plan, lo que significa que todas las cláusulas serán comprobadas por el ejecutor en tiempo de ejecución. Los FDW más complejos pueden comprobar algunas de las cláusulas internamente, en cuyo caso esas cláusulas se pueden eliminar de la lista qual del nodo de plan para que el ejecutor no pierda tiempo volviendo a comprobarlas.

Como ejemplo, el FDW podría identificar algunas cláusulas de restricción de la forma variable_externa = sub_expresion, que determine que se pueden ejecutar en el servidor remoto dado el valor evaluado localmente de la sub_expresion. La identificación real de dicha cláusula debe ocurrir durante GetForeignPaths, ya que afectaría la estimación de costos de la ruta. El campo fdw_private de la ruta probablemente incluiría un puntero al nodo RestrictInfo de la cláusula identificada. Luego, GetForeignPlan eliminaría esa cláusula de scan_clauses, pero añadiría la sub_expresion a fdw_exprs para asegurar que se transforme en forma ejecutable. Probablemente también colocaría información de control en el campo fdw_private del nodo de plan para indicar a las funciones de ejecución qué hacer en tiempo de ejecución. La consulta transmitida al servidor remoto involucraría algo como WHERE variable_externa = $1, obteniéndose el valor del parámetro en tiempo de ejecución a partir de la evaluación del árbol de expresiones fdw_exprs.

Cualquier cláusula eliminada de la lista qual del nodo de plan debe, en su lugar, añadirse a fdw_recheck_quals o volver a comprobarse mediante RecheckForeignScan para garantizar el comportamiento correcto en el nivel de aislamiento READ COMMITTED. Cuando ocurre una actualización concurrente para alguna otra tabla involucrada en la consulta, el ejecutor puede necesitar verificar que todas las condiciones originales todavía se cumplan para la tupla, posiblemente contra un conjunto diferente de valores de parámetros. Usar fdw_recheck_quals suele ser más fácil que implementar comprobaciones dentro de RecheckForeignScan, pero este método será insuficiente cuando las uniones externas se hayan empujado hacia abajo, ya que las tuplas de unión en ese caso podrían hacer que algunos campos pasen a ser NULL sin rechazar la tupla por completo.

Otro campo de ForeignScan que pueden completar los FDW es fdw_scan_tlist, que describe las tuplas devueltas por el FDW para este nodo de plan. Para escaneos simples de tablas externas, este campo se puede establecer en NIL, lo que implica que las tuplas devueltas tienen el tipo de fila declarado para la tabla externa. Un valor no NIL debe ser una lista de objetivos (lista de TargetEntrys) que contenga variables Vars y/o expresiones que representen las columnas devueltas. Esto podría usarse, por ejemplo, para mostrar que el FDW ha omitido algunas columnas de las que se dio cuenta de que no serán necesarias para la consulta. Además, si el FDW puede calcular las expresiones utilizadas por la consulta de forma más barata de lo que se puede hacer localmente, podría añadir esas expresiones a fdw_scan_tlist. Ten en cuenta que los planes de unión (creados a partir de rutas realizadas por GetForeignJoinPaths) siempre deben suministrar fdw_scan_tlist para describir el conjunto de columnas que devolverán.

El FDW siempre debe construir al menos una ruta que dependa únicamente de las cláusulas de restricción de la tabla. En las consultas de unión, también podría optar por construir ruta(s) que dependan de las cláusulas de unión, por ejemplo, variable_externa = variable_local. Dichas cláusulas no se encontrarán en baserel->baserestrictinfo sino que deben buscarse en las listas de unión de la relación. Una ruta que utiliza dicha cláusula se denomina ruta parametrizada. Debe identificar las otras relaciones utilizadas en la(s) cláusula(s) de unión seleccionada(s) con un valor adecuado de param_info; usa get_baserel_parampathinfo para calcular ese valor. En GetForeignPlan, la parte variable_local de la cláusula de unión se añadiría a fdw_exprs, y luego, en tiempo de ejecución, el caso funciona igual que para una cláusula de restricción ordinaria.

Si un FDW admite uniones remotas, GetForeignJoinPaths debe producir ForeignPaths para posibles uniones remotas de manera muy similar a como funciona GetForeignPaths para las tablas base. La información sobre la unión prevista se puede pasar a GetForeignPlan de la misma manera descrita anteriormente. Sin embargo, baserestrictinfo no es relevante para las relaciones de unión; en su lugar, las cláusulas de unión pertinentes para una unión en particular se pasan a GetForeignJoinPaths como un parámetro separado (extra->restrictlist).

An FDW podría, además, admitir la ejecución directa de algunas acciones del plan que están por encima del nivel de escaneos y uniones, como la agrupación o la agregación. Para ofrecer tales opciones, el FDW debe generar rutas e insertarlas en la relación superior adecuada. Por ejemplo, una ruta que representa una agregación remota debe insertarse en la relación UPPERREL_GROUP_AGG, utilizando add_path. Esta ruta se comparará en función del costo con la agregación local realizada al leer una ruta de escaneo simple para la relación externa (ten en cuenta que dicha ruta también debe suministrarse, de lo contrario habrá un error en el tiempo del plan). Si la ruta de agregación remota gana, lo cual sucedería habitualmente, se convertirá en un plan de la manera habitual, llamando a GetForeignPlan. El lugar recomendado para generar tales rutas es en la función de retrollamada GetForeignUpperPaths, que se llama para cada relación superior (es decir, cada paso de procesamiento posterior al escaneo/unión), si todas las relaciones base de la consulta provienen del mismo FDW.

PlanForeignModify y las demás retrollamadas descritas en la Section 58.2.4 se diseñaron en torno al supuesto de que la relación externa se escaneará de la manera habitual y luego las actualizaciones de filas individuales serán impulsadas por un nodo de plan ModifyTable local. Este enfoque es necesario para el caso general en el que una actualización requiere leer tanto tablas locales como externas. Sin embargo, si la operación se pudiera ejecutar por completo en el servidor remoto, el FDW podría generar una ruta que la represente e insertarla en la relación superior UPPERREL_FINAL, donde competiría contra el enfoque de ModifyTable. Este enfoque también podría usarse para implementar SELECT FOR UPDATE remotos, en lugar de usar las retrollamadas de bloqueo de filas descritas en la Section 58.2.6. Ten en cuenta que una ruta insertada en UPPERREL_FINAL es responsable de implementar todo el comportamiento de la consulta.

Al planificar un UPDATE o un DELETE, PlanForeignModify y PlanDirectModify pueden buscar la estructura RelOptInfo para la tabla externa y hacer uso de los datos de baserel->fdw_private creados previamente por las funciones de planificación de escaneo. Sin embargo, en INSERT la tabla de destino no se escanea, por lo que no hay un RelOptInfo para ella. La lista List devuelta por PlanForeignModify tiene las mismas restricciones que la lista fdw_private de un nodo de plan ForeignScan, es decir, debe contener solo estructuras que copyObject sepa cómo copiar.

INSERT con una cláusula ON CONFLICT no admite especificar el objetivo del conflicto, ya que las restricciones únicas o las restricciones de exclusión en tablas remotas no se conocen localmente. Esto a su vez implica que no se admite ON CONFLICT DO UPDATE, ya que la especificación es obligatoria allí.