Trino это распределенный SQL-движок для больших данных. В данной статье мы рассмотрим устройство оптимизатора запросов Trino: реляционное представление операторов, интерфейс оптимизатора, алгоритм применения трансформаций.
Trino это распределенный open-source SQL-движок для больших данных. Trino имеет массивно-параллельную архитектуру и содержит широкий набор коннекторов к различным системам, включая реляционные и NoSQL СУБД, и экосистему Hadoop. Это позволяет Trino выполнять сложные федеративные запросы к нескольким системам.
При получении запроса, Trino должен принять решение, какие вычисления и в каком порядке выполнить самостоятельно, а какие отправить в коннекторы (pushdown). Для этого Trino использует сложный rule-based оптимизатор запросов, архитектуру которого мы рассмотрим в данной статье.
Оптимизаторы SQL-запросов наиболее часто используют одну из двух моделей промежуточного представления:
Оптимизатор Trino так же использует реляционное дерево в качестве промежуточного представления:
Рассмотрим следующий запрос:
После преобразования в реляционное дерево, мы получим следующий логический план:
Для последующих трансформаций Trino использует исключительно реляционное представление. Процесс оптимизации разбит на несколько десятков фаз. Каждая фаза реализует интерфейс PlanOptimizer, который принимает текущее реляционное дерево, и возвращает трансформированное.
Некоторые фазы реализуют паттерн visitor, преобразуя дерево в процессе его направленного обхода. Например, фаза PredicatePushDown стремится переместить фильтры вниз по реляционному дереву. Однако большинство фаз представляют собой rule-based трансформации.
Rule-based оптимизация в Trino реализована с помощью итеративного драйвера IterativeOptimizer. Сначала драйвер записывает текущий план в специальную структуру данных Memo, в которой с каждым реляционным узлом сопоставляется вспомогательная информация, такая как входящие ссылки от других узлов.
Далее `IterativeOptimizer` проходит по дереву реляционных операторов сверху вниз. Для каждого оператора мы определяем набор подходящих правил трансформации на основе их паттернов, после чего применяем правила один за другим. Получив новый узел, мы повторяем процесс оптимизации для дочерних узлов. Изменение дочернего узла может открыть новые оптимизации для текущего узла, которые были недоступны ранее. Поэтому процесс top-down оптимизации повторяется в цикле до тех пор, пока не перестают изменяться дочерние узлы.
Данный алгоритм является эвристическим и не может гарантировать оптимальность плана. Он схож с алгоритмом `HepPlanner` Apache Calcite и фазой нормализации в CockroachDB, которые мы рассматривали в статье про rule-based оптимизацию.
Любопытно, что в Trino частично присутствует инфраструктура для полноценной cost-based оптимизации:
Это показывает, что на разных этапах создания Trino, разработчики держали в уме возможность внедрения полноценной cost-based оптимизации. Эта идея так же упоминается в оригинальной публикации Presto, форком которого является Trino:
We are in the process of enhancing the optimizer to perform a more comprehensive exploration of the search space using a cost-based evaluation of plans based on the techniques introduced by the Cascades framework.
Однако, Cascades в Trino по-прежнему не реализован, а cost-based принципы применены только в отдельных правилах трансформации. Например, правило ReorderJoins генерирует альтернативные порядки Join-ов, и выбирает наиболее дешевый на основе стоимости.
Расширение возможностей cost-based оптимизации остается одной из ключевых задач сообщества Trino, так как она может существенно улучшить производительность системы в целом. Наша команда занимается исследованием и прототипированием данного подхода.
Интерфейс правила Rule является универсальным, и позволяет закодировать произвольную трансформацию. Trino содержит около 200 правил, которые решают широкий спектр задач. Наиболее важные группы задач:
Все применяемые шаги и правила оптимизации в Trino определены в классе PlanOptimizers.
Оптимизатор Trino использует rule-based оптимизацию. Промежуточным представлением является дерево реляционных операторов, а большинство правил трансформации направлено на нормализацию или упрощение.
Trino использует итеративный планировщик, который безусловно применяет правила к текущему дереву сверху вниз. Итеративный оптимизатор не использует cost-based оптимизацию. Тем не менее отдельные правила могут генерировать несколько планов, и выбирать наиболее дешевый из них. Пример такого правила является правило `ReorderJoins`, которое находит оптимальный порядок Join-ов. Разработка полноценного cost-based остается одной из ключевых задач сообщества Trino.
В следующих статьях мы более подробно рассмотрим, как устроено планирование порядка Join-ов в Trino, а так же каким образом происходит pushdown вычислений в источники данных.