Интерфейсные расширения для индексов

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

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



Индексные методы и классы операторов

Таблица pg_am содержит одну строку для каждого индексного метода (внутренне известного как метод доступа). Поддержка обычного доступа к таблицам встроена в QHB, но все индексные методы описаны в pg_am. Можно добавить новый индексный метод доступа, написав необходимый код и затем создав запись в pg_am — но это выходит за рамки этой главы (см. главу Определение интерфейса для индексных методов доступа).

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

Для одного и того же типа данных и индексного метода можно определить несколько классов операторов. Благодаря этому для одного типа данных можно определить несколько вариантов доступа по индексу с различной семантикой. Например, для B-дерева требуется определить порядок сортировки для каждого типа данных, с которым он работает. Для типа комплексных чисел может быть полезно иметь один класс операторов B-дерева, который сортирует данные по абсолютному значению комплексного числа, еще один, который сортирует по вещественной части, и т. д. Как правило, считается, что один из классов операторов будет применяться чаще всего, и его помечают как класс операторов по умолчанию для этого типа данных и индексного метода.

Одно и то же имя класса операторов можно использовать для нескольких различных индексных методов (например, для методов B-дерева и хеш-индекса применяются классы операторов с именем int4_ops), но каждый такой класс является независимым объектом и должен определяться отдельно.



Стратегии индексного метода

Операторам, связанным с классом операторов, присваиваются «номера стратегий», которые служат для идентификации семантики каждого оператора в контексте его класса операторов. Например, B-деревья предписывают строгий порядок ключей, от меньшего к большему, и поэтому в данном контексте представляют интерес операторы типа «меньше» и «больше или равно». Поскольку QHB позволяет пользователю определять операторы, QHB не может посмотреть на имя оператора (например < или >=) и сказать, какое это сравнение. Вместо этого индексный метод определяет набор «стратегий», которые можно рассматривать как обобщенные операторы. Каждый класс операторов определяет, какой фактический оператор соответствует каждой стратегии для конкретного типа данных и интерпретации семантики индекса.

Индексный метод B-дерева определяет пять стратегий, показанных в Таблице 3.

Таблица 3. Стратегии В-дерева

ОперацияНомер стратегии
меньше1
меньше или равно2
равно3
больше или равно4
больше5

Хеш-индексы поддерживают только сравнения на равенство, поэтому они используют только одну стратегию, показанную в Таблице 4.

Таблица 4. Хеш-стратегии

ОперацияНомер стратегии
равно1

Индексы GiST более гибкие: у них вообще нет фиксированного набора стратегий. Вместо этого вспомогательная процедура «согласованности» каждого конкретного класса операторов GiST интерпретирует номера стратегий как ей угодно. Например, некоторые из встроенных классов операторов индекса GiST индексируют двумерные геометрические объекты, реализуя стратегии «R-дерева», показанные в Таблице 5.

Четыре из них являются настоящими двумерными тестами (пересекается с, одинаковы, содержит, содержится в); четыре из них учитывают только абсциссы, а еще четыре проводят те же тесты, только с ординатами.

Таблица 5. Стратегии двумерного R-дерева GiST

ОперацияНомер стратегии
строго слева от1
не простирается правее2
пересекается с3
не простирается левее4
строго справа от5
одно и то же6
содержит7
содержится в8
не простирается выше9
строго ниже10
строго выше11
не простирается ниже12

По гибкости индексы SP-GiST аналогичны индексам GiST: у них нет фиксированного набора стратегий. Вместо этого вспомогательные процедуры каждого класса операторов интерпретируют номера стратегий в соответствии с определением класса операторов. В качестве примера в Таблице 6 приведены номера стратегий, используемые встроенными классами операторов для точек.

Таблица 6. Стратегии SP-GiST для точек

ОперацияНомер стратегии
строго слева от1
строго справа от5
одно и то же6
содержится в8
строго ниже10
строго выше11

Индексы GIN аналогичны индексам GiST и SP-GiST: у них тоже нет фиксированного набора стратегий. Вместо этого вспомогательные процедуры каждого класса операторов интерпретируют номера стратегий в соответствии с определением класса операторов. В качестве примера в Таблице 7 приведены номера стратегий, используемые встроенным классом операторов для массивов.

Таблица 7. Стратегии GIN для массивов

ОперацияНомер стратегии
пересекается с1
содержит2
содержится в3
равно4

Индексы BRIN аналогичны индексам GiST, SP-GiST и GIN: у них тоже нет фиксированного набора стратегий. Вместо этого вспомогательные процедуры каждого класса операторов интерпретируют номера стратегий в соответствии с определением класса операторов. В качестве примера в Таблице 8 приведены номера стратегий, используемые встроенными классами операторов Minmax.

Таблица 8. Стратегии BRIN Minmax

ОперацияНомер стратегии
меньше1
меньше или равно2
равно3
больше или равно4
больше5

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



Вспомогательные процедуры индексного метода

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

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

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

Для B-деревьев требуется вспомогательная функция сравнения и могут предоставляться четыре дополнительные вспомогательные функции по выбору разработчика класса операторов, как показано в Таблице 9. Требования к этим вспомогательным функциям рассматриваются в разделе Вспомогательные функции B-деревьев.

Таблица 9. Вспомогательные функции B-деревьев

ФункцияНомер функции
Сравнивает два ключа и возвращает целое число меньше нуля, ноль или целое число больше нуля, показывающее, что первый ключ меньше, равен или больше второго1
Возвращает адреса вызываемых из C/RUST вспомогательных функций сортировки (необязательная)2
Сравнивает значение теста с базовым значением плюс/минус смещение и возвращает true или false в зависимости от результата сравнения (необязательная)3
Определяет, безопасно ли для индексов, использующих этот класс операторов, применять оптимизацию с дедупликацией, реализованную в btree (необязательная)4
Определяет параметры, специфичные для этого класса операторов (необязательная)5

Для хеш-индексов требуется одна вспомогательная функция, и могут предоставляться еще две дополнительные по выбору разработчика класса операторов, как показано в Таблице 10.

Таблица 10. Вспомогательные функции хеша

ФункцияНомер функции
Вычисляет 32-битное значение хеша для ключа1
Вычисляет 64-битное значение хеша для ключа с заданной 64-битной солью; если соль равна 0, младшие 32 бита результата должны соответствовать значению, которое было бы вычислено функцией 1 (необязательная)2
Определяет параметры, специфичные для этого класса операторов (необязательная)3

Индексы GiST имеют одиннадцать вспомогательных функций, шесть из которых являются необязательными, как показано в Таблице 11. (Дополнительную информацию см. в главе Индексы GiST.)

Таблица 11. Вспомогательные функции GiST

ФункцияОписаниеНомер функции
consistentопределяет, удовлетворяет ли ключ условию запроса1
unionвычисляет объединение набора ключей2
compressвычисляет сжатое представление ключа или индексируемого значения (необязательная)3
decompressвычисляет развернутое представление сжатого ключа (необязательная)4
penaltyвычисляет издержки добавления нового ключа в поддерево с заданным ключом5
picksplitопределяет, какие записи страницы должны быть перемещены на новую страницу, и вычисляет ключи объединения для результирующих страниц6
equalсравнивает два ключа и возвращает true, если они равны7
distanceопределяет расстояние от ключа до значения запроса (необязательная)8
fetchвычисляет исходное представление сжатого ключа для сканирования только по индексу (необязательная)9
optionsопределяет параметры, специфичные для этого класса операторов (необязательная)10
sortsupportпредоставляет компаратор для сортировки, который будет использоваться при быстром построении индекса (необязательная)11

Для индексов SP-GiST имеется шесть вспомогательных функций, одна из которых является необязательной, как показано в Таблице 12. (Дополнительную информацию см. в главе Индексы SP-GiST.)

Таблица 12. Вспомогательные функции SP-GiST

ФункцияОписаниеНомер функции
configпредоставляет основную информацию о классе операторов1
chooseопределяет, как вставить новое значение во внутренний кортеж2
picksplitопределяет, как разделить множество значений3
inner_consistentопределяет, какие внутренние ветви нужно искать для запроса4
leaf_consistentопределяет, удовлетворяет ли ключ условию запроса5
optionsопределяет параметры, специфичные для этого класса операторов (необязательная)6

Индексы GIN имеют семь вспомогательных функций, четыре из которых являются необязательными, как показано в Таблице 13. (Дополнительную информацию см. в главе Индексы GIN.)

Таблица 13. Вспомогательные функции GIN

ФункцияОписаниеНомер функции
compareсравнивает два ключа и возвращает целое число меньше нуля, ноль или целое число больше нуля, показывающее, что первый ключ меньше, равен или больше второго1
extractValueизвлекает ключи из индексируемого значения2
extractQueryизвлекает ключи из условия запроса3
consistentопределяет, соответствует ли значение условию запроса (логический вариант) (необязательна, если присутствует вспомогательная функция 6)4
comparePartialсравнивает частичный ключ из запроса и ключ из индекса и возвращает целое число меньше нуля, ноль или целое число больше нуля, показывающее, что GIN должен игнорировать эту запись индекса, относиться к записи как к соответствующей или остановить сканирование индекса (необязательная)5
triConsistentопределяет, соответствует ли значение условию запроса (троичный вариант) (необязательна, если присутствует вспомогательная функция 4)6
optionsопределяет параметры, специфичные для этого класса операторов (необязательная)7

Индексы BRIN имеют пять основных вспомогательных функций, одна из которых является необязательной, как показано в Таблице 14; для некоторых версий этих основных функций может потребоваться предоставить дополнительные вспомогательные функции. (Дополнительную информацию см. в разделе Расширяемость.)

Таблица 14. Вспомогательные функции BRIN

ФункцияОписаниеНомер функции
opcInfoвозвращает внутреннюю информацию, описывающую сводные данные индексированных столбцов1
add_valueдобавляет новое значение в существующий сводный кортеж индекса2
consistentопределяет, соответствует ли значение условию запроса3
unionвычисляет объединение двух сводных кортежей4
optionsопределяет параметры, специфичные для этого класса операторов (необязательная)5

В отличие от операторов поиска, вспомогательные функции возвращают тот тип данных, который ожидает конкретный индексный метод; например, в случае функции сравнения для B-деревьев, целое число со знаком. Количество и типы аргументов каждой вспомогательной функции также зависят от индексного метода. Для B-дерева и хеша вспомогательные функции сравнения и хеширования принимают те же типы входных данных, что и операторы, включенные в класс операторов, но к большинству вспомогательных функций GiST, SP-GiST, GIN и BRIN это не относится.



Пример

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

  • абсолютное значение меньше (стратегия 1)
  • абсолютное значение меньше или равно (стратегия 2)
  • абсолютное значение равно (стратегия 3)
  • абсолютное значение больше или равно (стратегия 4)
  • абсолютное значение больше (стратегия 5)

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

#define Mag(c)  ((c)->x*(c)->x + (c)->y*(c)->y)

static int
complex_abs_cmp_internal(Complex *a, Complex *b)
{
    double      amag = Mag(a),
                bmag = Mag(b);

    if (amag < bmag)
        return -1;
    if (amag > bmag)
        return 1;
    return 0;
}

Теперь функция «меньше» выглядит так:

PG_FUNCTION_INFO_V1(complex_abs_lt);

Datum
complex_abs_lt(PG_FUNCTION_ARGS)
{
    Complex    *a = (Complex *) PG_GETARG_POINTER(0);
    Complex    *b = (Complex *) PG_GETARG_POINTER(1);

    PG_RETURN_BOOL(complex_abs_cmp_internal(a, b) < 0);
}

Остальные четыре функции отличаются только тем, как они сравнивают результат внутренней функции с нулем.

Далее мы объявляем в SQL функции и операторы на основе этих функций:

CREATE FUNCTION complex_abs_lt(complex, complex) RETURNS bool
    AS 'имя_файла', 'complex_abs_lt'
    LANGUAGE C IMMUTABLE STRICT;

CREATE OPERATOR < (
   leftarg = complex, rightarg = complex, procedure = complex_abs_lt,
   commutator = >, negator = >=,
   restrict = scalarltsel, join = scalarltjoinsel
);

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

Здесь также стоит обратить внимание на следующие моменты:

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

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

  • Мы могли бы дать функции abs_eq имя SQL, полагаясь на то, что QHB отличит ее по типу данных аргумента от любой другой функции SQL с тем же именем. Чтобы упростить пример, мы дали функции одинаковые имена на уровне C и на уровне SQL.

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

CREATE FUNCTION complex_abs_cmp(complex, complex)
    RETURNS integer
    AS 'имя_файла'
    LANGUAGE C IMMUTABLE STRICT;

Теперь, когда у нас есть необходимые операторы и вспомогательная процедура, мы можем наконец создать класс операторов:

CREATE OPERATOR CLASS complex_abs_ops
    DEFAULT FOR TYPE complex USING btree AS
        OPERATOR        1       <,
        OPERATOR        2       <=,
        OPERATOR        3       =,
        OPERATOR        4       >=,
        OPERATOR        5       >,
        FUNCTION        1       complex_abs_cmp(complex, complex);

И готово! Теперь должно быть возможно создавать и использовать индексы B-деревья по столбцам complex.

Мы могли бы сделать записи операторов более подробными, например:

OPERATOR        1       < (complex, complex),

но когда операторы принимают тот же тип данных, для которого мы определяем класс операторов, в этом нет необходимости.

В приведенном выше примере предполагается, что вы хотите сделать этот новый класс операторов классом операторов B-деревьев по умолчанию для типа данных complex. Если вы этого не хотите, просто пропустите слово DEFAULT.



Классы операторов и семейства операторов

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

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

Все операторы и функции в семействе операторов должны иметь совместимую семантику, где требования совместимости устанавливаются индексным методом. Исходя из этого, вы можете спросить, зачем выделять отдельные подмножества семейства как классы операторов; и действительно, для многих целей разделение на классы не имеет значения, и единственной полезной группировкой является семейство. Смысл определения классов операторов состоит в том, что они указывают, какая часть семейства необходима для поддержки какого-либо конкретного индекса. Если существует индекс, использующий класс операторов, то этот класс нельзя удалить, не удалив сам индекс, — но остальные части семейства операторов, а именно другие классы операторов и операторы со слабой связью, удалить можно. Таким образом, класс операторов следует определить так, чтобы он содержал минимальный набор операторов и функций, которые объективно нужны для работы с индексом по определенному типу данных, а затем в качестве слабосвязанных членов семейства операторов можно добавить относящиеся к нему, но не необходимые операторы.

В качестве примера, в QHB есть встроенное семейство операторов B-дерева integer_ops, которое включает классы операторов int8_ops, int4_ops и int2_ops для индексов bigint (int8), integer (int4) и smallint (int2) соответственно. Это семейство также содержит межтиповые операторы сравнения, позволяющие сравнивать любые два из этих типов, так чтобы индекс по одному из этих типов можно было искать, используя значение сравнения другого типа. Такое семейство можно представить следующими определениями:

CREATE OPERATOR FAMILY integer_ops USING btree;

CREATE OPERATOR CLASS int8_ops
DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
  -- стандартные сравнения int8
  OPERATOR 1 <,
  OPERATOR 2 <=,
  OPERATOR 3 =,
  OPERATOR 4 >=,
  OPERATOR 5 >,
  FUNCTION 1 btint8cmp(int8, int8),
  FUNCTION 2 btint8sortsupport(internal),
  FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ;

CREATE OPERATOR CLASS int4_ops
DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
  -- стандартные сравнения int4
  OPERATOR 1 <,
  OPERATOR 2 <=,
  OPERATOR 3 =,
  OPERATOR 4 >=,
  OPERATOR 5 >,
  FUNCTION 1 btint4cmp(int4, int4),
  FUNCTION 2 btint4sortsupport(internal),
  FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ;

CREATE OPERATOR CLASS int2_ops
DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
  -- стандартные сравнения int2
  OPERATOR 1 <,
  OPERATOR 2 <=,
  OPERATOR 3 =,
  OPERATOR 4 >=,
  OPERATOR 5 >,
  FUNCTION 1 btint2cmp(int2, int2),
  FUNCTION 2 btint2sortsupport(internal),
  FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ;

ALTER OPERATOR FAMILY integer_ops USING btree ADD
  -- межтиповые сравнения int8 и int2
  OPERATOR 1 < (int8, int2),
  OPERATOR 2 <= (int8, int2),
  OPERATOR 3 = (int8, int2),
  OPERATOR 4 >= (int8, int2),
  OPERATOR 5 > (int8, int2),
  FUNCTION 1 btint82cmp(int8, int2),

  -- межтиповые сравнения int8 и int4
  OPERATOR 1 < (int8, int4),
  OPERATOR 2 <= (int8, int4),
  OPERATOR 3 = (int8, int4),
  OPERATOR 4 >= (int8, int4),
  OPERATOR 5 > (int8, int4),
  FUNCTION 1 btint84cmp(int8, int4),

  -- межтиповые сравнения int4 и int2
  OPERATOR 1 < (int4, int2),
  OPERATOR 2 <= (int4, int2),
  OPERATOR 3 = (int4, int2),
  OPERATOR 4 >= (int4, int2),
  OPERATOR 5 > (int4, int2),
  FUNCTION 1 btint42cmp(int4, int2),

  -- межтиповые сравнения int4 и int8
  OPERATOR 1 < (int4, int8),
  OPERATOR 2 <= (int4, int8),
  OPERATOR 3 = (int4, int8),
  OPERATOR 4 >= (int4, int8),
  OPERATOR 5 > (int4, int8),
  FUNCTION 1 btint48cmp(int4, int8),

  -- межтиповые сравнения int2 и int8
  OPERATOR 1 < (int2, int8),
  OPERATOR 2 <= (int2, int8),
  OPERATOR 3 = (int2, int8),
  OPERATOR 4 >= (int2, int8),
  OPERATOR 5 > (int2, int8),
  FUNCTION 1 btint28cmp(int2, int8),

  -- межтиповые сравнения int2 и int4
  OPERATOR 1 < (int2, int4),
  OPERATOR 2 <= (int2, int4),
  OPERATOR 3 = (int2, int4),
  OPERATOR 4 >= (int2, int4),
  OPERATOR 5 > (int2, int4),
  FUNCTION 1 btint24cmp(int2, int4),

  -- межтиповые функции in_range
  FUNCTION 3 in_range(int4, int4, int8, boolean, boolean),
  FUNCTION 3 in_range(int4, int4, int2, boolean, boolean),
  FUNCTION 3 in_range(int2, int2, int8, boolean, boolean),
  FUNCTION 3 in_range(int2, int2, int4, boolean, boolean) ;

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

В семействе операторов B-дерева все операторы должны быть совместимы в контексте сортировки, как подробно описано в разделе Поведение классов операторов B-деревьев. Для каждого оператора в семействе должна быть предусмотрена вспомогательная функция с теми же двумя типами входных данных, что и у оператора. Рекомендуется, чтобы семейство было полным, т. е. содержало все операторы для каждой комбинации типов данных. Каждый класс операторов должен включать в себя только однотиповые операторы и вспомогательную функцию для своего типа данных.

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

Индексы GiST, SP-GiST и GIN не имеют явного представления о межтиповых операциях. Набор поддерживаемых операторов определяется только теми операциями, которые могут обрабатывать основные вспомогательные функции данного класса операторов.

В BRIN требования зависят от структуры, предоставляющей классы операторов. Для классов операторов, основанных на minmax, требуется такое же поведение, как и для семейств операторов B-дерева: все операторы в семействе должны поддерживать совместимую сортировку, а приведение не должно изменять соответствующий порядок сортировки.



Системные зависимости от классов операторов

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

В частности, существуют средства SQL, такие как ORDER BY и DISTINCT, которым требуются сравнение и сортировка значений. Чтобы реализовать эту функциональность для определенного пользователем типа данных, QHB ищет класс оператора B-дерева по умолчанию для этого типа данных. Член «равно» этого класса операторов определяет представление системы о равенстве значений для GROUP BY и DISTINCT, а порядок сортировки, налагаемый классом операторов, определяет порядок ORDER BY по умолчанию.

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

Если для типа данных не существует класса операторов по умолчанию, то при попытке использовать вышеперечисленную функциональность SQL с этим типом данных вы получите ошибки вида «не удалось определить оператор упорядочивания».

Возможна сортировка по классу операторов B-дерева, отличному от заданного по умолчанию, если указать, например, в предложении USING оператор «меньше» этого класса:

SELECT * FROM mytable ORDER BY somecol USING ~<~;

Как вариант, указав в USING оператор «больше» этого класса, можно выбрать сортировку по убыванию.

Сравнение массивов пользовательского типа также основывается на семантике, определенной классом операторов B-дерева по умолчанию для этого типа. Если таковой отсутствует, но есть класс операторов хеширования по умолчанию, то поддерживается равенство массивов, но не упорядочивание.

Еще одна особенность SQL, которая требует даже больших знаний о типе данных, — это режим определения рамки RANGE смещение PRECEDING/FOLLOWING для оконных функций (см. подраздел Вызовы оконных функций). Для запроса вида

SELECT sum(x) OVER (ORDER BY x RANGE BETWEEN 5 PRECEDING AND 10 FOLLOWING)
  FROM mytable;

недостаточно знать, как упорядочить по x; база данных также должна понимать, как «вычесть 5» или «прибавить 10» к значению x текущей строки, чтобы определить границы текущей рамки окна. Сравнить результирующие границы со значениями x других строк можно с помощью операторов сравнения, предоставляемых классом операторов B-дерева, который определяет порядок ORDER BY, — но операторы сложения и вычитания не входят в этот класс операторов. Тогда какие операторы следует использовать? Жестко фиксировать выбранные операторы в коде было бы нежелательно, поскольку для разных порядков сортировки (разные классы операторов B-дерева) может потребоваться разное поведение. Поэтому класс операторов B-дерева позволяет указывать вспомогательную функцию in_range, в которой объединены сложение и вычитание, подходящие для порядка сортировки. Он даже способен предоставить более одной такой функции, в случае если в качестве смещения в предложениях RANGE имеет смысл использовать несколько разных типов данных. Если в классе операторов B-деревьев, связанном с указанным для окна предложением ORDER BY, нет соответствующей вспомогательной функции in_range, режим RANGE смещение PRECEDING FOLLOWING не поддерживается.

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



Операторы упорядочивания

Некоторые методы доступа к индексу (в настоящее время только GiST и SP-GiST) поддерживают концепцию операторов упорядочивания. До сих пор мы рассматривали операторы поиска. Оператор поиска — это оператор, для которого можно выполнить поиск по индексу, чтобы найти все строки, удовлетворяющие условию WHERE индексированный_столбец оператор константа. Обратите внимание, что при этом ничего не говорится о порядке, в котором будут возвращены подходящие строки. Оператор упорядочивания, напротив, не ограничивает набор возвращаемых строк, а вместо этого определяет их порядок. С оператором упорядочивания можно, просканировав индекс, получить строки в порядке, представленном условием ORDER BY индексированный_столбец оператор константа. Причина определения операторов упорядочивания таким образом состоит в том, что оно поддерживает поиск ближайшего соседа, если этот оператор измеряет расстояние. Например, запрос типа

SELECT * FROM places ORDER BY location <-> point '(101,456)' LIMIT 10;

находит десять мест, ближайших к заданной целевой точке. Индекс GiST по столбцу location может сделать это эффективно, потому что <-> является оператором упорядочивания.

В то время как операторы поиска должны возвращать логические результаты, операторы упорядочивания обычно возвращают какой-то другой тип, например float или numeric для расстояний. Этот тип обычно не совпадает с типом индексируемых данных. Чтобы избежать жестко зафиксированных в коде предположений о поведении различных типов данных, при определении оператора упорядочивания необходимо указать семейство операторов B-дерева, которое определяет порядок сортировки типа данных результата. Как было сказано в предыдущем подразделе, семейства операторов B-дерева определяют понятие упорядочивания в QHB, так что это естественное представление. Поскольку оператор <-> для точек возвращает float8, его можно указать в команде создания класса операторов так:

OPERATOR 15    <-> (point, point) FOR ORDER BY float_ops

где float_ops — это встроенное семейство операторов, которое включает операции с float8. В этом объявлении говорится, что индекс может возвращать строки в порядке возрастания значений оператора <->.



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

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

Как правило, объявление оператора в качестве члена класса (или семейства) операторов означает, что индексный метод может отобрать именно тот набор строк, который удовлетворяет условию WHERE с этим оператором. Например, запрос:

SELECT * FROM table WHERE integer_column < 4;

может быть точно удовлетворен индексом B-деревом по целочисленному столбцу. Но бывают случаи, когда индекс полезен как примерный указатель на соответствующие строки. Например, если индекс GiST хранит только ограничивающие прямоугольники для геометрических объектов, то он не сможет точно удовлетворить условию WHERE, которое проверяет пересечение между непрямоугольными объектами, такими как многоугольники. Тем не менее этот индекс можно применить для поиска объектов, у которых ограничивающий прямоугольник пересекает ограничивающий прямоугольник целевого объекта, а затем выполнить точную проверку пересечения только для объектов, найденных по индексу. Если этот сценарий применим, такой индекс считается «неполным» для оператора. Поиск по неполному индексу реализуется с помощью индексного метода, возвращающего флаг recheck (перепроверить), когда строка действительно может или не может удовлетворять условию запроса. Затем базовая система проверит извлеченную строку по исходному условию запроса, чтобы понять, следует ли возвращать ее как действительного соответствующую ему. Этот подход работает, если индекс гарантированно возвращает все необходимые строки плюс, возможно, некоторые дополнительные строки, которые можно отбросить, вызвав исходный оператор. Индексные методы, поддерживающие неполный поиск (в настоящее время это GiST, SP-GiST и GIN), позволяют вспомогательным функциям отдельных классов операторов устанавливать флаг перепроверки, так что по сути это свойство класса операторов.

Вернемся к ситуации, когда мы храним в индексе только ограничивающий прямоугольник сложного объекта, например многоугольника. В этом случае нет смысла хранить в элементе индекса весь многоугольник — с тем же успехом там можно хранить и более простой объект типа box. Эта ситуация выражается указанием STORAGE в команде CREATE OPERATOR CLASS, которое мы запишем примерно так:

CREATE OPERATOR CLASS polygon_ops
    DEFAULT FOR TYPE polygon USING gist AS
        ...
        STORAGE box;

В настоящее время только индексные методы GiST, GIN и BRIN поддерживают тип STORAGE, который отличается от типа данных столбца. В GiST преобразованием типов данных при помощи STORAGE должны заниматься вспомогательные процедуры compress и decompress. В GIN тип STORAGE определяет тип значений «ключа», который обычно отличается от типа индексируемого столбца — например, в классе операторов для столбцов с целочисленным массивом ключами могут быть просто целые числа. В GIN за извлечение ключей из индексированных значений отвечают вспомогательные функции extractValue и extractQuery. BRIN аналогичен GIN: тип STORAGE определяет тип хранимых обобщенных значений, а вспомогательные процедуры классов операторов отвечают за их правильную интерпретацию.