36.14. Информация об оптимизации операторов#

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

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

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

Дополнительные оптимизационные предложения могут быть добавлены в будущих версиях Tantor SE. Здесь описаны предложения, которые понимает версия 15.6.

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

36.14.1. COMMUTATOR

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

Тип левого операнда коммутируемого оператора совпадает с типом правого операнда его коммутатора, и наоборот. Таким образом, имя оператора-коммутатора - это все, что Tantor SE должно быть указано для поиска коммутатора, и это все, что должно быть предоставлено в предложение COMMUTATOR.

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

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

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

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

36.14.2. 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 могут быть вставлены в результате других перестановок.

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

36.14.3. 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. На данный момент они являются заглушками, но вы можете все равно использовать их (или, что еще лучше, улучшить).

36.14.4. JOIN

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

table1.column1 OP table2.column2

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

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

eqjoinsel для =
neqjoinsel для <>
scalarltjoinsel для <
scalarlejoinsel for <=
scalargtjoinsel для >
scalargejoinsel для >=
matchingjoinsel для обобщенных операторов сопоставления
areajoinsel для сравнения на основе двумерных областей
positionjoinsel для сравнений на основе позиции в 2D
contjoinsel для сравнений на основе 2D-содержания

36.14.5. HASHES

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

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

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

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

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

Примечание

Функция, лежащая в основе оператора, поддерживающего хеш-соединение, должна быть помечена как неизменяемая (immutable) или стабильная (stable). Если она является изменчивой (volatile), система никогда не будет пытаться использовать этот оператор для соединения по хешу.

Примечание

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

36.14.6. MERGES

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

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

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

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

Примечание

Функция, лежащая в основе оператора, который может быть объединен, должна быть помечена как неизменяемая (immutable) или стабильная (stable). Если она является изменчивой (volatile), система никогда не будет пытаться использовать этот оператор для соединения слиянием.