Skip to content

Всё о GUC по порядку: enable_hashjoin

Автор: Christophe Pettus, All Your GUCs in a Row: enable_hashjoin


Первый из трёх переключателей стратегий соединения, поэтому прежде чем перейти к параметру, — один абзац о том, внутри чего он находится: в PostgreSQL есть ровно три способа соединения двух таблиц, и для каждого соединения в каждом запросе планировщик выбирает один из них. Три переключателя enable_* для соединений — этот, enable_mergejoin и enable_nestloop — позволяют вам убрать один вариант со стола и посмотреть, что планировщик выберет вместо него. То же правило семейства, что и всегда (enable_async_append): диагностические инструменты, а не регулировочные ручки. По умолчанию включён, контекст — user.

Три соединения, кратко



Вложенный цикл (Nested Loop) сканирует внешнее отношение и для каждой строки ищет соответствующие строки во внутреннем отношении — дёшево, когда внешняя сторона мала или внутренняя сторона имеет индекс по ключу соединения, и жестоко, когда обе стороны велики и не индексированы, потому что это O(внешняя × внутренняя). Соединение слиянием (Merge Join) требует, чтобы оба входа были отсортированы по ключу соединения, затем проходит по двум отсортированным потокам синхронно, продвигая тот, который отстаёт, — отлично, когда обе стороны уже отсортированы (индекс обеспечивает порядок бесплатно), но для этого нужна сортировка, а сортировка двух больших несортированных входов дорога. Хеш-соединение (Hash Join) строит хеш-таблицу из одного входа и зондирует её другим; ему совершенно не важен порядок входа, что делает его «рабочей лошадкой» для соединения больших несортированных таблиц по условию равенства.



Хеш-соединение работает в две фазы. Фаза построения (build phase) сканирует меньший вход — планировщик намеренно выбирает меньший, чтобы минимизировать использование памяти, — и строит хеш-таблицу, ключи которой — столбцы соединения. Фаза зондирования (probe phase) проходит по большему входу, хеширует ключ соединения каждой строки и ищет совпадения. Поскольку оба входа сканируются последовательно, индекс по условию соединения не помогает хеш-соединению вообще; именно поэтому вы часто видите, что планировщик выбирает последовательное сканирование под хеш-соединением, даже когда индексы существуют, и это правильно, а не ошибка. Хеш-соединению нужно только условие равенства (нельзя хешировать <) и достаточно памяти, чтобы разместить хеш-таблицу строящейся стороны.



Эта память и есть «загвоздка», и бюджет тот же, что и для хеш-агрегации: work_mem × hash_mem_multiplier (множитель был повышен с 1.0 до 2.0 в PostgreSQL 15). Если хеш-таблица строящейся стороны помещается, соединение выполняется за один проход. Если нет, PostgreSQL разбивает оба входа на пакеты (batches) по ключу соединения и обрабатывает по одной паре пакетов за раз, сбрасывая остальные во временные файлы, — это работает, но ввод-вывод вредит, и планировщик часто предпочтёт соединение слиянием, если хеш не помещается в память.



Что на самом деле делает его отключение



Стоит знать для всех трёх переключателей соединений: enable_hashjoin = off на самом деле не запрещает хеш-соединения. Он добавляет disable_cost — жёстко заданное огромное число, 1e10, — к каждому пути хеш-соединения, поэтому планировщик всё равно использует его, если у него буквально нет другого способа выполнить соединение. Для соединения по равенству он переключится на слияние или вложенный цикл, но для соединения, которое другие методы не могут обработать, вы увидите хеш-соединение в вашем плане, несмотря на то, что переключатель выключен. Переключатели «отговаривают», но не запрещают строго.



Симптомы, оправдывающие его переключение



Признак находится в узле Hash Join в EXPLAIN (ANALYZE): Batches: больше 1, что означает, что хеш-таблица строящейся стороны не поместилась в work_mem × hash_mem_multiplier, и соединение было сброшено на диск в несколько пакетов. Сбрасывающееся хеш-соединение часто составляет большую часть времени выполнения медленного запроса, и это сигнал, который отправляет вас к этому параметру.



Когда вы видите это, выполните SET enable_hashjoin = off для сеанса и повторно запустите EXPLAIN (ANALYZE). Планировщик переключается на соединение слиянием (или вложенный цикл), и вы получаете сравнение: если альтернатива быстрее, вы подтвердили, что сбрасывающееся хеш-соединение было проблемой, и теперь вы выбираете реальное исправление. Чаще всего это память — увеличьте work_mem (для каждой роли или сеанса, но никогда слепо для всего кластера, поскольку это бюджет на оператор, умноженный на вашу параллельность) или увеличьте hash_mem_multiplier, чтобы дать хеш-операциям больше пространства. Иногда это индекс, который делает соединение слиянием действительно дешевле, предоставляя предварительно отсортированный вход, или делает вложенный цикл с индексированной внутренней стороной лучшим выбором для маленькой стороны зондирования. И часто более глубокая причина находится выше по течению: хеш-соединение (или любой сюрприз с методом соединения) часто связан с ошибкой оценки количества строк — планировщик построил план для неверной кардинальности, — поэтому соединение — это место, где вы тратите время, но ANALYZE или лучшая статистика — это то, где вы это исправляете. Классическая версия: планировщик занижает оценку количества строк, выбирает хеш-соединение, рассчитанное на гораздо меньшее количество строк, чем приходит на самом деле, и выполняет интенсивный сброс на диск.



Оставление enable_hashjoin = off в postgresql.conf — это обычная ошибка семейства, и притом плохая: хеш-соединение является правильной, незаменимой стратегией для больших несортированных соединений по равенству, и его отключение для всего кластера заставляет каждое такое соединение использовать сортировку для слияния или повторяющиеся сканирования для вложенного цикла. Диагностируйте с помощью переключателя, исправьте реальное ограничение — work_mem, hash_mem_multiplier, индекс или статистику, — и верните его обратно. Зонд говорит вам, что хеш-соединение навредило; он не говорит вам, что нужно жить без хеш-соединений.


Trackbacks

No Trackbacks

Comments

Display comments as Linear | Threaded

No comments

The author does not allow comments to this entry

Add Comment

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
Standard emoticons like :-) and ;-) are converted to images.

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

Form options

Submitted comments will be subject to moderation before being displayed.