SQLПрограммированиеСтарший инженер баз данных

При каком точном пороге на этапе оптимизации запроса планировщик **PostgreSQL** определяет, что узел **Gather Merge** предпочтительнее узла **Gather** для параллельной консолідации результата, и какое конкретное свойство базовых узлов сканирования диктует этот выбор?

Проходите собеседования с ИИ помощником Hintsage

Ответ на вопрос.

Введение параллельных возможностей запроса в PostgreSQL 9.6 принесло узел Gather, чтобы объединять результаты от фоновый работ в основной процесс. Однако стандартный узел Gather уничтожает любой порядок кортежей, произведенный параллельными работниками, что требует дорогостоящего финального шага Sort в ведущем процессе для восстановления последовательности. Чтобы устранить эту избыточность при обработке по существу упорядоченных потоков данных, версия 10 ввела узел Gather Merge, который выполняет k-стороннее объединение отсортированных входных данных от работников, минуя необходимость в материализации и сортировке на стороне ведущего.

Планировщик решает вставить Gather Merge исключительно тогда, когда параллельный подсчет гарантирует выходные данные, упорядоченные в соответствии с необходимым свойством, обычно генерируемым Index Scans или Merge Joins, которые сохраняют последовательность кортежей. Если подсчет теряет порядок через операции, такие как Hash Joins или неупорядоченные агрегирования, Gather Merge становится неприемлемым, заставляя оптимизатор выбирать между Gather, за которым следует дорогостоящий Sort, или полностью отказаться от параллелизма, чтобы сохранить порядок с помощью одного процесса.

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

Ситуация из жизни

Наша команда управляла платформой аналитики временных рядов, хранящей данные датчиков в таблице PostgreSQL, разбитой по часам, содержащей более 2 миллиардов строк. Критическая панель управления требовала отображения последних 1000 чтений по всем разделам, отсортированных по timestamp в порядке убывания, с латентностью менее 500 миллисекунд. Изначальный однопоточный план запроса не соответствовал этим требованиям, создавая узкое место в пользовательском опыте во время пиковых аналитических нагрузок.

Однопроцессный Index Scan: Мы изначально рассматривали возможность использования обратного Index Scan на каждом разделе, за которым следовал узел Limit, выполненный последовательно. Этот подход предлагал простоту реализации и детерминированный порядок без сложной параллельной координации. Однако он не смог насытить полосу пропускания ввода-вывода нашего массива NVMe и постоянно превышал 2 секунды во время пиковых нагрузок, что делало его неприемлемым для обновлений панели в реальном времени.

Параллельный Seq Scan с Gather и Sort: Второй подход заключался в включении max_parallel_workers_per_gather и использовании Parallel Seq Scan со стандартным узлом Gather, собирая все строки в ведущем процессе для финальной Sort и Limit. Это использовало параллелизм CPU и значительно улучшило пропускную способность сканирования. Тем не менее, это привело к тому, что ведущий процесс выделил более 4 ГБ work_mem для сортировки миллионов строк, часто вызывая переполнения диска и ошибки OutOfMemory на нашем ограниченном ведущем узле, что компрометировало стабильность системы.

Параллельный Index Scan с Gather Merge: В конечном итоге мы выбрали план, в котором работники выполняли Parallel Index Scans в порядке убывания по timestamp, подавая данные в узел Gather Merge. Работники сканировали листовые страницы индекса в требуемой последовательности, потоково передавая отсортированные кортежи в ведущий процесс, который выполнял легкое k-стороннее объединение для извлечения верхних 1000 строк. Эта архитектура исключила необходимость в финальной сортировке на стороне ведущего, значительно снизив нагрузку на память, сохраняя при этом эффективность потокового выполнения.

Мы выбрали подход Gather Merge, потому что он уникально удовлетворял как ограничения по латентности, так и по памяти, используя существующую структуру индекса, а не борясь с ней с помощью хешированных операций. Это решение уменьшило объем памяти, занимаемой ведущим процессом, до менее 64 МБ для буферов слияния, и обеспечивало постоянные времена отклика менее 300 мс. Теперь система справляется с пиковыми нагрузками без истощения памяти, что подтверждает архитектурный выбор сохранить порядок при параллельном выполнении.

Что часто упускают кандидаты

Почему размещение узла Hash Aggregate ниже узла Gather Merge заставляет планировщик PostgreSQL либо отклонить план, либо вставить явный шаг Sort, и как это отличается от поведения GroupAggregate?

Hash Aggregate строит неупорядоченную хеш-таблицу для группировки кортежей, что по своей сути уничтожает любой входной порядок, создаваемый базовыми сканами. Поскольку Gather Merge требует строго упорядоченных входных потоков от всех параллельных работников для выполнения своего потокового k-стороннего объединения, неупорядоченный выход от агрегации блокирует его прямое использование. Напротив, GroupAggregate может работать с предварительно отсортированными входными данными и сохранять порядок кортежей, когда ключи GROUP BY совпадают с порядком сортировки, что делает его совместимым с Gather Merge без необходимости в промежуточном шаге сортировки.

Как parallel_tuple_cost GUC влияет на порог, при котором планировщик переключается с плана Gather на план Gather Merge, оценивая стоимость слияния отсортированных потоков от восьми параллельных работников?

parallel_tuple_cost добавляет накладные расходы на процессор на каждую кортеж для передачи строк между параллельными работниками и ведущим процессом. Для Gather Merge эта стоимость немного выше, чем для стандартного узла Gather, из-за дополнительной логики сравнения, необходимой для поддержания кучи слияния. Когда оценочный объем результирующего набора мал, планировщик может предпочесть узел Gather, соединенный с недорогим Sort в ведущем процессе, вместо Gather Merge, потому что совокупные накладные расходы восьми потоков слияния могут превысить стоимость сортировки небольшой выборки кортежей в центре.

Какое конкретное ограничение возникает при использовании DECLARE CURSOR с опцией SCROLL над планом запроса, содержащим узел Gather Merge, и почему исполнитель может молча материализовать весь набор результатов, несмотря на потоковую природу слияния?

SCROLL курсоры требуют возможности двигаться назад по результатам, что необходимо для материализации строк в work_mem или переполнения диска для поддержки обратной выборки. Хотя Gather Merge эффективно производит потоковый, упорядоченный выход, опция SCROLL заставляет исполнителя вставить узел Materialize над Gather Merge, чтобы буферизовать строки для потенциального обратного обхода. Эта материализация потребляет память, пропорциональную размеру результирующего набора, по сути, отрицая преимущества эффективности памяти потоковой стратегии слияния и потенциально вызывая переполнения диска, которые были бы избегнуты, если бы изначально был выбран Gather Merge.