Spark RDD vs DataFrame vs Dataset на собеседовании Data Engineer
Карьерник — 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 для проверок.
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 plan → optimized logical plan → physical plan → codegen. Видеть план: 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 МБ. Меньше — слишком много мелких задач.
Связанные темы
- Spark на собесе DE
- Spark shuffle и skew на собесе DE
- Parquet ORC Avro для DE
- Hadoop и MapReduce для DE
- Подготовка к собесу Data Engineer
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+ вопросами для собесов.