Learning Platform
Глоссарий Troubleshooting
Урок 07.02 · 18 мин
Продвинутый
PushDownFilterOptimizeProjectionsSimplifyExpressionsCommonSubexprEliminateEliminateLimitConstantFoldingLogicalPlan

Логические правила оптимизации

Logical Optimizer в DataFusion содержит десятки встроенных правил, каждое из которых выполняет одну конкретную трансформацию плана. В этом уроке разберём ключевые правила, их эффект на логический план и как они работают вместе в multi-pass конвейере.

PushDownFilter: спуск предикатов

Самое важное правило оптимизации в любом query engine. PushDownFilter перемещает фильтры (WHERE, HAVING, join predicates) как можно ближе к источнику данных — в идеале до TableScan.

До оптимизации

SELECT o.id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.region = 'EU' AND o.amount > 100;
Filter: c.region = 'EU' AND o.amount > 100
  Join: o.customer_id = c.id
    TableScan: orders (все колонки)
    TableScan: customers (все колонки)

После PushDownFilter

Join: o.customer_id = c.id
  Filter: o.amount > 100
    TableScan: orders
  Filter: c.region = 'EU'
    TableScan: customers

Фильтр c.region = 'EU' спущен к таблице customers, o.amount > 100 — к orders. Каждый источник теперь читает меньше данных, а соединение работает с уменьшенным объёмом.

Предикаты в scan

Для форматов с поддержкой predicate pushdown (Parquet, CSV с индексом) фильтры переносятся прямо в TableScan:

Join: o.customer_id = c.id
  TableScan: orders, filter=[amount > 100]
  TableScan: customers, filter=[region = 'EU']
NOTE

PushDownFilter умеет разделять конъюнктивные предикаты (AND). Выражение a > 5 AND b < 10 разбивается на два независимых фильтра, каждый из которых спускается к своему источнику. Дизъюнкции (OR) спустить сложнее — a > 5 OR b < 10 остаётся на месте, если a и b из разных таблиц.

OptimizeProjections: устранение лишних колонок

OptimizeProjections анализирует, какие колонки реально используются на каждом уровне плана, и убирает чтение ненужных.

До оптимизации

SELECT name FROM users WHERE age > 25;
Projection: users.name
  Filter: users.age > 25
    TableScan: users projection=[id, name, age, email, created_at]

После OptimizeProjections

Projection: users.name
  Filter: users.age > 25
    TableScan: users projection=[name, age]

Из пяти колонок осталось две: name (результат) и age (нужна фильтру). Для Parquet это означает, что три column chunk физически не читаются с диска.

Каскадный эффект

В сложных запросах с подзапросами и соединениями эффект проекции накапливается:

SELECT o.id
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.region = 'EU';

Без оптимизации обе таблицы читаются полностью. После OptimizeProjections:

  • orders — только id, customer_id (для join)
  • customers — только id (для join), region (для фильтра)

SimplifyExpressions: упрощение выражений

Алгебраические и логические упрощения выражений на этапе планирования:

Исходное выражениеУпрощениеПравило
x AND truexНейтральный элемент AND
x OR falsexНейтральный элемент OR
x AND falsefalseАннигиляция AND
NOT NOT xxДвойное отрицание
CAST(5 AS Int64) + 38Constant folding
x = xtrue (если NOT NULL)Рефлексивность
x != xfalse (если NOT NULL)Антирефлексивность
CASE WHEN true THEN a ELSE b ENDaТривиальный CASE

Constant folding

Константные выражения вычисляются на этапе планирования:

-- До
SELECT * FROM t WHERE created_at > '2024-01-01'::timestamp + INTERVAL '30 days';

-- После SimplifyExpressions
SELECT * FROM t WHERE created_at > '2024-01-31T00:00:00';

Runtime не тратит время на сложение даты с интервалом для каждой строки.

TIP

SimplifyExpressions особенно полезно в сочетании с параметризованными запросами. Когда параметры подставлены, выражения вроде WHERE status IN ('active') упрощаются до WHERE status = 'active', что может включить более эффективные стратегии фильтрации.

CommonSubexprEliminate: устранение повторных вычислений

CommonSubexprEliminate (CSE) находит одинаковые подвыражения и вычисляет их один раз:

-- Одно выражение используется дважды
SELECT
    revenue - cost AS profit,
    (revenue - cost) / revenue AS margin
FROM sales;

До CSE

Projection:
  revenue - cost AS profit
  (revenue - cost) / revenue AS margin
    TableScan: sales

Выражение revenue - cost вычисляется дважды.

После CSE

Projection:
  __common_expr_1 AS profit
  __common_expr_1 / revenue AS margin
  Projection:
    revenue - cost AS __common_expr_1
    revenue
      TableScan: sales

Промежуточная проекция вычисляет revenue - cost один раз, результат переиспользуется.

NOTE

CSE работает только с детерминированными выражениями (Volatility::Immutable). Вызовы random() или now() не устраняются, потому что каждый вызов может вернуть разный результат.

EliminateLimit и EliminateFilter

EliminateLimit

Удаляет бессмысленные LIMIT:

-- LIMIT 0 → EmptyRelation (результат пуст)
SELECT * FROM t LIMIT 0;

-- LIMIT без OFFSET, где дочерний узел уже ограничен
-- Два последовательных LIMIT — оставляем меньший
SELECT * FROM (SELECT * FROM t LIMIT 10) sub LIMIT 100;
-- → SELECT * FROM t LIMIT 10

EliminateFilter

Удаляет фильтры, которые всегда истинны или всегда ложны:

-- WHERE true → убрать Filter
SELECT * FROM t WHERE 1 = 1;
-- → TableScan: t

-- WHERE false → EmptyRelation
SELECT * FROM t WHERE 1 = 0;
-- → EmptyRelation: rows=0

PropagateEmptyRelation

Если оператор получает пустой вход, весь поддеревья можно заменить на EmptyRelation:

SELECT * FROM t1
JOIN (SELECT * FROM t2 WHERE false) sub ON t1.id = sub.id;

После EliminateFilter внутренний запрос становится EmptyRelation. PropagateEmptyRelation распознаёт, что INNER JOIN с пустой стороной всегда пуст, и заменяет весь JOIN на EmptyRelation.

Совместная работа правил

Правила спроектированы для работы в цепочке. Каждое правило создаёт возможности для следующих:

Каскад оптимизаций
WHERE a > 5 AND true AND b = bИсходный предикат с избыточными условиями — AND true и рефлексивное сравнение
SimplifyExpressions
WHERE a > 5 AND true → WHERE a > 5SimplifyExpressions удаляет тавтологии (AND true) и рефлексивные сравнения (b = b)
PushDownFilter
TableScan: t, filter=[a > 5]PushDownFilter перемещает упрощённый фильтр в TableScan для выполнения на уровне источника
OptimizeProjections
TableScan: t, projection=[a, b], filter=[a > 5]OptimizeProjections ограничивает проекцию только используемыми колонками

Если запустить PushDownFilter до SimplifyExpressions, тривиальный предикат true будет мешать спуску. Multi-pass решает большинство таких зависимостей, но правильный порядок правил уменьшает количество необходимых проходов.

Наблюдение за правилами

Для отладки используйте observer callback:

use datafusion::optimizer::Optimizer;

fn trace_rules(plan: &LogicalPlan, rule: &dyn OptimizerRule) {
    println!("=== {} ===", rule.name());
    println!("{}", plan.display_indent());
    println!();
}

let optimized = optimizer.optimize(plan, &config, trace_rules)?;

Или через SQL:

-- Показать план до и после оптимизации
EXPLAIN VERBOSE SELECT * FROM t WHERE a > 5 AND true;

EXPLAIN VERBOSE выводит как исходный, так и оптимизированный логический планы, позволяя сравнить эффект всех правил.

Полный список встроенных правил

DataFusion 53.x содержит более 30 логических правил (релиз 53.0.0 от 2026-04-02 расширил pushdown через UnionExec и добавил set-comparison subqueries). Ключевые категории:

КатегорияПравилаЭффект
PredicatePushDownFilter, EliminateFilterМинимизация читаемых данных
ProjectionOptimizeProjectionsМинимизация читаемых колонок
ExpressionSimplifyExpressions, UnwrapCastInComparisonУпрощение вычислений
SubqueryDecorrelatePredicateSubquery, ScalarSubqueryToJoinЗамена подзапросов на JOIN
JoinExtractEquijoinPredicate, EliminateCrossJoinОптимизация соединений
RedundancyEliminateLimit, PropagateEmptyRelation, CommonSubexprEliminateУдаление избыточных операций
RewriteReplaceDistinctWithAggregate, SingleDistinctToGroupByПереписывание в эффективную форму
WARNING

Не все правила одинаково полезны для каждого запроса. Некоторые правила (например, DecorrelatePredicateSubquery) активируются редко — только когда в плане есть соответствующий паттерн. Если правило не находит свой паттерн, оно возвращает Transformed::no и не влияет на время оптимизации.

Итоги

  • PushDownFilter — перемещает фильтры к источнику, минимизируя объём данных на каждом этапе
  • OptimizeProjections — убирает чтение ненужных колонок, критично для columnar-форматов
  • SimplifyExpressions — constant folding и алгебраические упрощения на этапе планирования
  • CommonSubexprEliminate — вычисляет повторяющиеся подвыражения один раз
  • EliminateLimit / EliminateFilter / PropagateEmptyRelation — удаляют бессмысленные операции
  • Правила работают в цепочке: каждое создаёт возможности для следующих через multi-pass
Spark Catalyst: те же правила в JVM ClickHouse: New Analyzer

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. PushDownFilter разделяет конъюнктивный предикат WHERE c.region = 'EU' AND o.amount > 100 при JOIN. Что происходит с каждой частью?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6