Как выбрать ключ для partitionBy в Spark?

«Как выбрать ключ для partitionBy в Spark?» — вопрос из категории Apache Spark, который задают на 33% собеседований Data Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Выбор колонок для partitionBy() — это компромисс между равномерностью распределения данных и эффективностью последующих запросов. Я руководствуюсь следующими принципами:

1. Критерии выбора "хорошего" ключа партиционирования:

  • Высокая кардинальность в запросах: Колонка часто используется в условиях WHERE, JOIN или GROUP BY. Это позволяет Spark использовать partition pruning — пропускать целые директории при чтении.
  • Ограниченное и предсказуемое число уникальных значений: Идеально подходят категориальные данные (страна, регион, тип события) или временные интервалы (год, месяц, день). Это предотвращает создание "россыпи" из миллионов мелких файлов.
  • Равномерное распределение данных: Избегаю колонок с перекосом (skew). Например, партиционирование по user_id, где 90% действий принадлежит 1% пользователей, приведет к гигантским и маленьким партициям.

2. Практические примеры на PySpark:

# Хороший пример: запросы часто идут по дате и стране
df.write 
  .partitionBy("event_date", "country_code") 
  .parquet("/data/events")
# При запросе WHERE event_date='2024-08-30' Spark прочитает только одну поддиректорию.

# Плохой пример: партиционирование по колонке с уникальным ID
df.write.partitionBy("transaction_id").parquet("/data/transactions") # Катастрофа! Создаст столько же партиций, сколько строк.

# Компромиссный пример для временных рядов:
# Добавляем колонки для партиционирования
from pyspark.sql.functions import year, month, dayofmonth
df_with_partitions = df.withColumn("y", year("timestamp")) 
                       .withColumn("m", month("timestamp")) 
                       .withColumn("d", dayofmonth("timestamp"))
df_with_partitions.write.partitionBy("y", "m", "d").parquet("/data/time_series")

3. Что я избегаю:

  • Колонки с NULL значениями (они попадут в партицию __HIVE_DEFAULT_PARTITION__).
  • Колонки, которые часто обновляются (перезапись одной строки потребует перезаписи целого файла партиции).
  • Слишком глубокое вложение партиций (например, year/month/day/hour/minute), которое создаст много мелких файлов и накладные расходы на метаданные.