Как данные лежат на диске, определяет, сколько их придётся прочитать. Хорошая раскладка ускоряет запросы в десятки раз без единой строчки нового кода, плохая заставляет кластер читать терабайты ради мегабайта ответа.
Строчные и колоночные форматы
- CSV, JSON: текст, строка за строкой. Просто и универсально, но нет типов (или они угадываются), плохое сжатие, чтение одной колонки требует разбора всей строки.
- Avro: бинарный строчный формат со схемой. Хорош для потоков и записи событий по одному (Kafka), поддерживает эволюцию схемы.
- Parquet, ORC: бинарные колоночные форматы. Стандарт для аналитики и озёр данных.
Как устроен Parquet
Файл Parquet делится на:
- Row groups: горизонтальные куски по многу строк (часто десятки-сотни мегабайт).
- Внутри row group column chunks: данные каждой колонки отдельно.
- Внутри chunk pages: единицы кодирования и сжатия.
- Footer в конце файла: схема, расположение row groups и статистики по колонкам (min, max, число NULL).
Что это даёт:
- Column pruning (projection): читаются только нужные колонки.
- Predicate pushdown: по min и max в футере движок пропускает row groups, где условие заведомо ложно. Запрос
WHERE order_date = '2025-09-01'не читает группы, у которыхmax(order_date) < '2025-09-01'. Работает хорошо, если данные отсортированы или сгруппированы по колонке фильтра; при случайном порядке у каждой группы широкий диапазон, и пропускать нечего. - Кодирование: словарное для колонок с небольшим числом значений (город, статус), RLE для повторов, дельта для возрастающих чисел и дат.
- Сжатие поверх кодирования: Snappy (быстро), Zstd (лучше сжимает при разумной скорости), Gzip (сильно, но медленно).
- Типы и вложенные структуры: схема хранится в файле, поддерживаются массивы и структуры.
Parquet неизменяем: обновить строку внутри файла нельзя, только переписать файл. Поэтому для UPDATE и DELETE поверх Parquet нужны табличные форматы (Iceberg, Delta, Hudi).
Партиционирование
Данные раскладываются по каталогам по значению колонки:
s3://dwh/events/event_date=2025-09-01/part-0001.parquet
s3://dwh/events/event_date=2025-09-02/part-0001.parquet
Это «hive-style» партиционирование. Запрос с фильтром по event_date читает только нужные каталоги: partition pruning.
Как выбрать ключ партиционирования:
- Колонка, по которой почти всегда фильтруют: обычно дата события.
- Низкая или умеренная кардинальность: сотни-тысячи партиций, а не миллионы. Партиционирование по
user_idсоздаст миллионы крошечных каталогов. - Размер партиции: от сотен мегабайт до нескольких гигабайт. Если данных мало, партиция по месяцу вместо дня.
- Фильтр должен быть по самой колонке партиции: условие
WHERE date(event_ts) = ...при партиции поevent_dateможет не сработать для pruning, если движок не связывает колонки.
Проблема мелких файлов
Тысячи файлов по несколько килобайт: частая беда стриминга и чрезмерного партиционирования.
- Каждый файл это запрос к хранилищу, открытие, чтение футера: накладные расходы больше полезной работы.
- Метаданных много, листинг каталогов медленный.
- Сжатие и статистики работают хуже на маленьких файлах.
Решения: компакция (периодически переписывать мелкие файлы в крупные, табличные форматы делают это процедурами), запись меньшим числом крупных файлов (coalesce, repartition по колонке партиции перед записью), более крупные партиции.
Бакетинг
Бакетинг делит данные внутри таблицы на фиксированное число файлов-бакетов по хешу колонки: hash(user_id) % 64. Если две таблицы бакетированы по одному ключу с одинаковым числом бакетов, движок соединяет их бакет с бакетом без shuffle. Полезно для частых джойнов и агрегаций по ключу высокой кардинальности, где партиционирование невозможно.
Отличие: партиционирование по значению создаёт каталоги и даёт pruning по фильтру; бакетинг по хешу даёт фиксированное число файлов и помогает джойнам. Их часто сочетают: партиция по дате, внутри бакеты по user_id.
В табличных форматах развивают эту идею: скрытое партиционирование и трансформации (Iceberg: day(ts), bucket(16, id)), сортировка и z-order внутри файлов для лучшего пропуска данных по нескольким колонкам.
Частые ошибки
- Партиционировать по колонке высокой кардинальности.
- Оставлять тысячи мелких файлов после стриминга без компакции.
- Хранить аналитику в CSV или JSON.
- Ожидать predicate pushdown от неотсортированных данных.
- Фильтровать не по колонке партиции, а по вычисленному выражению.
Что спросят на собесе
- Почему Parquet быстрее CSV для аналитики, как он устроен внутри.
- Что такое predicate pushdown и когда он не работает.
- Как выбрать ключ партиционирования, чем плохи мелкие файлы.
- Чем бакетинг отличается от партиционирования.