Условие
В аналитическом пайплайне «сырьё → очистка → агрегаты → витрина». На каждом шаге сохраняем промежуточное. Почему parquet, а не csv? Как правильно партиционировать?
Решение
Почему parquet
| CSV | Parquet | |
|---|---|---|
| Размер на диске | 1× | 0.1–0.3× (snappy/zstd) |
| Чтение колонок | весь файл | только нужные (columnar) |
| Типы | теряются | сохраняются |
| Partition filter | нет | hive-style партиции |
| Repeatable | зависит от парсера | детерминированно |
Базовое сохранение
df.to_parquet(
'clean/events.parquet',
compression='snappy', # 'zstd' даёт ещё меньше при чуть большем CPU
engine='pyarrow',
index=False,
)Партиционирование
Полезно, когда чтение всегда фильтрует по одному-двум столбцам:
df.to_parquet(
'clean/events/',
partition_cols=['dt', 'country'],
engine='pyarrow',
)
# Создаст дерево: clean/events/dt=2024-01-01/country=RU/part-0.parquetПри чтении:
df = pd.read_parquet(
'clean/events/',
filters=[('dt', '>=', '2024-01-01'), ('country', '==', 'RU')],
)Правила партиционирования
- Кардинальность ≤ ~10k — иначе тысячи маленьких файлов убьют чтение.
- Партиционируйте по тому, по чему всегда фильтруют (
dt,country). - Целевой размер файла — 128–512 МБ. Меньше — overhead; больше — плохо для параллельного чтения.
- Один файл — одна партиция.
repartitionперед записью.
Подводные камни
partition_cols=['user_id']при 10M уникальных user_id создаст 10M файлов и убьёт файловую систему.- NaN в partition column превращаются в строку
__HIVE_DEFAULT_PARTITION__— потом фильтр не находит. - Smashing мелких parquet — раз в день запускать compaction (объединение мелких файлов в крупные).
pyarrowvsfastparquet— pyarrow надёжнее, fastparquet чуть быстрее на простых типах. Использовать pyarrow.- Schema evolution — добавил колонку в новый файл, читаешь старые и новые вместе → pyarrow умеет, но fastparquet иногда падает.
Эталонный ответ
Parquet (snappy/zstd) с партиционированием по тому, по чему фильтруют (дата, страна). Партиции с кардинальностью ≤10k, размер файла 128–512 МБ. Не партиционируйте по user_id.