Написание провайдера индивидуализированного сканирования

QHB поддерживает набор экспериментальных средств, позволяющих модулям расширения добавлять в систему новые типы сканирования. В отличие от обертки сторонних данных, которая должна знать только, как сканировать собственные сторонние таблицы, провайдер индивидуализированного сканирования может предоставить альтернативный метод сканирования любого отношения в системе. Как правило, мотив для написания провайдера индивидуализированного сканирования состоит в возможности использования некоторой оптимизации, не поддерживаемой основной системой, например кэширования или некоторой формы аппаратного ускорения. В этой главе описывается, как написать новый провайдер индивидуализированного сканирования.

Реализация нового типа индивидуализированного сканирования — это трехэтапный процесс. Во-первых, во время планирования необходимо сгенерировать пути доступа, представляющие сканирование с использованием предложенной стратегии. Во-вторых, если планировщик выбрал один из этих путей доступа в качестве оптимальной стратегии для сканирования конкретного отношения, такой путь доступа нужно преобразовать в план. Наконец, следует предусмотреть возможность выполнения плана и получения тех же результатов, которые получились бы с любым другим путем доступа, направленным на то же отношение.



Создание путей индивидуализированного сканирования

Провайдер индивидуализированного сканирования обычно добавляет пути для базового отношения, устанавливая следующий обработчик, который вызывается после того, как код ядра создал все возможные для него пути доступа для отношения (за исключением путей Gather, которые создаются после этого вызова с тем, чтобы они могли использовать частичные пути, добавленные этим обработчиком):

typedef void (*set_rel_pathlist_hook_type) (PlannerInfo *root,
                                            RelOptInfo *rel,
                                            Index rti,
                                            RangeTblEntry *rte);
extern PGDLLIMPORT set_rel_pathlist_hook_type set_rel_pathlist_hook;

Хотя эту функцию обработки можно использовать для проверки, изменения или удаления путей, созданных основной системой, провайдер индивидуализированного сканирования обычно ограничивается созданием объектов CustomPath и добавлением их к rel с помощью add_path. Провайдер индивидуализированного сканирования отвечает за инициализацию объекта CustomPath, который объявляется следующим образом:

typedef struct CustomPath
{
    Path      path;
    uint32    flags;
    List     *custom_paths;
    List     *custom_private;
    const CustomPathMethods *methods;
} CustomPath;

Поле path необходимо инициализировать, как и для любого другого пути, включая оценку количества строк, начальную и общую стоимость, а также порядок сортировки, предоставляемый этим путем. Поле flags — это битовая маска, которая должна включать в себя CUSTOMPATH_SUPPORT_BACKWARD_SCAN, если индивидуализированный путь может поддерживать обратное сканирование, и CUSTOMPATH_SUPPORT_MARK_RESTORE, если он может поддерживать маркировку и восстановление позиции. Обе эти возможности являются необязательными. Необязательный параметр custom_paths — это список узлов Path, используемых этим узлом индивидуализированного пути; они будут преобразованы планировщиком в узлы Plan. Поле custom_private можно использовать для хранения частных данных индивидуализированного пути. Частные данные должны храниться в форме, которую может обработать функция nodeToString, чтобы отладочные подпрограммы, пытающиеся вывести индивидуализированный путь, работали как задумано. Поле methods должно указывать на объект (обычно статически выделенный), реализующий требуемые методы индивидуализированного пути, из которых в настоящее время существует только один.

Кроме того, провайдер индивидуализированного сканирования может предоставить пути соединения. Так же, как и для базовых отношений, такой путь должен выдавать тот же результат, что и соединение, которое он заменяет. Для этого провайдер соединения должен установить следующий обработчик, а затем создать в этой функции обработки путь(и) CustomPath для отношения соединения.

typedef void (* set_join_pathlist_hook_type) (PlannerInfo * root,
                                             RelOptInfo * joinrel,
                                             RelOptInfo * externalrel,
                                             RelOptInfo * innerrel,
                                             JoinType jointype,
                                             JoinPathExtraData * extra);
extern PGDLLIMPORT set_join_pathlist_hook_type set_join_pathlist_hook;

Этот обработчик будет вызываться повторно для одного и того же отношения соединения, с различными комбинациями внутренних и внешних отношений; обработчик отвечает за минимизацию лишней работы.


Функции обратного вызова пути индивидуализированного сканирования

Plan *(*PlanCustomPath) (PlannerInfo *root,
                         RelOptInfo *rel,
                         CustomPath *best_path,
                         List *tlist,
                         List *clauses,
                         List *custom_plans);

Преобразовать индивидуализированный путь в готовый план. Возвращаемым значением обычно будет объект CustomScan, который эта функция обратного вызова должна аллоцировать и инициализировать. Более подробную информацию см. в разделе Создание планов индивидуализированного сканирования.



Создание планов индивидуализированного сканирования

Индивидуализированное сканирование представляется в конечном дереве планов в виде следующей структуры:

typedef struct CustomScan
{
    Scan      scan;
    uint32    flags;
    List     *custom_plans;
    List     *custom_exprs;
    List     *custom_private;
    List     *custom_scan_tlist;
    Bitmapset *custom_relids;
    const CustomScanMethods *methods;
} CustomScan;

Поле scan необходимо инициализировать, как и для любого другого сканирования, включая оценки стоимости, списки целевых объектов, условия и т. д. Поле flags — это битовая маска, имеющая тот же смысл, что и в CustomPath. Поле custom_plans можно использовать для хранения дочерних узлов Plan. Поле custom_exprs следует использовать для хранения деревьев выражений, которые необходимо будет исправить с помощью setrefs.c и subselect.c, тогда как custom_private следует использовать для хранения других частных данных, используемых только самим провайдером индивидуализированного сканирования. Поле custom_scan_tlist может иметь значение NIL при сканировании базового отношения, указывая, что индивидуализированное сканирование возвращает кортежи сканирования, соответствующие типу строки базового отношения. В противном случае это список целевых объектов, описывающий фактические кортежи сканирования. Список custom_scan_tlist должен быть предоставлен для соединений и может быть предоставлен для сканирования, если провайдер индивидуализированного сканирования может вычислить некоторые выражения без переменных. Поле custom_relids устанавливается кодом ядра для набора отношений (индексов таблицы диапазонов), которые обрабатывает этот узел сканирования, но когда это сканирование заменяет соединение, в этом поле будет только один элемент. Поле methods должно указывать на объект (обычно статически выделенный), реализующий необходимые методы индивидуализированного сканирования, которые более подробно описаны ниже.

Когда CustomScan сканирует отношение, scan.scanrelid должен быть индексом таблицы диапазонов сканируемой таблицы. Когда этот объект заменяет соединение, scan.scanrelid должен быть нулевым.

Деревья плана должны иметь возможность дублироваться с помощью copyObject, поэтому все данные, хранящиеся в «индивидуализированных» полях, должны состоять из узлов, которые эта функция может обрабатывать. Кроме того, провайдеры индивидуализированного сканирования не могут заменить структуру CustomScan на содержащую ее более крупную структуру, как это было бы возможно для CustomPath или CustomScanState.


Функции обратного вызова плана индивидуализированного сканирования

Node *(*CreateCustomScanState) (CustomScan *cscan);

Выделяет CustomScanState для этого объекта CustomScan. Фактический размер выделенной памяти зачастую будет больше, чем требуется для обычной структуры CustomScanState, потому что многие провайдеры захотят встроить этот объект в качестве первого поля более крупной структуры. Возвращаемое значение должно иметь соответствующим образом заполненные тег узла и поле methods, но другие поля на этом этапе все еще должны содержать нули; после того, как ExecInitCustomScan выполнит базовую инициализацию, будет вызвана функция обратного вызова BeginCustomScan, которая позволит провайдеру индивидуализированного сканирования осуществить все остальные требуемые действия.



Выполнение индивидуализированного сканирования

Когда выполняется CustomScan, его состояние выполнения представляется структурой CustomScanState, которая определяется следующим образом:

typedef struct CustomScanState
{
    ScanState ss;
    uint32    flags;
    const CustomExecMethods *methods;
} CustomScanState;

Поле ss инициализируется, как и для любого другого состояния сканирования, за исключением того, что если сканирование выполняется для соединения, а не для базового отношения, ss.ss_currentRelation остается равным NULL. Поле flags — это битовая маска, имеющая тот же смысл, что и в CustomPath и CustomScan. Поле methods должно указывать на объект (обычно статически выделенный), реализующий требуемые методы состояния индивидуализированного сканирования, более подробно описанные ниже. Как правило, структура CustomScanState, которой не нужно поддерживать copyObject, на самом деле будет включена в более крупную структуру в качестве первого ее члена.


Функции обратного вызова выполнения индивидуализированного сканирования

void (*BeginCustomScan) (CustomScanState *node,
                         EState *estate,
                         int eflags);

Завершить инициализацию предоставленной структуры CustomScanState. Стандартные поля были инициализированы функцией ExecInitCustomScan, но все частные поля должны инициализироваться здесь.

TupleTableSlot *(*ExecCustomScan) (CustomScanState *node);

Извлечь следующий кортеж сканирования. Если какие-либо кортежи остаются, эта функция должна заполнить ps_ResultTupleSlot следующим кортежем в текущем направлении сканирования, а затем вернуть слот кортежа. Если же кортежи закончились, должен быть возвращен NULL или пустой слот.

void (*EndCustomScan) (CustomScanState *node);

Очистить данные, связанные со структурой CustomScanState. Этот метод является обязательным, но ему необязательно что-то делать, если связанные данные отсутствуют или будут очищены автоматически.

void (*ReScanCustomScan) (CustomScanState *node);

Переместить позицию текущего сканирования в начало и подготовить повторное сканирование отношения.

void (*MarkPosCustomScan) (CustomScanState *node);

Сохранить текущую позицию сканирования, чтобы впоследствии ее можно было восстановить с помощью функции обратного вызова RestrPosCustomScan. Эта функция является необязательной и должна предоставляться только в том случае, если установлен флаг CUSTOMPATH_SUPPORT_MARK_RESTORE.

void (*RestrPosCustomScan) (CustomScanState *node);

Восстановить предыдущую позицию сканирования, сохраненную с помощью функции обратного вызова MarkPosCustomScan. Эта функция является необязательной и должна предоставляться только в том случае, если установлен флаг CUSTOMPATH_SUPPORT_MARK_RESTORE.

Size (*EstimateDSMCustomScan) (CustomScanState *node,
                               ParallelContext *pcxt);

Оценить объем разделяемой динамической памяти, которая потребуется для параллельной операции. Это значение может быть больше, чем фактически использованный объем сумма, но не должно быть меньше. Возвращаемое значение представлено в байтах. Эта функция является необязательной и должна предоставляться только в том случае, если этот провайдер индивидуализированного сканирования поддерживает параллельное выполнение.

void (*InitializeDSMCustomScan) (CustomScanState *node,
                                 ParallelContext *pcxt,
                                 void *coordinate);

Инициализировать разделяемую динамическую память, которая потребуется для параллельной операции. Поле coordinate указывает на область разделяемой памяти размером, равным значению, возвращаемому EstimateDSMCustomScan. Эта функция является необязательной и должна предоставляться только в том случае, если этот провайдер индивидуализированного сканирования поддерживает параллельное выполнение.

void (*ReInitializeDSMCustomScan) (CustomScanState *node,
                                   ParallelContext *pcxt,
                                   void *coordinate);

Повторно инициализировать разделяемую динамическую память, требуемую для параллельной операции, перед тем как узел плана индивидуализированного сканирования будет повторно сканироваться. Эта функция является необязательной и должна предоставляться только в том случае, если этот провайдер индивидуализированного сканирования поддерживает параллельное выполнение. Рекомендуемый подход заключается в том, чтобы эта функция сбрасывала только разделяемое состояние, а функция ReScanCustomScan сбрасывала только локальное состояние. В настоящее время эта функция обратного вызова будет вызываться перед ReScanCustomScan, но лучше всего на такой порядок не рассчитывать.

void (*InitializeWorkerCustomScan) (CustomScanState *node,
                                    shm_toc *toc,
                                    void *coordinate);

Инициализировать локальное состояние параллельного рабочего процесса на основе разделяемого состояния, заданного ведущим процессом во время выполнения InitializeDSMCustomScan. Эта функция является необязательной и должна предоставляться только в том случае, если этот провайдер индивидуализированного сканирования поддерживает параллельное выполнение.

void (*ShutdownCustomScan) (CustomScanState *node);

Освободить ресурсы, когда ожидается, что узел не будет выполнен до конца. Эта функция вызывается не всегда; иногда EndCustomScan можно вызвать без предварительного вызова ShutdownCustomScan. Поскольку сегмент DSM, используемый параллельным запросом, уничтожается сразу же после вызова этой функции обратного вызова, провайдеры индивидуализированного сканирования, желающие предпринять некоторые действия, прежде чем сегмент DSM исчезнет, должны реализовать этот метод.

void (*ExplainCustomScan) (CustomScanState *node,
                           List *ancestors,
                           ExplainState *es);

Вывести дополнительную информацию для узла EXPLAIN плана индивидуализированного сканирования. Эта функция является необязательной. Разделяемые данные, сохраненные в ScanState, например список целевых объектов и отношение сканирования, будут показаны даже без этой функции обратного вызова, но она позволяет отображать дополнительное, внутреннее состояние.