Ответ
Выбор колонок для 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), которое создаст много мелких файлов и накладные расходы на метаданные.