COMMUTATORNEGATORRESTRICTJOINHASHESMERGESОпределение оператора Digital Q.DataBase может включать несколько необязательных предложений, которые сообщают системе полезные сведения о том, как оператор ведёт себя. Эти предложения следует предоставлять всегда, когда это уместно, потому что они могут обеспечить значительное ускорение выполнения запросов, использующих оператор. Но если вы их предоставляете, вы должны быть уверены, что они верны! Неправильное использование предложения оптимизации может привести к медленным запросам, слегка неверным результатам или другим Плохим Вещам. Вы всегда можете опустить предложение оптимизации, если не уверены в нём; единственное последствие — запросы могут выполняться медленнее, чем могли бы.
Дополнительные предложения оптимизации могут быть добавлены в будущих версиях Digital Q.DataBase. Описанные здесь — все те, которые понимает выпуск 17.4.
Также возможно прикрепить функцию поддержки планировщика к функции, лежащей в основе оператора, предоставив ещё один способ сообщить системе о поведении оператора. См. Раздел 5.1.11 для получения дополнительной информации.
COMMUTATOR #
Предложение COMMUTATOR, если оно предоставлено, задаёт оператор, который является
коммутатором определяемого оператора. Мы говорим, что оператор A является
коммутатором оператора B, если (x A y) равно (y B x) для всех возможных входных
значений x, y. Заметьте, что B также является коммутатором A. Например,
операторы < и > для определённого типа данных обычно являются коммутаторами друг друга,
а оператор + обычно коммутативен сам с собой.
Но оператор - обычно ни с чем не коммутативен.
Тип левого операнда коммутативного оператора такой же, как тип
правого операнда его коммутатора, и наоборот. Так что имя
оператора-коммутатора — это всё, что Digital Q.DataBase
нужно, чтобы найти коммутатор, и это всё, что нужно
предоставить в предложении COMMUTATOR.
Критически важно предоставлять информацию о коммутаторе для операторов, которые
будут использоваться в индексах и условиях соединения, потому что это позволяет
оптимизатору запросов «переворачивать» такие условия в формы,
необходимые для разных типов планов. Например, рассмотрим запрос с
условием WHERE наподобие tab1.x = tab2.y, где tab1.x
и tab2.y имеют пользовательский тип, и предположим, что
tab2.y индексирован. Оптимизатор не может сгенерировать
индексное сканирование, если не может определить, как перевернуть условие в
tab2.y = tab1.x, потому что механизм индексного сканирования ожидает
увидеть индексированный столбец слева от оператора, который ему дан.
Digital Q.DataBase не будет просто
предполагать, что это допустимое преобразование — создатель
оператора = должен указать, что оно допустимо, пометив оператор
информацией о коммутаторе.
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,
предоставив другую оценку по умолчанию.)
Существуют дополнительные функции оценки избирательности, предназначенные для геометрических
операторов в src/backend/utils/adt/geo_selfuncs.c: areasel, positionsel,
и contsel. На момент написания это всего лишь заглушки, но вы можете
захотеть использовать их (или, что ещё лучше, улучшить их) в любом случае.
JOIN #
Предложение JOIN, если оно предоставлено, задаёт функцию оценки
избирательности соединения для оператора. (Заметьте, что это имя
функции, а не имя оператора.) Предложения JOIN имеют смысл только для
бинарных операторов, которые возвращают boolean. Идея, стоящая за оценщиком избирательности
соединения, — угадать, какая доля строк в
паре таблиц будет удовлетворять условию WHERE вида:
table1.column1 OP table2.column2
для текущего оператора. Как и с предложением RESTRICT, это очень существенно помогает
оптимизатору, позволяя ему выяснить, какая
из нескольких возможных последовательностей соединений, вероятно, потребует наименьшей работы.
Как и ранее, эта глава не будет пытаться объяснить, как писать функцию оценки избирательности соединения, а просто предложит использовать один из стандартных оценщиков, если он применим:
eqjoinsel для = |
neqjoinsel для <> |
scalarltjoinsel для < |
scalarlejoinsel для <= |
scalargtjoinsel для > |
scalargejoinsel для >= |
matchingjoinsel для общих операторов соответствия |
areajoinsel для сравнений на основе 2D-площади |
positionjoinsel для сравнений на основе 2D-позиции |
contjoinsel для сравнений на основе 2D-содержания |
HASHES #
Предложение HASHES, если присутствует, сообщает системе, что
допустимо использовать метод хеш-соединения для соединения на основе этого
оператора. HASHES имеет смысл только для бинарного оператора, который
возвращает boolean, и на практике оператор должен представлять
равенство для некоторого типа данных или пары типов данных.
Предположение, лежащее в основе хеш-соединения, заключается в том, что оператор соединения может
возвращать true только для пар левых и правых значений, которые хешируются в один и тот же
хеш-код. Если два значения попадают в разные хеш-бакеты, соединение
никогда не будет сравнивать их вообще, неявно предполагая, что
результат оператора соединения должен быть false. Поэтому никогда не имеет смысла
указывать HASHES для операторов, которые не представляют
некоторую форму равенства. В большинстве случаев поддерживать
хеширование практично только для операторов, которые принимают один и тот же тип данных с обеих сторон.
Однако иногда возможно разработать совместимые хеш-функции
для двух или более типов данных; то есть функции, которые будут генерировать
одинаковые хеш-коды для «равных» значений, даже если значения
имеют разные представления. Например, довольно просто
обеспечить это свойство при хешировании целых чисел разной ширины.
Чтобы быть помеченным как HASHES, оператор соединения должен присутствовать
в семействе операторов хеш-индекса. Это не проверяется при создании
оператора, поскольку, конечно, ссылающееся семейство операторов ещё не может
существовать. Но попытки использовать оператор в хеш-соединениях будут терпеть неудачу
во время выполнения, если такое семейство операторов не существует. Системе нужно
семейство операторов, чтобы найти специфичные для типа данных хеш-функции для
входного типа данных (типов данных) оператора. Конечно, вы также должны создать подходящие
хеш-функции, прежде чем сможете создать семейство операторов.
Следует проявлять осторожность при подготовке хеш-функции, потому что есть
машинно-зависимые способы, которыми она может не выполнить правильные действия.
Например, если ваш тип данных — это структура, в которой могут быть
неинтересные биты заполнения, вы не можете просто передать всю структуру в
hash_any. (Если только вы не напишете другие операторы и
функции так, чтобы неиспользуемые биты всегда были нулевыми, что является
рекомендуемой стратегией.)
Другой пример — на машинах, соответствующих стандарту IEEE
для чисел с плавающей запятой, отрицательный ноль и положительный ноль — разные
значения (разные битовые шаблоны), но они определены как равные при сравнении.
Если значение с плавающей запятой может содержать отрицательный ноль, то необходимы дополнительные шаги,
чтобы гарантировать, что оно генерирует тот же хеш-значение, что и положительный ноль.
Оператор, поддерживающий хеш-соединение, должен иметь коммутатор (самого себя, если два операнда имеют одинаковый тип данных, или связанный оператор равенства, если они разные), который присутствует в том же семействе операторов. Если это не так, могут возникать ошибки планировщика при использовании оператора. Также хорошей идеей (но не строго обязательной) для семейства операторов хеширования, которое поддерживает несколько типов данных, является предоставление операторов равенства для каждой комбинации типов данных; это позволяет лучше оптимизировать.
Функция, лежащая в основе оператора, поддерживающего хеш-соединение, должна быть помечена как immutable или stable. Если она volatile, система никогда не попытается использовать оператор для хеш-соединения.
Если у оператора, поддерживающего хеш-соединение, есть базовая функция, помеченная
как strict, функция также должна быть полной: то есть она должна возвращать true или
false, никогда null, для любых двух ненулевых входных данных. Если это правило
не соблюдается, хеш-оптимизация операций IN может
генерировать неверные результаты. (Конкретно, IN может возвращать
false там, где правильный ответ согласно стандарту был бы null;
или может выдавать ошибку, жалуясь, что не был подготовлен к
нулевому результату.)
MERGES #
Предложение MERGES, если присутствует, сообщает системе, что
допустимо использовать метод соединения слиянием для соединения на основе этого
оператора. MERGES имеет смысл только для бинарного оператора, который
возвращает boolean, и на практике оператор должен представлять
равенство для некоторого типа данных или пары типов данных.
Соединение слиянием основано на идее сортировки левой и правой таблиц
по порядку и затем их параллельного сканирования. Таким образом, оба типа данных должны
быть способны к полному упорядочиванию, и оператор соединения должен быть таким,
который может успешно выполняться только для пар значений, которые оказываются на
«одном и том же месте»
в порядке сортировки. На практике это означает, что оператор соединения должен
вести себя как равенство. Но можно соединять слиянием два
различных типа данных, если они логически совместимы. Например,
оператор равенства smallint-против-integer
поддерживает соединение слиянием.
Нам нужны только операторы сортировки, которые приведут оба типа данных к
логически совместимой последовательности.
Чтобы быть помеченным как MERGES, оператор соединения должен присутствовать
как член равенства в семействе операторов btree-индекса.
Это не проверяется при создании
оператора, поскольку, конечно, ссылающееся семейство операторов ещё не может
существовать. Но оператор фактически не будет использоваться для соединений слиянием,
пока не будет найдено соответствующее семейство операторов.
Флаг MERGES таким образом действует как подсказка для планировщика, что
стоит поискать соответствующее семейство операторов.
Оператор, поддерживающий соединение слиянием, должен иметь коммутатор (самого себя, если два
операнда имеют одинаковый тип данных, или связанный оператор равенства,
если они разные), который присутствует в том же семействе операторов.
Если это не так, могут возникать ошибки планировщика при использовании оператора.
Также хорошей идеей (но не строго обязательной) для
семейства операторов btree, которое поддерживает несколько типов данных, является предоставление
операторов равенства для каждой комбинации типов данных; это
позволяет лучше оптимизировать.
Функция, лежащая в основе оператора, поддерживающего соединение слиянием, должна быть помечена как IMMUTABLE или STABLE. Если она VOLATILE, система никогда не попытается использовать оператор для соединения слиянием.