53.4. Планирование запросов с обёртками сторонних данных
Процедуры в FDW, реализующие функции GetForeignRelSize, GetForeignPaths, GetForeignPlan, PlanForeignModify, GetForeignJoinPaths, GetForeignUpperPaths и PlanDirectModify, должны вписываться в работу планировщика Postgres Pro. Здесь даётся несколько замечаний о том, как это должно происходить.
Для уменьшения объёма выбираемых из сторонней таблицы данных (и как следствие, сокращения стоимости) может использоваться информация, поступающая в root и baserel. Особый интерес представляет поле baserel->baserestrictinfo, так как оно содержит ограничивающие условия (предложение WHERE), по которым можно отфильтровать выбираемые строки. (Сама FDW не обязательно должна применять эти ограничения, так как их может проверить и ядро исполнителя.) Список baserel->reltarget->exprs позволяет определить, какие именно столбцы требуется выбрать; но учтите, что в нём перечисляются только те столбцы, которые выдаются узлом плана ForeignScan, но не столбцы, которые задействованы в ограничивающих условиях и при этом не выводятся запросом.
Когда функциям планирования FDW требуется сохранять свою информацию, они могут использовать различные частные поля. Вообще, все структуры, которые FDW помещает в закрытые поля, должны выделяться функцией palloc, чтобы они автоматически освобождались при завершении планирования.
Для хранения информации, относящейся к определённой сторонней таблице, функции планирования FDW могут использовать поле baserel->fdw_private, которое может содержать указатель на void. Ядро планировщика никак не касается его, кроме того, что записывает в него NULL при создании узла RelOptInfo. Оно полезно для передачи информации из GetForeignRelSize в GetForeignPaths и/или из GetForeignPaths в GetForeignPlan и позволяет избежать повторных вычислений.
GetForeignPaths может обозначить свойства различных путей доступа, сохранив частную информацию в поле fdw_private узлов ForeignPath. Это поле fdw_private объявлено как указатель на список (List), но в принципе может содержать всё, что угодно, так как ядро планировщика его не касается. Однако лучше поместить в него данные, которые сможет представить функция nodeToString, для применения средств отладки, имеющихся на сервере.
GetForeignPlan может изучить поле fdw_private выбранного узла ForeignPath и сформировать списки fdw_exprs и fdw_private, которые будут помещены в узел ForeignScan, где они будут находиться во время выполнения запроса. Оба эти списка должны быть представлены в форме, которую способна копировать функция copyObject. Список fdw_private не имеет других ограничений и никаким образом не интерпретируется ядром сервера. Список fdw_exprs, если этот указатель не NULL, предположительно содержит деревья выражений, которые должны быть вычислены при выполнении запроса. Затем планировщик обрабатывает эти деревья, чтобы они были полностью готовы к выполнению.
GetForeignPlan обычно может скопировать полученный целевой список в узел плана как есть. Передаваемый список scan_clauses содержит те же предложения, что и baserel->baserestrictinfo, но, возможно, в другом порядке для более эффективного выполнения. В простых случаях FDW может просто убрать узлы RestrictInfo из списка scan_clauses (используя функцию extract_actual_clauses) и поместить все предложения в список ограничений узла плана, что будет означать, что эти предложения будут проверяться исполнителем во время выполнения. Более сложные FDW могут самостоятельно проверять некоторые предложения, и в этом случае такие предложения можно удалить из списка ограничений узла, чтобы исполнитель не тратил время на их перепроверку.
Например, FDW может распознавать некоторые предложения ограничений вида сторонняя_переменная = подвыражение, которые, по её представлению, могут выполняться на удалённом сервере с локально вычисленным значением подвыражения. Собственно выявление такого предложения должно происходить в функции GetForeignPaths, так как это влияет на оценку стоимости пути. Эта функция может включить в поле fdw_private конкретного пути указатель на узел RestrictInfo этого предложения. Затем GetForeignPlan удалит это предложение из scan_clauses, но добавит подвыражение в fdw_exprs, чтобы оно было приведено к исполняемой форме. Она также может поместить управляющую информацию в поле fdw_private плана узла, которая скажет исполняющим функциям, что делать во время выполнения. Запрос, передаваемый удалённому серверу, будет содержать что-то вроде WHERE , а значение параметра будет получено во время выполнения в результате вычисления дерева выражения сторонняя_переменная = $1fdw_exprs.
Все предложения, удаляемые из списка условий узла плана, должны быть добавлены в fdw_recheck_quals или перепроверены функцией RecheckForeignScan для обеспечения корректного поведения на уровне изоляции READ COMMITTED. Когда имеет место параллельное изменение в некоторой другой таблице, задействованной в запросе, исполнителю может потребоваться убедиться в том, что все исходные условия по-прежнему выполняются для кортежа, возможно, с другим набором значений параметров. Использовать fdw_recheck_quals обычно проще, чем реализовывать проверки внутри RecheckForeignScan, но этот метод недостаточен, когда внешние соединения выносятся наружу, так как вследствие перепроверки в соединённых кортежах могут обнуляться некоторые поля, но сами кортежи не будут исключаться.
Ещё одно поле ForeignScan, которое могут заполнять FDW, это fdw_scan_tlist, описывающее кортежи, возвращаемые обёрткой для этого узла плана. Для простых сторонних таблиц в него можно записать NIL, из чего будет следовать, что возвращённые кортежи имеют тип, объявленный для сторонней таблицы. Отличное от NIL значение должно указывать на список целевых элементов (список структур TargetEntry), содержащий переменные и/или выражения, представляющие возвращаемые столбцы. Это можно использовать, например, чтобы показать, что FDW опустила некоторые столбцы, которые по её наблюдению не нужны для запроса. Также, если FDW может вычислить выражения, используемые в запросе, более эффективно, чем это можно сделать локально, она должна добавить эти выражения в список fdw_scan_tlist. Заметьте, что планы соединения (полученные из путей, созданных функцией GetForeignJoinPaths) должны всегда заполнять fdw_scan_tlist, описывая набор столбцов, которые они будут возвращать.
FDW должна всегда строить минимум один путь, зависящий только от предложений ограничения таблицы. В запросах с соединением она может также построить пути, зависящие от ограничения соединения, например сторонняя_переменная = локальная_переменная. Такие предложения будут отсутствовать в baserel->baserestrictinfo; их нужно искать в списках соединений отношений. Путь, построенный с таким предложением, называется «параметризованным». Другие отношения, задействованные в выбранном предложении соединения, должны связываться c этим путём соответствующим значением param_info; для получения этого значения используется get_baserel_parampathinfo. В GetForeignPlan часть локальная_переменная предложения соединения будет добавлена в fdw_exprs, и затем, во время выполнения, это будет работать так же, как и обычное предложение ограничения.
Если FDW поддерживает удалённые соединения, GetForeignJoinPaths должна выдавать пути ForeignPath для потенциально удалённых соединений почти так же, как это делает GetForeignPaths для базовых таблиц. Информация о выбранном соединении может быть передана функции GetForeignPlan так же, как было описано выше. Однако поле baserestrictinfo неприменимо к отношениям соединения; вместо этого соответствующие предложения соединения для конкретного соединения передаются в GetForeignJoinPaths в отдельном параметре (extra->restrictlist).
FDW может дополнительно поддерживать прямое выполнение некоторых действий плана, находящихся выше уровня сканирований и соединений, например, группировки или агрегирования. Для реализации этой возможности FDW должна сформировать пути и вставить их в соответствующее верхнее отношение. Например, путь, представляющий удалённое агрегирование, должен вставляться в отношение UPPERREL_GROUP_AGG с помощью add_path. Этот путь будет сравниваться по стоимости с локальным агрегированием, выполненным по результатам пути простого сканирования стороннего отношения (заметьте, что такой путь также должен быть сформирован, иначе во время планирования произойдёт ошибка). Если путь с удалённым агрегированием выигрывает, что, как правило, и происходит, он будет преобразован в план обычным образом, вызовом GetForeignPlan. Такие пути рекомендуется формировать в обработчике GetForeignUpperPaths, который вызывается для каждого верхнего отношения (то есть на каждом шаге обработки после сканирования/соединения), если все базовые отношения запроса выдаются одной обёрткой.
PlanForeignModify и другие обработчики, описанные в Подразделе 53.2.4, рассчитаны на то, что стороннее отношение будет сканироваться обычным способом, а затем отдельные изменения строк будут обрабатываться локальным узлом плана ModifyTable. Этот подход необходим в общем случае, когда для такого изменения требуется прочитать не только сторонние, но и локальные таблицы. Однако если операция может быть целиком выполнена сторонним сервером, FDW может построить путь, представляющий эту возможность, и вставить его в верхнее отношение UPPERREL_FINAL, где он будет конкурировать с подходом ModifyTable. Этот подход также должен применяться для реализации удалённого SELECT FOR UPDATE, вместо обработчиков блокировки строк, описанных Подразделе 53.2.5. Учтите, что путь, вставляемый в UPPERREL_FINAL, отвечает за реализацию всех аспектов поведения запроса.
При планировании запросов UPDATE или DELETE функции PlanForeignModify и PlanDirectModify могут обратиться к структуре RelOptInfo сторонней таблицы и воспользоваться информацией baserel->fdw_private, записанной ранее функциями планирования сканирования. Однако при запросе INSERT целевая таблица не сканируется, так что для неё RelOptInfo не заполняется. На список (List), возвращаемый функцией PlanForeignModify, накладываются те же ограничения, что и на список fdw_private в узле плана ForeignScan, то есть он должен содержать только такие структуры, которые способна копировать функция copyObject.
Команда INSERT с предложением ON CONFLICT не поддерживает указание объекта конфликта, так как уникальные ограничения или ограничения-исключения в удалённых таблицах неизвестны локально. Из этого, в свою очередь, вытекает, что предложение ON CONFLICT DO UPDATE не поддерживается, так как в нём это указание является обязательным.
53.4. Foreign Data Wrapper Query Planning
The FDW callback functions GetForeignRelSize, GetForeignPaths, GetForeignPlan, PlanForeignModify, GetForeignJoinPaths, GetForeignUpperPaths, and PlanDirectModify must fit into the workings of the Postgres Pro planner. Here are some notes about what they must do.
The information in root and baserel can be used to reduce the amount of information that has to be fetched from the foreign table (and therefore reduce the cost). baserel->baserestrictinfo is particularly interesting, as it contains restriction quals (WHERE clauses) that should be used to filter the rows to be fetched. (The FDW itself is not required to enforce these quals, as the core executor can check them instead.) baserel->reltarget->exprs can be used to determine which columns need to be fetched; but note that it only lists columns that have to be emitted by the ForeignScan plan node, not columns that are used in qual evaluation but not output by the query.
Various private fields are available for the FDW planning functions to keep information in. Generally, whatever you store in FDW private fields should be palloc'd, so that it will be reclaimed at the end of planning.
baserel->fdw_private is a void pointer that is available for FDW planning functions to store information relevant to the particular foreign table. The core planner does not touch it except to initialize it to NULL when the RelOptInfo node is created. It is useful for passing information forward from GetForeignRelSize to GetForeignPaths and/or GetForeignPaths to GetForeignPlan, thereby avoiding recalculation.
GetForeignPaths can identify the meaning of different access paths by storing private information in the fdw_private field of ForeignPath nodes. fdw_private is declared as a List pointer, but could actually contain anything since the core planner does not touch it. However, best practice is to use a representation that's dumpable by nodeToString, for use with debugging support available in the backend.
GetForeignPlan can examine the fdw_private field of the selected ForeignPath node, and can generate fdw_exprs and fdw_private lists to be placed in the ForeignScan plan node, where they will be available at execution time. Both of these lists must be represented in a form that copyObject knows how to copy. The fdw_private list has no other restrictions and is not interpreted by the core backend in any way. The fdw_exprs list, if not NIL, is expected to contain expression trees that are intended to be executed at run time. These trees will undergo post-processing by the planner to make them fully executable.
In GetForeignPlan, generally the passed-in target list can be copied into the plan node as-is. The passed scan_clauses list contains the same clauses as baserel->baserestrictinfo, but may be re-ordered for better execution efficiency. In simple cases the FDW can just strip RestrictInfo nodes from the scan_clauses list (using extract_actual_clauses) and put all the clauses into the plan node's qual list, which means that all the clauses will be checked by the executor at run time. More complex FDWs may be able to check some of the clauses internally, in which case those clauses can be removed from the plan node's qual list so that the executor doesn't waste time rechecking them.
As an example, the FDW might identify some restriction clauses of the form foreign_variable = sub_expression, which it determines can be executed on the remote server given the locally-evaluated value of the sub_expression. The actual identification of such a clause should happen during GetForeignPaths, since it would affect the cost estimate for the path. The path's fdw_private field would probably include a pointer to the identified clause's RestrictInfo node. Then GetForeignPlan would remove that clause from scan_clauses, but add the sub_expression to fdw_exprs to ensure that it gets massaged into executable form. It would probably also put control information into the plan node's fdw_private field to tell the execution functions what to do at run time. The query transmitted to the remote server would involve something like WHERE , with the parameter value obtained at run time from evaluation of the foreign_variable = $1fdw_exprs expression tree.
Any clauses removed from the plan node's qual list must instead be added to fdw_recheck_quals or rechecked by RecheckForeignScan in order to ensure correct behavior at the READ COMMITTED isolation level. When a concurrent update occurs for some other table involved in the query, the executor may need to verify that all of the original quals are still satisfied for the tuple, possibly against a different set of parameter values. Using fdw_recheck_quals is typically easier than implementing checks inside RecheckForeignScan, but this method will be insufficient when outer joins have been pushed down, since the join tuples in that case might have some fields go to NULL without rejecting the tuple entirely.
Another ForeignScan field that can be filled by FDWs is fdw_scan_tlist, which describes the tuples returned by the FDW for this plan node. For simple foreign table scans this can be set to NIL, implying that the returned tuples have the row type declared for the foreign table. A non-NIL value must be a target list (list of TargetEntrys) containing Vars and/or expressions representing the returned columns. This might be used, for example, to show that the FDW has omitted some columns that it noticed won't be needed for the query. Also, if the FDW can compute expressions used by the query more cheaply than can be done locally, it could add those expressions to fdw_scan_tlist. Note that join plans (created from paths made by GetForeignJoinPaths) must always supply fdw_scan_tlist to describe the set of columns they will return.
The FDW should always construct at least one path that depends only on the table's restriction clauses. In join queries, it might also choose to construct path(s) that depend on join clauses, for example foreign_variable = local_variable. Such clauses will not be found in baserel->baserestrictinfo but must be sought in the relation's join lists. A path using such a clause is called a “parameterized path”. It must identify the other relations used in the selected join clause(s) with a suitable value of param_info; use get_baserel_parampathinfo to compute that value. In GetForeignPlan, the local_variable portion of the join clause would be added to fdw_exprs, and then at run time the case works the same as for an ordinary restriction clause.
If an FDW supports remote joins, GetForeignJoinPaths should produce ForeignPaths for potential remote joins in much the same way as GetForeignPaths works for base tables. Information about the intended join can be passed forward to GetForeignPlan in the same ways described above. However, baserestrictinfo is not relevant for join relations; instead, the relevant join clauses for a particular join are passed to GetForeignJoinPaths as a separate parameter (extra->restrictlist).
An FDW might additionally support direct execution of some plan actions that are above the level of scans and joins, such as grouping or aggregation. To offer such options, the FDW should generate paths and insert them into the appropriate upper relation. For example, a path representing remote aggregation should be inserted into the UPPERREL_GROUP_AGG relation, using add_path. This path will be compared on a cost basis with local aggregation performed by reading a simple scan path for the foreign relation (note that such a path must also be supplied, else there will be an error at plan time). If the remote-aggregation path wins, which it usually would, it will be converted into a plan in the usual way, by calling GetForeignPlan. The recommended place to generate such paths is in the GetForeignUpperPaths callback function, which is called for each upper relation (i.e., each post-scan/join processing step), if all the base relations of the query come from the same FDW.
PlanForeignModify and the other callbacks described in Section 53.2.4 are designed around the assumption that the foreign relation will be scanned in the usual way and then individual row updates will be driven by a local ModifyTable plan node. This approach is necessary for the general case where an update requires reading local tables as well as foreign tables. However, if the operation could be executed entirely by the foreign server, the FDW could generate a path representing that and insert it into the UPPERREL_FINAL upper relation, where it would compete against the ModifyTable approach. This approach could also be used to implement remote SELECT FOR UPDATE, rather than using the row locking callbacks described in Section 53.2.5. Keep in mind that a path inserted into UPPERREL_FINAL is responsible for implementing all behavior of the query.
When planning an UPDATE or DELETE, PlanForeignModify and PlanDirectModify can look up the RelOptInfo struct for the foreign table and make use of the baserel->fdw_private data previously created by the scan-planning functions. However, in INSERT the target table is not scanned so there is no RelOptInfo for it. The List returned by PlanForeignModify has the same restrictions as the fdw_private list of a ForeignScan plan node, that is it must contain only structures that copyObject knows how to copy.
INSERT with an ON CONFLICT clause does not support specifying the conflict target, as unique constraints or exclusion constraints on remote tables are not locally known. This in turn implies that ON CONFLICT DO UPDATE is not supported, since the specification is mandatory there.