Информация по оптимизации операторов

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

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

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



COMMUTATOR

Предложение COMMUTATOR, если имеется, задает оператор, коммутирующий определяемый. Мы говорим, что оператор A является коммутатором оператора B, если (x A y) равно (y B x) для всех возможных входных значений x, y. Обратите внимание, что B также является коммутатором A. Например, операторы < и > для определенного типа данных обычно являются коммутаторами друг друга, а оператор + обычно коммутирует сам с собой. А вот оператор - обычно не коммутирует ни с одним оператором.

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

Очень важно предоставлять информацию о коммутаторах тем операторам, которые будут использоваться в индексах и предложениях соединения, поскольку это позволяет оптимизатору запросов «переворачивать» такое предложение, приводя его к форме, необходимой для различных типов планов. Например, рассмотрим запрос с предложением WHERE вроде tab1.x = tab2.y, где tab1.x и tab2.y имеют пользовательский тип, и предположим, что по столбцу tab2.y есть индекс. Оптимизатор сможет задействовать сканирование по индексу, только если сумеет определить, как перевернуть это предложение, превратив его в tab2.y = tab1.x, поскольку механизм сканирования по индексу ожидает увидеть индексированный столбец слева от переданного ему оператора. QHB не будет по умолчанию считать, что такое преобразование возможно — создатель оператора = должен это указать, добавив в оператор информацию о коммутаторе.

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

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

  • Другой, более простой способ — просто включить предложения COMMUTATOR в оба определения. Когда QHB обрабатывает первое определение и понимает, что COMMUTATOR ссылается на несуществующий оператор, система сделает фиктивную запись для этого оператора в системном каталоге. Эта фиктивная запись будет содержать актуальные данные только для имени оператора, левого и правого типов операндов и типа результата, поскольку это все, что QHB может выяснить на данный момент. Запись первого оператора будет связана с этой фиктивной записью. Позже, когда вы определите второй оператор, система обновит фиктивную запись дополнительной информацией из второго определения. Если вы попытаетесь использовать фиктивный оператор до его заполнения, то просто получите сообщение об ошибке.



NEGATOR

Предложение NEGATOR, если имеется, задает оператор, обратный определяемому. Мы говорим, что оператор A является обратным к оператору B, если оба возвращают логические результаты и (x A y) равно NOT (x B y) для всех возможных входных значений x, y. Обратите внимание, что B также является обратным к A. Например, < и >= являются парой обратных друг к другу операторов для большинства типов данных. Никакой оператор никогда не может быть обратным себе самому.

В отличие от коммутаторов, пару унарных операторов вполне можно пометить как обратные друг к другу; это будет означать, что (A x) равно NOT (B x) для всех x (и аналогично для правых унарных операторов).

У обратного оператора должны быть те же типы левого и/или правого операнда, что и у определяемого оператор, поэтому, как и в случае с COMMUTATOR, в предложении NEGATOR следует указывать только имя оператора.

Предоставление обратного оператора очень полезно для оптимизатора запросов, поскольку тот позволяет упростить выражения типа NOT (x >= y) до x < y. Это происходит чаще, чем можно подумать, так как операции NOT могут добавляться вследствие других преобразований.

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



RESTRICT

Предложение RESTRICT, если имеется, задает функцию оценки избирательности ограничения для оператора. (Обратите внимание, что это имя функции, а не оператора.) Предложения RESTRICT имеют смысл только для бинарных операторов, возвращающих boolean. Идея оценки избирательности ограничения состоит в том, чтобы примерно определить, какая доля строк в таблице будет удовлетворять условию предложения WHERE вида:

column OP constant

для текущего оператора и определенного значения константы. Это помогает оптимизатору, давая ему некоторое представление о том, сколько строк будет исключено предложениями WHERE такого вида. (А что произойдет, если константа будет слева, спросите вы? Ну, как раз для таких случаев и предназначен COMMUTATOR...)

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

eqsel для =
neqsel для <>
scalarltsel для <
scalarlesel для <=
scalargtsel для >
scalargesel для >=

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

Для сравнения типов данных, которые можно каким-либо разумным способом преобразовать в числовые скалярные значения для сравнения диапазонов, можно использовать функции scalarltsel, scalarlesel, scalargtsel иscalargesel. По возможности, добавьте свой тип данных в число типов, которые понимает функция convert_to_scalar() в src/backend/utils/adt/selfuncs.c. (В конечном итоге эту функцию следует заменить функциями для каждого типа данных, указанного в столбце системного каталога pg_type, но пока этого не произошло.) Если вы этого не сделаете, все будет работать, но оценки оптимизатора будут не так хороши, как могли бы быть.

Еще одна полезная встроенная функция оценки избирательности — matchingsel, которая будет работать практически со всеми бинарными операторами, если для их входных типов данных собирается стандартная статистика MCV и/или строится гистограмма. Ее оценка по умолчанию в два раза больше оценки по умолчанию, выдаваемой eqsel, благодаря чему эта функция лучше всего подходит для операторов сравнения, несколько менее строгих, чем оператор равенства. (Либо можно вызвать нижележащую функцию generic_restriction_selectivity, предоставив ей другую оценку по умолчанию.)



JOIN

Предложение JOIN, если имеется, задает функцию оценки избирательности соединения для оператора. (Обратите внимание, что это имя функции, а не оператора.) Предложения JOIN имеют смысл только для бинарных операторов, возвращающих boolean. Идея оценки избирательности соединения состоит в том, чтобы примерно определить, какая доля строк в паре таблиц будет удовлетворять условию предложения WHERE вида:

ON table1.column1 OP table2.column2

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

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

eqjoinsel для =
neqjoinsel для <>
scalarltjoinsel для <
scalarlejoinsel для <=
scalargtjoinsel для >
scalargejoinsel для >=
matchingjoinsel для типовых операторов сопоставления
areajoinsel для сравнений областей в плоскости
positionjoinsel для сравнений позиций в плоскости
contjoinsel для сравнений включения в плоскости



HASHES

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

Соединение по хешу основано на том, что оператор соединения может возвращать true только для тех пар значений слева и справа, которые хешируют один и тот же хеш-код. Если два значения помещаются в разные хеш-блоки, объединение вообще никогда их не сравнит, неявно предполагая, что результат оператора соединения должен быть false. Поэтому нет смысла указывать HASHES для операторов, которые не выражают некоторую форму равенства. В большинстве случаев практично поддерживать хеширование только для тех операторов, которые принимают одинаковый тип данных с обеих сторон. Однако иногда возможно разработать хеш- функции, совместимые с двумя и более типами данных, то есть функции, которые будут генерировать одинаковые хеш-коды для «равных» значений, даже если эти значения имеют разные представления. Например, довольно просто реализовать это свойство при хешировании целых чисел различной ширины.

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

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

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

Примечание
Функция, реализующая оператор соединения по хешу, должна быть помечена как постоянная (IMMUTABLE) или стабильная (STABLE). Если эта функция изменчивая (VOLATILE), система никогда не будет пытаться использовать этот оператор для соединения по хешу.

Примечание
Если оператор соединения по хешу реализован через строгую функцию (STRICT), эта функция также должна быть полной: то есть она должна возвращать true или false, но не NULL для любых входных значений не NULL. Если это правило не соблюдается, оптимизация операций IN с хешем может привести к неверным результатам. (В частности, IN может вернуть false, когда правильным ответом, в соответствии со стандартом, должен быть NULL, или выдать ошибку, сообщающую, что он не готов к результату NULL.)



MERGES

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

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

Чтобы получить характеристику MERGES, оператор соединения должен являться членом семейства операторов индекса btree, реализующим равенство. Это не проверяется при создании оператора, поскольку, разумеется, соответствующее семейство операторов еще не может существовать. Но оператор не будет фактически использоваться для соединений слиянием, пока не будет найдено соответствующее семейство операторов. Таким образом, флаг MERGES служит подсказкой для планировщика, что стоит искать подходящее семейство.

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

Примечание
Функция, реализующая оператор соединения слиянием, должна быть помечена как постоянная (IMMUTABLE) или стабильная (STABLE). Если эта функция изменчивая (VOLATILE), система никогда не будет пытаться использовать этот оператор для соединения слиянием.