Какие подходы к оптимизации запросов кроме broadcast join вы используете?
Что проверяют
Broadcast join знают почти все, поэтому интервьюер проверяет, понимаете ли вы, откуда берётся медленный запрос в распределённом движке: объём чтения, shuffle и перекос данных. Следом обычно просят рассказать, как вы нашли узкое место в конкретном тормозящем запросе.
Как отвечать
Начните с диагностики: EXPLAIN и Spark UI показывают, какой этап читает больше всего, сколько данных уходит в shuffle и нет ли задач, которые идут в десятки раз дольше соседних. Дальше приёмы:
- читать меньше: Parquet или ORC, только нужные колонки, фильтр как можно раньше. Тогда срабатывают predicate pushdown и partition pruning, и движок пропускает целые файлы;
- партиционирование и бакетирование: таблица, разбитая по дате, не сканируется целиком ради одного дня, а бакеты по ключу соединения позволяют делать join без полного shuffle;
- борьба с перекосом: если один ключ занимает половину данных, одна задача тянет весь этап. Помогают salting, то есть случайная соль к ключу, и обработка skew join в AQE;
- Adaptive Query Execution: начиная со Spark 3.2 включён по умолчанию, объединяет мелкие партиции после shuffle и меняет стратегию соединения по фактическим размерам;
- число партиций:
spark.sql.shuffle.partitionsпо умолчанию 200, для больших объёмов это мало, для маленьких много; - агрегация до соединения: сначала свернуть большую таблицу до нужной гранулярности, потом соединять;
- повторное использование:
cache()для датафрейма, который читается несколько раз, или промежуточная витрина вместо повторного тяжёлого расчёта; - встроенные функции вместо UDF: Python UDF выпадает из оптимизатора и гоняет данные через сериализацию.
Где спотыкаются
Отвечают набором из учебника по OLTP: индексы, нормализация, Redis. Для аналитического запроса в Spark это мимо.
Не могут сказать, как узнали, что стало быстрее. Нужны цифры до и после: время этапа, объём shuffle, прочитанные байты.
Расставляют cache() и repartition() без причины. Кэш занимает память исполнителей, а лишний repartition() добавляет ещё один shuffle.