54.4. Планирование запросов с обёртками сторонних данных

Процедуры в FDW, реализующие функции GetForeignRelSize, GetForeignPaths, GetForeignPlan, PlanForeignModify и GetForeignJoinPaths должны вписываться в работу планировщика PostgreSQL. Здесь даётся несколько замечаний о том, как это должно происходить.

Для уменьшения объёма выбираемой из сторонней таблицы информации (и как следствие, сокращения стоимости) может использоваться информация, поступающая в root и baserel. Особый интерес представляет поле baserel->baserestrictinfo, так как оно содержит ограничивающие условия (предложения WHERE), по которым можно отфильтровать выбираемые строки. (Сама FDW не обязательно должна применять эти ограничения, так как их может проверить и ядро исполнителя.) Список baserel->reltargetlist позволяет определять, какие именно столбцы требуется выбрать; но учтите, что в нём перечисляются только те столбцы, которые выдаются узлом плана 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 сторонняя_переменная = $1, а значение параметра будет получено во время выполнения в результате вычисления дерева выражения fdw_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).

При планировании запросов UPDATE или DELETE PlanForeignModify может обратиться к структуре RelOptInfo сторонней таблицы и воспользоваться информацией baserel->fdw_private, записанной ранее функциями планирования сканирования. Однако при запросе INSERT целевая таблица не сканируется, так что для неё RelOptInfo не заполняется. На список (List), возвращаемый функцией PlanForeignModify, накладываются те же ограничения, что и на список fdw_private в узле плана ForeignScan, то есть он должен содержать только такие структуры, которые способна копировать функция copyObject.

Команда INSERT с предложением ON CONFLICT не поддерживает указание объекта конфликта, так как уникальные ограничения или ограничения-исключения в удалённых таблицах неизвестны локально. Из этого, в свою очередь, вытекает, что предложение ON CONFLICT DO UPDATE не поддерживается, так как в нём это указание является обязательным.