Определение интерфейса для индексных методов доступа

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

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

По сути индекс — это средство сопоставления некоторых значений ключей данных с идентификаторами кортежей (TID) версий строк (кортежей) в родительской таблице индекса. TID состоит из номера блока и номера элемента внутри этого блока (см. раздел Внутренняя структура страницы базы данных). Этой информации достаточно для извлечения определенной версии строки из таблицы. Сами индексы не знают, что при использовании MVCC может существовать несколько версий одной логической строки; для индекса каждый кортеж является независимым объектом, которому требуется отдельная запись в индексе. Таким образом, при изменении строки в индексах всегда создаются для нее новые записи, даже если значения ключей не изменились. (Кортежи HOT являются исключением из этого утверждения, но индексы не имеют с ними дела.) Индексные записи для неиспользуемых кортежей высвобождаются (при очистке), когда высвобождаются сами неиспользуемые кортежи.



Базовая структура API для индексов

Каждый индексный метод доступа описывается строкой в системном каталоге pg_am. Запись в pg_am содержит имя и функцию обработки индексного метода доступа. Эти записи можно создавать и удалять с помощью команд SQL CREATE ACCESS METHOD и DROP ACCESS METHOD.

Функция обработки индексного метода доступа должна принимать один аргумент типа internal и возвращать псевдотип index_am_handler. Аргумент является фиктивным значением, которое требуется только для предотвращения вызова функций обработки непосредственно из команд SQL. Результатом работы функции должна быть выделенная с помощью palloc структура типа IndexAmRoutine, содержащая все, что должен знать код ядра для использования индексного метода доступа. Структура IndexAmRoutine, также называемая API-структурой метода доступа, включает поля, задающие различные фиксированные свойства метода доступа, например возможность поддержки многостолбцовых индексов. Что еще более важно, она содержит указатели на вспомогательные функции метода доступа, выполняющие всю реальную работу по обращению к индексам. Эти вспомогательные функции являются простыми функциями на C/RUST и не видны и не могут вызываться на уровне SQL. Вспомогательные функции описываются в разделе Функции индексных методов доступа.

Структура IndexAmRoutine определяется таким образом:

typedef struct IndexAmRoutine
{
    NodeTag     type;

    /*
     * Общее количество стратегий (операторов), с которыми возможен просмотр/поиск
     * этого метода доступа (МД). Ноль, если у МД нет фиксированного набора
     * назначенных стратегий.
     */
    uint16      amstrategies;
    /* общее количество вспомогательных функций, используемых этим МД */
    uint16      amsupport;
    /* номер вспомогательной функции options класса операторов или 0 */
    uint16      amoptsprocnum;
    /* поддерживает ли МД упорядочивание (ORDER BY) значений индексированного столбца? */
    bool        amcanorder;
    /* поддерживает ли МД упорядочивание (ORDER BY) результата оператора с индексированным столбцом? */
    bool        amcanorderbyop;
    /* поддерживает ли МД обратное сканирование? */
    bool        amcanbackward;
    /* поддерживает ли МД уникальные индексы (UNIQUE)? */
    bool        amcanunique;
    /* поддерживает ли МД многостолбцовые индексы? */
    bool        amcanmulticol;
    /* требуется ли для сканирование с МД ограничение первого столбца индекса? */
    bool        amoptionalkey;
    /* обрабатывает ли МД условия ScalarArrayOpExpr? */
    bool        amsearcharray;
    /* обрабатывает ли МД условия IS NULL/IS NOT NULL? */
    bool        amsearchnulls;
    /* может ли тип данных индексного хранилища отличаться от типа данных столбца? */
    bool        amstorage;
    /* возможна ли клаcтеризация по индексу этого типа? */
    bool        amclusterable;
    /* обрабатывает ли МД предикатные блокировки? */
    bool        ampredlocks;
    /* обрабатывает ли МД параллельное сканирование? */
    bool        amcanparallel;
    /* поддерживает ли МД столбцы, добавляемые предложением INCLUDE? */
    bool        amcaninclude;
    /* использует ли МД maintenance_work_mem? */
    bool        amusemaintenanceworkmem;
    /* ИЛИ флаги параллельной очистки */
    uint8       amparallelvacuumoptions;
    /* тип данных, хранящихся в индексе, или InvalidOid, если он переменный */
    Oid         amkeytype;

    /* функции интерфейса */
    ambuild_function ambuild;
    ambuildempty_function ambuildempty;
    aminsert_function aminsert;
    ambulkdelete_function ambulkdelete;
    amvacuumcleanup_function amvacuumcleanup;
    amcanreturn_function amcanreturn;   /* может быть NULL */
    amcostestimate_function amcostestimate;
    amoptions_function amoptions;
    amproperty_function amproperty;     /* может быть NULL */
    ambuildphasename_function ambuildphasename;   /* может быть NULL */
    amvalidate_function amvalidate;
    amadjustmembers_function amadjustmembers; /* может быть NULL */
    ambeginscan_function ambeginscan;
    amrescan_function amrescan;
    amgettuple_function amgettuple;     /* может быть NULL */
    amgetbitmap_function amgetbitmap;   /* может быть NULL */
    amendscan_function amendscan;
    ammarkpos_function ammarkpos;       /* может быть NULL */
    amrestrpos_function amrestrpos;     /* может быть NULL */

    /* функции интерфейса для поддержки параллельного сканирования по индексу */
    amestimateparallelscan_function amestimateparallelscan;    /* может быть NULL */
    aminitparallelscan_function aminitparallelscan;    /* может быть NULL */
    amparallelrescan_function amparallelrescan;    /* может быть NULL */
} IndexAmRoutine;

Чтобы индексный метод доступа можно было применять на практике, он также должен иметь одно или несколько семейств операторов и классов операторов, определенных в каталогах pg_opfamily, pg_opclass, pg_amop и pg_amproc. Эти записи позволяют планировщику определить, какие виды запросов можно использовать с индексами этого метода доступа. Семейства операторов и классы описываются в разделе Интерфейсные расширения для индексов, материал которого необходимо изучить, прежде чем приступить к чтению этой главы.

Отдельный индекс определяется записью в каталоге pg_class, описывающей его как физическое отношение, плюс запись в каталоге pg_index, показывающей логическое содержимое индекса, то есть набор столбцов индекса, и семантику этих столбцов, установленную связанными классами операторов. Столбцы индекса (ключевые значения) могут быть либо простыми столбцами нижележащей таблицы, либо выражениями, вычисляемыми по строкам этой таблицы. Обычно для индексного метода доступа не имеет значения, откуда берутся значения ключей индекса (ему всегда передаются заранее вычисленные значения ключей), но очень важна информация о классе оператора в pg_index. Обе эти записи каталога доступны в составе структуры данных Relation, которая передается для всех операций с индексом.

Некоторые из полей флагов структуры IndexAmRoutine имеют нетривиальное назначение. Требования, предъявляемые к amcanunique, рассматриваются в разделе Проверки уникальности индекса. Флаг amcanmulticol означает, что метод доступа поддерживает многостолбцовые индексы, тогда как amoptionalkey — что метод доступа разрешает сканирование, когда для первого столбца индекса не задано индексируемое ограничительное предложение. Когда amcanmulticol равен false, amoptionalkey по сути говорит, поддерживает ли метод доступа полное сканирование по индексу без каких-либо ограничительных условий. Методы доступа, поддерживающие несколько столбцов в индексе, должны поддерживать сканирования, позволяющие пропускать ограничения для любого или всех столбцов после первого, однако им разрешено требовать, чтобы для первого столбца индекса ограничения использовались; об этом сигнализирует установка флага amoptionalkey в false. Одна из причин, по которой метод доступа может установить в amoptionalkey значение false, это, например, если он не индексирует значения NULL. Поскольку большинство индексируемых операторов являются строгими и, следовательно, не могут возвращать true, если операнд равен NULL, на первый взгляд кажется правильным не хранить записи индекса для значений NULL: они никогда не могут быть возвращены при сканировании индекса. Однако этот аргумент не сработает, когда при сканировании индекса отсутствует ограничительное предложение для данного столбца индекса. На практике это означает, что индексы, у которых amoptionalkey равен true, должны индексировать значения NULL, поскольку планировщик может решить использовать такой индекс вообще без ключей сканирования. Связанное с этим ограничение заключается в том, что индексный метод доступа, поддерживающий несколько столбцов индекса, должен поддерживать индексирование значений NULL в столбцах после первого, поскольку планировщик будет предполагать, что индекс можно использовать для запросов, не ограничивающих эти столбцы. Например, рассмотрим индекс по (a,b) и запрос с WHERE a = 4. Система будет считать, что индекс можно использовать для сканирования строк с а = 4, что неверно, если индекс пропускает строки, где b равен NULL. Однако вполне допустимо пропускать строки, в которых первый индексированный столбец равен NULL. Индексный метод доступа, действительно индексирующий значения NULL, может также установить флаг amsearchnulls, показывая, что он поддерживает предложения IS NULL и IS NOT NULL в условиях поиска.

Флаг amcaninclude показывает, поддерживает ли метод доступа «неключевые» столбцы, то есть может ли он сохранить (без обработки) дополнительные столбцы помимо ключевых. Требования в предыдущем абзаце применимы только к ключевым столбцам. В частности, сочетание amcanmulticol=false и amcaninclude=true имеет смысл: оно означает, что в индексе может быть только один ключевой столбец, но при этом могут быть дополнительные неключевые столбцы. Кроме того, неключевые столбцы могут содержать NULL независимо от флага amoptionalkey.


Функции индексных методов доступа

Индексный метод доступа в структуре IndexAmRoutine должен предоставить следующие функции построения и обслуживания индекса:

IndexBuildResult *
ambuild (Relation heapRelation,
         Relation indexRelation,
         IndexInfo *indexInfo);

Построить новый индекс. Отношение индекса было физически создано, но остается пустым. Оно должно быть заполнено всеми фиксированными данными, которые требуются методу доступа, а также записями для всех кортежей, уже существующих в таблице. Обычно для сканирования таблицы на наличие существующих кортежей и вычисления ключей, которые необходимо вставить в индекс, функция ambuild вызывает table_index_build_scan(). Эта функция должна возвращать структуру, выделенную с помощью palloc и содержащую статистические данные о новом индексе.

void
ambuildempty (Relation indexRelation);

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

bool
aminsert (Relation indexRelation,
          Datum *values,
          bool *isnull,
          ItemPointer heap_tid,
          Relation heapRelation,
          IndexUniqueCheck checkUnique,
          IndexInfo *indexInfo);

Вставить новую запись в существующий индекс. Массивы values и isnull задают значения ключей для индексирования, а heap_tid — это TID индексируемого кортежа. Если метод доступа поддерживает уникальные индексы (его флаг amcanunique равен true), тогда аргумент checkUnique указывает тип выполняемой проверки уникальности. Ее тип зависит от того, является ли ограничение уникальности откладываемым; подробную информацию см. в разделе Проверки уникальности индекса. Обычно при выполнении проверки уникальности методу доступа требуется только параметр heapRelation (так ему придется просмотреть кучу, чтобы убедиться в активности кортежа).

Логическое значение indexUnchanged дает подсказку о природе индексируемого кортежа. Когда это значение true, кортеж является дубликатом некоторого существующего кортежа в индексе. Новый кортеж является логически неизмененным преемником MVCC-версии кортежа. Это происходит, когда выполняется команда UPDATE, которая не изменяет столбцы, охватываемые индексом, но все равно требует добавления в индекс новой версии кортежа. Индексный МД может использовать эту подсказку для принятия решения о применении восходящего удаления в тех частях индекса, где скапливается много версий одной логической строки. Обратите внимание, что изменение неключевого столбца не влияет на значение indexUnchanged.

Логическое значение результата функции является значимым, только когда checkUnique имеет значение UNIQUE_CHECK_PARTIAL. В этом случае результат true означает, что новая запись считается уникальной, тогда как false означает, что она может быть неуникальной (и следует запланировать отложенную проверку уникальности). В других случаях рекомендуется всегда возвращать false.

Некоторые индексы могут индексировать не все кортежи. Если кортеж не будет индексироваться, aminsert должна просто вернуться, ничего не делая.

Если индексный МД хочет кэшировать данные между последовательными добавлениями в индекс в одном операторе SQL, он может выделить пространство в indexInfo-> ii_Context и хранить указатель на данные в поле indexInfo->ii_AmCache (которое изначально будет равно NULL).

IndexBulkDeleteResult *
ambulkdelete (IndexVacuumInfo *info,
              IndexBulkDeleteResult *stats,
              IndexBulkDeleteCallback callback,
              void *callback_state);

Удалить кортеж(и) из индекса. Это операция «массового удаления», которая предположительно будет реализована путем сканирования всего индекса и проверки каждой записи на предмет, следует ли ее удалить. Переданную функцию callback следует вызывать в стиле callback( TID, callback_state) returns bool, чтобы определить, нужно ли удалить какую-то конкретную запись индекса, заданную соответствующим TID. Эта функция должна возвращать либо NULL, либо структуру, выделенную palloc и содержащую статистику по результатам операции удаления. NULL можно вернуть, если никакая информация не должна передаваться функции amvacuumcleanup.

Из-за ограничения maintenance_work_mem при необходимости удаления большого количества кортежей может потребоваться вызывать функцию ambulkdelete несколько раз. Аргумент stats является результатом предыдущего вызова для этого индекса (он равен NULL для первого вызова в рамках операции VACUUM). Это позволяет МД накапливать статистику по всей операции. Как правило, ambulkdelete модифицирует и возвращает ту же структуру, если переданный stats не равен NULL.

IndexBulkDeleteResult *
amvacuumcleanup (IndexVacuumInfo *info,
                 IndexBulkDeleteResult *stats);

Выполнить очистку после операции VACUUM (до этого могло быть ноль и более вызовов ambulkdelete). Эта функция не должна делать ничего, кроме возврата статистики по индексу, но может выполнить массовую очистку, например высвобождение пустых страниц индекса. Аргумент stats содержит результат последнего вызова функции ambulkdelete или NULL, если ambulkdelete не вызывалась, поскольку не нужно было удалять никакие кортежи. Если результат отличен от NULL, это должна быть структура, выделенная palloc. Содержащаяся в ней статистика будет использоваться для обновления каталога pg_class и выводиться командой VACUUM, если в той указано VERBOSE. NULL можно вернуть, если индекс вообще не изменялся во время операции VACUUM, однако в противном случае должна возвращаться корректная статистика.

Функция amvacuumcleanup также будет вызваться по завершении операции ANALYZE. В этом случае stats всегда равен NULL, а любое возвращаемое значение будет игнорироваться. Этот случай можно распознать проверкой info->analyze_only. Рекомендуется, чтобы при таком вызове метод доступа выполнял только очистку после добавления данных и только в рабочем процессе автоочистки.

bool
amcanreturn (Relation indexRelation, int attno);

Проверить, поддерживает ли индекс сканирование только по индексу для данного столбца, возвращая исходное индексированное значение этого столбца. Нумерация атрибутов начинается с 1, т. е. attno для первого столбца должен быть равен 1. Возвращает true, если такое поведение поддерживается, и false в противном случае. Эта функция должна всегда возвращать true для неключевых столбцов (если таковые поддерживаются), поскольку в неключевом столбце, значение которого нельзя извлечь, мало смысла. Если метод доступа вообще не поддерживает сканирование только по индексу, то в поле amcanreturn в его структуре IndexAmRoutine можно установить NULL.

void
amcostestimate (PlannerInfo *root,
                IndexPath *path,
                double loop_count,
                Cost *indexStartupCost,
                Cost *indexTotalCost,
                Selectivity *indexSelectivity,
                double *indexCorrelation,
                double *indexPages);

Оценить примерную стоимость сканирования индекса. Эта функция полностью описана в разделе Функции оценки стоимости индекса ниже.

bytea *
amoptions (ArrayType *reloptions,
           bool validate);

Проанализировать и проверить массив параметров для индекса. Эта функция вызывается, только когда для индекса существует массив параметров, отличный от NULL. Аргумент reloptions — это массив типа text, содержащий записи вида имя=значение. Функция должна сформировать значение bytea, которое будет скопировано в поле rd_options записи индекса в кэше отношений. Содержимое bytea определяется самим методом доступа; большинство стандартных методов доступа используют структуру StdRdOptions. Когда аргумент validate равен true, функция должна выдавать соответствующее сообщение об ошибке, если какой-либо из параметров не распознается или имеет недопустимое значение; когда validate равен false, недопустимые значения должны молча игнорироваться. (Аргумент validate имеет значение false при загрузке параметров, уже сохраненных в pg_catalog; недопустимую запись можно найти, только если метод доступа изменил свои правила для параметров, и в этом случае игнорировать устаревшие записи уместно). NULL можно вернуть, если требуется поведение по умолчанию.

bool
amproperty (Oid index_oid, int attno,
            IndexAMProperty prop, const char *propname,
            bool *res, bool *isnull);

Метод amproperty позволяет методу доступа переопределить поведение по умолчанию pg_index_column_has_property и связанных функций. Если у метода доступа нет никакого специального поведения для запросов свойств индекса, то в поле amproperty структуры IndexAmRoutine можно установить NULL. В противном случае метод amproperty будет вызываться с нулевыми аргументами index_oid и attno при вызовах pg_indexam_has_property или с допустимым значением index_oid и attno больше нуля при вызовах pg_index_column_has_property. Аргумент prop является перечислимым значением, идентифицирующим проверяемое свойство, тогда как аргумент propname является исходной строкой имени свойства. Если код ядра не распознает имя свойства, то в prop передается AMPROP_UNKNOWN. Методы доступа могут определять нестандартные имена свойств, проверяя propname на совпадение (для согласованности с кодом ядра используйте для этого pg_strcasecmp); для имен, известных коду ядра, лучше проверить аргумент prop. Если метод amproperty возвращает true, значит он определил результат проверки свойства: он должен установить в *res возвращаемое логическое значение или установить в *isnull true, чтобы вернуть NULL. (Обе указанные переменные инициализируются перед вызовом в false.) Если метод amproperty возвращает false, то код ядра продолжает свою обычную логику для определения результата проверки свойства.

Методы доступа, поддерживающие операторы сортировки, должны реализовывать проверку свойства AMPROP_DISTANCE_ORDERABLE, так как код ядра не знает, как это сделать, и вернет NULL. Также стоит реализовать проверку AMPROP_RETURNABLE, если ее можно сделать гораздо дешевле, чем открывая индекс и вызывая amcanreturn, что является поведением кода ядра. Поведение по умолчанию должно быть удовлетворительным для всех остальных стандартных свойств.

char *
ambuildphasename (int64 phasenum);

Вернуть текстовое название фазы построения индекса с заданным номером. Номера фаз выводятся во время построения индекса через интерфейс функции pgstat_progress_update_param. Затем названия фаз отображаются в представлении pg_stat_progress_create_index.

bool
amvalidate (Oid opclassoid);

Проверить записи каталога для заданного класса операторов, насколько адекватно это может сделать метод доступа. Например, это может включать проверку на предмет того, предоставляются ли все необходимые вспомогательные функции. Функция amvalidate должна вернуть false, если класс операторов является недопустимым. О проблемах следует сообщать посредством сообщений ereport, обычно уровня INFO.

void
amadjustmembers (Oid opfamilyoid,
                 Oid opclassoid,
                 List *operators,
                 List *functions);

Проверить предложенные новые члены семейства операторов (операторы и функции), насколько адекватно это может сделать метод доступа, и установить типы их зависимостей, если значения по умолчанию неудовлетворительны. Эта функция вызывается во время выполнения команд CREATE OPERATOR CLASS и ALTER OPERATOR FAMILY ADD; в последнем случае аргумент opclassoid имеет значение InvalidOid. Аргументы List являются списками структур OpFamilyMember, как описано в amapi.h. Проверки, проводимые этой функцией, обычно являются подмножеством проверок, осуществляемых amvalidate, поскольку amadjustmembers не может предположить, что видит полный набор членов. Например, будет иметь смысл проверить сигнатуры вспомогательной функции, но не проверять, предоставляются ли все необходимые вспомогательные функции. О любых проблемах можно сообщить, выдав ошибку. Связанные с зависимостями поля структур OpFamilyMember инициализируются кодом ядра, создавая жесткие зависимости от класса операторов, если это CREATE OPERATOR CLASS, или мягкие зависимости от семейства оператором, если это ALTER OPERATOR FAMILY ADD. Функция amadjustmembers может скорректировать эти поля, если более уместно некое другое поведение. К примеру, GIN, GiST и SP-GiST всегда устанавливают для членов- операторов мягкую зависимость от семейства операторов, поскольку в этих типах индексов связь между оператором и классом операторов относительно слаба; поэтому имеет смысл разрешить свободное добавление и удаление членов-операторов. Необязательным вспомогательным функциям обычно тоже выдают мягкие зависимости, чтобы при необходимости их можно было удалить.

Цель индекса, конечно же, заключается в поддержке сканирования для кортежей, соответствующих индексируемому условию WHERE, часто называемому квалификатором или ключом сканирования. Более подробно семантика индексного сканирования описывается в разделе Сканирование индекса ниже. Индексный метод доступа может поддерживать «простые» индексные сканирования, индексные сканирования «по битовой карте» или оба этих вида. Индексный метод доступа должен или может предоставить следующие относящиеся к сканированию функции:

IndexScanDesc
ambeginscan (Relation indexRelation,
             int nkeys,
             int norderbys);

Подготовить метод к сканированию индекса. Параметры nkeys и norderbys указывают количество условий и операторов сортировки, которые будут использоваться в этом сканировании; это может быть полезно для выделения памяти. Обратите внимание, что фактические значения ключей сканирования еще не предоставляются. Результатом функции должна быть структура, выделенная с помощью palloc. В связи с особенностями реализации, индексный метод доступа должен создать эту структуру путем вызова функции RelationGetIndexScan(). В большинстве случаев все действия ambeginscan сводятся к данному вызову и, возможно, получению блокировок; интересные этапы запуска сканирования индекса происходят в функции amrescan.

void
amrescan (IndexScanDesc scan,
          ScanKey keys,
          int nkeys,
          ScanKey orderbys,
          int norderbys);

Запустить или перезапустить сканирование индекса, возможно, с новыми ключами сканирования. (Для перезапуска с использованием ранее переданных ключей в аргументах keys и/или orderbys передается NULL.) Обратите внимание, что количество ключей или операторов сортировки не может быть больше значений, переданных в функцию ambeginscan. На практике возможность перезапуска используется, когда в соединении с вложенным циклом выбирается новый внешний кортеж, и поэтому требуется новое значение сравнения с ключом, но структура ключа сканирования остается прежней.

boolean
amgettuple (IndexScanDesc scan,
            ScanDirection direction);

Извлечь следующую запись в заданном сканировании, двигаясь по индексу в указанном направлении (вперед или назад). Возвращает true, если кортеж получен, false, если подходящих кортежей не осталось. В случае успеха (true) TID этого кортежа сохраняется в структуре scan. Обратите внимание, что «успех» означает только то, что индекс содержит запись, соответствующую ключам сканирования, а не то, что кортеж обязательно все еще существует в таблице или пройдет проверку на видимость в снимке вызывающего кода. При успехе функция amgettuple должна также установить в scan->xs_recheck значение true или false. False означает, что запись индекса точно соответствует ключам сканирования. True означает, что это не точно, и условия, представленные ключами сканирования, следует перепроверить для кортежа кучи после его извлечения. Эта мера предосторожности поддерживает операторы индексов «c потерями». Обратите внимание, что перепроверка будет распространяться только на условия сканирования; предикат частичных индексов (если таковой имеется) никогда не перепроверяется кодом, вызывающим amgettuple.

Если индекс поддерживает сканирование только по индексу (т. е. amcanreturn возвращает true для любого из его столбцов), то в случае успешного сканирования МД также должен проверить значение scan->xs_want_itup, и если оно равно true, то МД обязан вернуть исходные индексированные данные для этой записи индекса. В столбцах, для которых amcanreturn возвращает false, можно вернуть NULL. Данные могут быть возвращены в форме указателя IndexTuple, сохраненного в scan->xs_itup, с дескриптором кортежа scan->xs_itupdesc, или в форме указателя HeapTuple, сохраненного в scan->xs_hitup, с дескриптором кортежа scan->xs_hitupdesc. (Последний формат следует использовать при восстановлении данных, которые могут не уместиться в IndexTuple). В любом случае управление данными, на которые ссылается указатель, является ответственностью метода доступа. Данные должны оставаться пригодными по крайней мере до следующего вызова amgettuple, amrescan, или amendscan.

Функцию amgettuple нужно предоставлять, только если метод доступа поддерживает «простые» индексные сканирования. Если это не так, то в поле amgettuple его структуры IndexAmRoutine необходимо установить NULL.

int64
amgetbitmap (IndexScanDesc scan,
             TIDBitmap *tbm);

Извлечь все кортежи в данном сканировании и добавить их в переданную вызывающим кодом структуру TIDBitmap (то есть объединяет операцией OR множество идентификаторов кортежей со множеством, уже находящимся в битовой карте). Возвращается количество извлеченных кортежей (это может быть только примерное число; например, некоторые МД не распознают дубликаты). При добавлении идентификаторов кортежей в битовую карту amgettbitmap может пометить, что для некоторых кортежей нужно перепроверить условия сканирования. Это аналогично выходному параметру xs_recheck функции amgettuple. Примечание: в текущей реализации поддержка этой возможности объединена с поддержкой хранения с потерями самой битовой карты, поэтому вызывающая функция перепроверяет и условия сканирования, и предикат частичного индекса (если таковой имеется) для кортежей, помеченных для повторной проверки. Однако так может быть не всегда. Функции amgettbitmap и amgettuple нельзя использовать в одном сканировании индекса; при использовании amgettbitmap есть и другие ограничения, описанные в разделе Сканирование индекса.

Функцию amgettbitmap нужно предоставлять, только если метод доступа поддерживает сканирование индекса «по битовой карте». Если это не так, то в поле amgettbitmap его структуры IndexAmRoutine необходимо установить NULL.

void
amendscan (IndexScanDesc scan);

Завершить сканирование и освободить ресурсы. Саму структуру scan освобождать не следует, но любые блокировки или закрепления, полученные внутри метода доступа, должны быть освобождены, равно как и любая другая память, выделенная ambeginscan и другими функциями, относящимися к сканированию.

void
ammarkpos (IndexScanDesc scan);

Пометить текущую позицию сканирования. Метод доступа должен поддерживать запоминание только одной позиции на сканирование.

Функция ammarkpos нужно предоставлять, только если метод доступа поддерживает упорядоченное сканирование. Если это не так, то в поле ammarkpos его структуры IndexAmRoutine можно установить NULL.

void
amrestrpos (IndexScanDesc scan);

Восстановить сканирование до самой последней отмеченной позиции.

Функцию amrestrpos нужно предоставлять, только если метод доступа поддерживает упорядоченное сканирование. Если это не так, то в поле amrestrpos его структуры IndexAmRoutine можно установить NULL.

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

Size
amestimateparallelscan (void);

Предварительно рассчитать и вернуть количество байтов динамической разделяемой памяти, которое потребуется методу доступа для выполнения параллельного сканирования. (Это количество дополняет, а не заменяет объем памяти, требуемый для независимых от МД данных в структуре ParallelIndexScanDescData.)

Нет необходимости реализовывать эту функцию для методов доступа, которые не поддерживают параллельное сканирование или для которых не требуются дополнительные байты памяти.

void
aminitparallelscan (void *target);

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

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

void
amparallelrescan (IndexScanDesc scan);

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


Сканирование индекса

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

Ключ сканирования — это внутреннее представление предложения WHERE в форме ключ_индекса оператор константа, где ключ индекса является одним из столбцов индекса, а оператор — одним из членов семейства операторов, связанных с этим столбцом. Сканирование индекса имеет ноль или более ключей сканирования, неявно объединенных операцией AND — ожидается, что возвращенные кортежи будут удовлетворять всем указанным условиям.

Метод доступа для конкретного запроса может сообщить, что это индекс с потерями, т. е. требует перепроверок. Это подразумевает, что сканирование индекса вернет все записи, удовлетворяющие ключу сканирования, и, возможно, дополнительные записи, которые ему не удовлетворяют. Затем механизм основной системы, отвечающий за сканирование индекса, снова применит условия индекса к кортежу кучи, чтобы проверить, действительно ли его нужно выбрать. Если параметр перепроверки не задан, сканирование индекса должно возвращать множество записей, точно соответствующих ключу.

Обратите внимание, что именно метод доступа должен гарантировать, что он правильно находит все и только те записи, которые удовлетворяют всем заданным ключам сканирования. Кроме того, основная система просто передаст все предложения WHERE, соответствующие ключам индекса и семействам операторов, без какого-либо семантического анализа на избыточность или противоречивость. Например, при заданном условии WHERE x > 4 AND x > 14, где x является индексированным столбцом B-дерева, именно самой функции amrescan B-дерева нужно будет понять, что первый ключ сканирования избыточен, и его можно отбросить. Объем предварительной обработки, требуемой во время выполнения amrescan, будет зависеть от степени, до которой индексному методу доступа нужно сводить ключи сканирования к «нормализованной» форме.

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

  • Методы доступа, всегда возвращающие записи в естественном порядке их данных как, например, в B-дереве), должны установить в amcanorder значение true. В настоящее время такие методы доступа должны использовать для своих операторов равенства и сортировки номера стратегий, совместимых с B-деревьями.

  • Методы доступа, поддерживающие операторы сортировки, должны установить в amcanorderbyop значение true. Это указывает на то, что индекс способен возвращать записи в порядке, удовлетворяющем предложению ORDER BY ключ_индекса оператор константа. Модификаторы сканирования этой формы могут передаваться в функцию amrescan, как описывалось ранее.

У функции amgettuple имеется аргумент direction, который может принимать значение ForwardScanDirection (обычный вариант) или BackwardScanDirection. Если в первом вызове после amrescan задается BackwardScanDirection, то множество подходящих записей индекса будет сканироваться от конца к началу, а не в обычном направлении от начала к концу, поэтому функция amgettuple должна вернуть последний соответствующий кортеж в индексе, а не первый, как это обычно бывает. (Это будет происходить только для методов доступа, установивших флаг amcanorder в true.) После первого вызова amgettuple должна быть готова продолжить сканирование в любом направлении от последней возвращенной записи. (Но если флаг amcanbackward равен false, все последующие вызовы будут иметь то же направление, что и первый.)

Методы доступа, поддерживающие упорядоченное сканирование, должны поддерживать «маркировку» позиции сканирования и последующий возврат к помеченной позиции. В одну позицию можно вернуться несколько раз. Однако запоминать нужно только одну позицию на сканирование; новый вызов функции ammarkpos переопределяет позицию, помеченную ранее. Методам доступа, не поддерживающим упорядоченное сканирование, необязательно реализовывать функции ammarkpos и amrestrpos в IndexAmRoutine; вместо этого можно установить в этих указателях NULL.

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

Если индекс хранит исходные значения индексированных данных (а не некоторое их представление с потерями), полезно поддерживать сканирование только по индексу, в котором индекс возвращает фактические данные, а не только TID кортежа кучи. Это позволит избежать нагрузки ввода/вывода только в том случае, если карта видимости показывает, что TID находится на полностью видимой странице; иначе кортеж кучи все равно придется прочитать, чтобы проверить видимость для MVCC. Но метода доступа это уже не касается.

Вместо amgettuple сканирование индекса можно выполнить с помощью функции amgettbitmap, чтобы извлечь все кортежи за один вызов. Это может быть гораздо эффективнее amgettuple, потому что позволяет избежать циклов блокировки/ разблокировки в методе доступа. В принципе, вызов amgettbitmap должен приводить к тому же результату, что и несколько вызовов amgettuple, но чтобы упростить дело, мы накладываем несколько ограничений. Во-первых, amgettbitmap возвращает все кортежи сразу, и маркировка или восстановление позиции сканирования не поддерживается. Во-вторых, кортежи возвращаются в виде битовой карты, в которой нет конкретного упорядочивания, поэтому amgettbitmap не принимает аргумент direction. (Операторы упорядочивания также никогда не будут предоставлены для такого сканирования.) Кроме того, amgettbitmap не может сканировать только по индексу, поскольку нет никакого способа вернуть содержимое индексных кортежей. В-третьих, amgettbitmap не гарантирует никакой блокировки для возвращаемых кортежей, и последствия этого описываются в разделе Особенности блокировки индексов.

Обратите внимание, что методу доступа разрешается реализовывать только amgettbitmap, но не amgettuple, и наоборот, если его внутренняя реализация не совместима с какой-то из этих API.


Особенности блокировки индексов

Индексные методы доступа должны обрабатывать параллельные изменения индекса несколькими процессами. Основная система QHB получает блокировку AccessShareLock на индекс во время его сканирования и RowExclusiveLock при изменении индекса (включая обычную операцию VACUUM). Поскольку эти типы блокировок не конфликтуют, метод доступа отвечает за обработку любых мелкомодульных блокировок, которые могут ему понадобиться. Блокировка целого индекса в режиме ACCESS EXCLUSIVE будет устанавливаться только во время его создания, уничтожения или операции REINDEX (вместо этого устанавливается SHARE UPDATE EXCLUSIVE вместе с CONCURRENTLY).

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

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

  • Новая запись в куче создается перед созданием для нее записи в индексе. (Поэтому параллельное сканирование индекса, скорее всего, не сможет увидеть запись кучи. Это нормально, поскольку читатель индекса в любом случае не заинтересован в получении незафиксированной строки. Но см. раздел Проверки уникальности индекса.)

  • Когда запись кучи удаляется (командой VACUUM), сначала должны быть удалены все ее записи в индексах.

  • Сканирование индекса должно закрепить страницу индекса, содержащую элемент, возвращенный последним вызовом функции amgettuple, а функция ambulkdelete не должна удалять записи со страниц, закрепленных другими обслуживающими процессами. Необходимость этого правила объясняется ниже.

Без третьего правила читатель индекса может увидеть запись индекса непосредственно перед ее удалением VACUUM, а затем перейти к соответствующей записи кучи после того, как ее удалит VACUUM. Это не создает никаких серьезных проблем, если этот номер элемента еще не используется, когда читатель к нему переходит, поскольку пустой слот элемента будет игнорироваться функцией heap_fetch(). Но что делать, если третий обслуживающий процесс уже повторно использовал этот слот элемента для чего-то другого? При использовании снимка, совместимого с MVCC, это не проблема, потому что новое содержимое слота наверняка будет слишком новым, чтобы пройти тест видимости для этого снимка. Однако со снимком, не совместимым с MVCC (например SnapshotAny), возможна ситуация, когда будет принята и возвращена строка, в действительности не соответствующая ключам сканирования. От подобного сценария можно защититься, всегда требуя перепроверки ключей сканирования для строки кучи, но это слишком затратно. Вместо этого используется закрепление страницы индекса в качестве посредника, указывающего, что читатель все еще может быть «в пути» от записи индекса к соответствующей записи кучи. Блокировка функции ambulkdelete на таких закрепленных страницах гарантирует, что VACUUM не сможет удалить запись кучи раньше, чем читатель закончит работать с ней. Это решение не требует больших затрат времени выполнения и добавляет издержки на блокировку только в редких случаях, когда на самом деле существует конфликт.

Такое решение требует, чтобы сканирования индексов были «синхронными»: нам нужно извлекать каждый кортеж кучи сразу после сканирования соответствующей записи индекса. Это дорого по ряду причин. «Асинхронное» сканирование, при котором мы собираем из индекса много TID, a к кортежам кучи обращаемся только через некоторое время после этого, требует гораздо меньше издержек на блокировки и позволяет реализовать более эффективный сценарий обращения к куче. В соответствии с приведенным выше анализом, синхронный подход необходимо использовать для снимков, несовместимых с MVCC, а асинхронное сканирование применимо для запросов, использующих снимок MVCC.

При сканировании индекса функцией amgetbitmap метод доступа не закрепляет страницы индекса ни для каких возвращаемых кортежей. Поэтому такие сканирования можно безопасно использовать только со снимками, совместимыми с MVCC.

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


Проверки уникальности индекса

QHB реализует ограничения уникальности SQL, используя уникальные индексы, то есть индексы, не допускающие нескольких записей с одинаковыми ключами. Метод доступа, поддерживающий эту особенность, устанавливает флаг amcanunique в true. (В настоящее время его поддерживает только B-дерево.) Столбцы, перечисленные в предложении INCLUDE, при обеспечении уникальности не учитываются.

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

  • Если конфликтующая допустимая строка была удалена текущей транзакцией, это нормально. (В частности, поскольку UPDATE всегда удаляет старую версию строки перед добавлением новой, это позволит выполнить для строки операцию UPDATE без изменения ключа.)

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

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

Более того, непосредственно перед сообщением о нарушении уникальности согласно вышеуказанным правилам метод доступа должен повторно проверить работоспособность добавляемой строки. Если она признана неиспользуемой, то ни о каком нарушении сообщать не нужно. (Эта ситуация не может возникнуть во время обычного сценария добавления строки, только что созданной текущей транзакцией. Однако это может произойти при выполнении CREATE UNIQUE INDEX CONCURRENTLY.)

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

Если ограничение уникальности является откладываемым, возникает дополнительная сложность: нужно иметь возможность добавлять запись индекса для новой строки, но откладывать любую ошибку нарушения уникальности до конца оператора или даже позже. Чтобы избежать ненужных повторных поисков индекса, индексный метод доступа должен выполнить предварительную проверку уникальности во время изначального добавления строки. Если проверка показывает, что конфликтующего активного кортежа нет, дальнейшие проверки не проводятся. В противном случае повторная проверка планируется на время, когда придет время применить ограничение. Если во время повторной проверки и добавленный кортеж, и какой-то другой кортеж с тем же ключом являются активными, то необходимо сообщить об ошибке. (Обратите внимание, что в данном случае «активный» фактически означает «все кортежи в цепочке HOT записи индекса активны».) Чтобы реализовать это, в функцию aminsert передается параметр checkUnique, имеющий одно из следующих значений:

  • UNIQUE_CHECK_NO указывает, что проверка уникальности не должна выполняться (это не уникальный индекс).

  • UNIQUE_CHECK_YES указывает, что это неоткладываемый уникальный индекс, и проверка уникальности должна быть выполнена немедленно, как описано выше.

  • UNIQUE_CHECK_PARTIAL указывает, что ограничение уникальности является откладываемым. QHB будет использовать этот режим для добавления каждой строки в индекс. Метод доступа должен допускать повторяющиеся записи в индексе и сообщать о любых потенциальных дубликатах, возвращая false из aminsert. Для каждой строки, для которой возвращается false, будет запланирована отложенная повторная проверка.
    Метод доступа должен выявить все строки, которые могут нарушить ограничение уникальности, но ложноположительные результаты не являются ошибкой. Это позволяет выполнить проверку, не дожидаясь завершения других транзакций; конфликты, выявленные на данном этапе, не рассматриваются как ошибки и будут перепроверены позже, когда они могут уже исчезнуть.

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


Функции оценки стоимости индекса

Функция amcostestimate предоставляет информацию, описывающую возможное сканирование индекса, включая предложения WHERE и ORDER BY, которые были определены как применимые с индексом. Она должна возвращать оценку стоимости обращения к индексу и избирательность предложений WHERE (то есть доли строк родительской таблицы, которые будут получены во время сканирования индекса). Для простых случаев почти всю работа оценщика стоимости можно выполнить путем вызова стандартных подпрограмм оптимизатора; смысл наличия функции amcostestimate заключается в том, чтобы позволить индексному методу доступа предоставлять знания, специфичные для типа индекса, если это может улучшить стандартные оценки.

Каждая функция amcostestimate должна иметь следующую сигнатуру:

void
amcostestimate (PlannerInfo *root,
                IndexPath *path,
                double loop_count,
                Cost *indexStartupCost,
                Cost *indexTotalCost,
                Selectivity *indexSelectivity,
                double *indexCorrelation,
                double *indexPages);

Первые три параметра являются входными:

root
Информация планировщика об обрабатываемом запросе.

path
Рассматриваемый путь доступа к индексу. Все поля, кроме значений стоимости и избирательности, являются действительными.

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

Последние пять параметров являются указателями на выходные данные:

*indexStartupCost
Стоимость обработки запуска индекса.

*indexTotalCost
Общая стоимость обработки индекса.

*indexSelectivity
Индекс избирательности.

*indexCorrelation
Коэффициент корреляции между порядком сканирования индекса и порядком записей в нижележащей таблицы.

*indexPages
Количество листовых страниц индекса.

Обратите внимание, что функции оценки затрат необходимо писать на C/RUST, а не на SQL или любом другом доступном процедурном языке, поскольку они должны обращаться к внутренним структурам данных планировщика/оптимизатора.

Стоимость обращения к индексу следует рассчитывать с помощью следующих параметров:

последовательная выборка дисковых блоков имеет стоимость seq_page_cost, произвольная выборка — random_page_cost, а стоимость обработки одной строки индекса обычно принимается как cpu_index_tuple_cost. Кроме того, для всех операторов сравнения, вызываемых во время обработки индекса (особенно за вычисление самих условий индекса), следует взимать цену в виде соответствующего количества значений cpu_operator_cost.

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

«Стоимость запуска» является частью общей стоимости сканирования, которая должна быть израсходована до начала извлечения первой строки. Для большинства индексов она может считаться нулевой, но для типа индекса с высокой стоимостью запуска это значение может быть ненулевым.

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

В indexCorrelation следует установить значение корреляции (в диапазоне от -1.0 до 1.0) между порядком записей в индексе и таблице. Это значение используется для корректировки оценки стоимости извлечения строк из родительской таблицы.

В indexPages следует установить количество листовых страниц. Это значение используется для оценки количества рабочих процессов для параллельного сканирования индекса.

Когда loop_count больше единицы, возвращаемые числа должны быть средними значениями, ожидаемыми для любого одного сканирования индекса.

Оценка стоимости

Типичный оценщик стоимости будет действовать следующим образом:

  1. Оценить и вернуть долю строк родительской таблицы, которые будут посещены на основе заданных условий соответствия. При отсутствии каких-либо специфических знаний для данного типа индекса следует использовать стандартную функцию оптимизатора clauselist_selectivity():

    *indexSelectivity = clauselist_selectivity(root, path->indexquals,
                                               path->indexinfo->rel->relid,
                                               JOIN_INNER, NULL);
    
  2. Оценить количество строк индекса, которые будут посещены во время сканирования. Для многих типов индексов это то же самое, что значение indexSelectivity, умноженное на количество строк в индексе, но оно может быть и больше. (Обратите внимание, что размер индекса в страницах и строках можно посмотреть в структуре path->indexinfo).

  3. Оценить количество страниц индекса, которые будут получены во время сканирования. Это может быть просто значение indexSelectivity, умноженное на размер индекса в страницах.

  4. Вычислить стоимость обращения к индексу. Универсальный оценщик может сделать это так:

    /*
     * Обычно предполагается, что страницы индекса будут считываться последовательно,
     * поэтому их стоимость будет seq_page_cost, а не random_page_cost за страницу.
     * Также мы назначаем цену за вычисление условий индекса для каждой его строки.
     * Предполагается, что все стоимости пропорционально увеличиваются при сканировании.
     */
    cost_qual_eval(&index_qual_cost, path->indexquals, root);
    *indexStartupCost = index_qual_cost.startup;
    *indexTotalCost = seq_page_cost * numIndexPages +
        (cpu_index_tuple_cost + index_qual_cost.per_tuple) * numIndexTuples;
    

    Однако все вышеизложенное не учитывает амортизацию чтений индекса при повторном его сканировании.

  5. Оценить коэффициент корреляции. Для простого упорядоченного индекса по одному полю это значение можно получить из pg_statistic. Если корреляция неизвестна, то консервативная оценка равна нулю (корреляция отсутствует).