Spark RDD vs DataFrame vs Dataset на собеседовании Data Engineer

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Карьерник — Duolingo для аналитиков: 10 минут в день тренируй SQL, Python, A/B, статистику, метрики и ещё 3 темы собеса. 1500+ вопросов в Telegram-боте. Бесплатно.

Зачем спрашивают на собесе DE

Spark — основной distributed-движок в DE. На собесе обязательно: «отличие RDD от DataFrame», «зачем lazy evaluation», «что такое action». Senior — нюансы Catalyst, codegen, Tungsten.

Главная боль без понимания — DE использует RDD на DataFrame-задачах, теряет 10× производительности от Catalyst optimizer.

RDD: первый абстрактный слой

RDD (Resilient Distributed Dataset) — нижний уровень API Spark. Распределённая коллекция объектов с операциями map / filter / reduce.

Свойства:

  • Immutable — каждая операция создаёт новый RDD.
  • Distributed — данные на N executors, разбиты на partitions.
  • Resilient — lineage помнит, как получен RDD; при потере partition можно пересчитать.
  • Без схемы — Spark видит «коллекцию Any».
  • Без оптимизатора — что вы напишете, то и будет.
rdd = sc.parallelize([1, 2, 3, 4, 5])
result = rdd.filter(lambda x: x > 2).map(lambda x: x * x).collect()
# [9, 16, 25]

Когда RDD реально нужны:

  • Очень нестандартная логика, которую нельзя выразить в DataFrame.
  • Контроль низкоуровневых операций (graph processing).
  • Старый Spark < 2.0.

В 2026 — RDD редко. DataFrame покрывает 95% задач DE.

DataFrame: со схемой и Catalyst

DataFrame — таблица со схемой (имена колонок и типы). Внутренне — RDD[Row], но с метаданными о структуре.

Преимущества:

  • Catalyst Optimizer оптимизирует план запроса (predicate pushdown, projection pruning, join reorder).
  • Tungsten — оптимизация памяти и codegen для CPU.
  • SQL и DSL.
  • В 5-10× быстрее RDD на типовых задачах.
from pyspark.sql import functions as F

df = spark.read.parquet("s3://bucket/orders/")
result = (df.filter(F.col("amount") > 100)
            .groupBy("user_id")
            .agg(F.sum("amount").alias("total"))
            .orderBy(F.desc("total")))
result.show()

SQL-эквивалент:

df.createOrReplaceTempView("orders")
spark.sql("""
  SELECT user_id, SUM(amount) AS total
  FROM orders
  WHERE amount > 100
  GROUP BY user_id
  ORDER BY total DESC
""")

Catalyst парсит, оптимизирует, codegen → JVM-байткод. Самое быстрое API.

Dataset: тот же DataFrame, но типизированный

Dataset — DataFrame + статическая типизация. Доступен только в Scala / Java (в PySpark Dataset = DataFrame).

case class Order(userId: Long, amount: Double)
val ds: Dataset[Order] = spark.read.parquet("...").as[Order]

Преимущества:

  • Compile-time type safety (опечатка в имени колонки — ошибка компиляции).
  • Совмещает Catalyst-оптимизацию с типобезопасностью.

Недостатки:

  • В Python — нет (PySpark не имеет статической типизации).
  • Сериализация Encoder может быть дороже Row для сложных типов.

В Python мире — DataFrame достаточно. Тип-сейф на уровне mypy + pandera для проверок.

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Lazy evaluation, transformations vs actions

Lazy evaluation. Каждая операция на RDD/DataFrame не выполняется сразу — только строит план. Real execution начинается на action.

Transformations — ленивые: map, filter, groupBy, select, join, union, withColumn.

Actions — запускают вычисление: collect, count, show, write, toPandas, take, first, reduce, foreach.

# никаких вычислений не было
df1 = df.filter(F.col("amount") > 100)
df2 = df1.groupBy("user_id").sum("amount")

# здесь триггерится план
df2.show()

Catalyst строит logical planoptimized logical planphysical plancodegen. Видеть план: df2.explain().

Caveat: каждый action тригерит full execution. Если хотите дважды показать один результат — cache() или persist():

df2.cache()
df2.show()      # вычислит и закэширует
df2.count()     # из кэша

Когда что использовать

DataFrame:

  • Дефолт для всего DE.
  • Чтение/запись Parquet / JSON / CSV.
  • Aggregations, joins, windowing.
  • ETL pipelines.

RDD:

  • Текстовые данные без структуры (логи).
  • Очень специфичная логика (graph algorithms через GraphX, ML algorithms через MLlib).
  • Контроль над partition'ингом не выражаемый в DataFrame.

Spark SQL:

  • Когда команда привычнее к SQL.
  • Когда уже есть SQL на Hive/Presto, миграция.

Dataset (Scala):

  • Сложные доменные модели в типобезопасной кодовой базе.

В современном Spark (3.x+) — 95% DE задач решаются DataFrame'ами + parquet + Delta/Iceberg.

Частые ошибки

RDD по привычке. Без скоростного выигрыша Catalyst. На сотнях ГБ — катастрофически медленнее.

collect() на больших данных. df.collect() тащит ВСЕ строки в driver. OOM. Используй take(N), show(), или сохраняй на диск.

UDF вместо встроенных функций. Python UDF — десериализация → выполнение в Python → сериализация. В 10-100× медленнее. Используй встроенные F.* или Pandas UDF (vectorized).

Игнорировать cache для multi-action. Дважды посчитал тот же DataFrame = дважды читал источник.

Не делать partitionBy на запись. При df.write.parquet(...) без partitionBy — все данные одной партицией. На больших датасетах — невозможно эффективно прочитать сегмент.

Использовать .show() для production output. show рендерит в консоль. Запись в файл / DWH — .write....

Считать, что DataFrame = pandas DataFrame. Совершенно разные. Spark — distributed, lazy, immutable. Pandas — single-node, eager, mutable.

Слишком много партиций после repartition(N). Каждая партиция — overhead. Эмпирика: партиция 100-200 МБ. Меньше — слишком много мелких задач.

Связанные темы

FAQ

Pandas на Spark — что это?

Pandas API на Spark (бывший Koalas) — pandas-like API, но под капотом DataFrame. Удобно для миграции pandas-кода. С 3.2+ часть Spark.

Что такое DataFrame.rdd?

df.rdd возвращает RDD[Row] — деградация до низкого уровня. Иногда нужно для совместимости со старыми библиотеками. Обычно не нужно.

cache() vs persist()?

cache() = persist(StorageLevel.MEMORY_ONLY). persist() принимает уровень — MEMORY_AND_DISK, DISK_ONLY и т.д.

Сколько партиций оптимально?

Эмпирика: 100-200 МБ на партицию для shuffle. spark.sql.shuffle.partitions — дефолт 200, для больших данных увеличивай (1000-4000).

Catalyst оптимизирует Python UDF?

Нет — функция чёрный ящик. Catalyst может только pushdown filter ДО UDF. Лучше избегать Python UDF, использовать SQL-функции или Scala UDF.

Это официальная информация?

Нет. Статья основана на документации Spark 3.x и материалах Databricks.


Тренируйте Data Engineering — откройте тренажёр с 1500+ вопросами для собесов.