awesome-everything EN
↑ Обратно к восхождению

Data engineering

Parquet: почему аналитика хранит колонки, а не строки

Суть Parquet — колоночный, самоописывающий формат: в футере лежат min/max-статистики на каждую row group, поэтому фильтр пропускает целые row groups и читает только нужные колонки. CSV парсит каждый байт. Сеньорские ловушки: мелкие файлы, размер row group, разбухание словаря.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

Ночной Spark-джоб пишет таблицу events в S3 как gzip CSV. Простой SELECT user_id, country FROM events WHERE day = '2024-02-01' читает 1.9 ГБ, распаковывает каждый байт, парсит каждую колонку, выбрасывает 38 из 40 колонок и работает 90 секунд. Аналитик перезапускает его сорок раз, настраивая дашборд. Те же данные в Parquet с snappy: 220 МБ на диске, движок читает два column chunk и row groups одного дня, и запрос возвращается меньше чем за две секунды. В данных не изменилось ничего — только формат файла.

Колонки на диске, а не строки

CSV и JSON хранят данные строка за строкой: все поля строки 1, потом все поля строки 2. Чтобы прочитать одну колонку, ты всё равно проходишь каждый байт каждой строки, парсишь разделители и выбрасываешь 95% распарсенного. Нет статистик, нет типов — "2024" может быть строкой или числом, и читатель не узнает без парсинга.

Parquet переворачивает раскладку. Внутри каждого файла данные разбиты на row groups (горизонтальный срез строк, целевой размер по умолчанию ~128 МБ), а внутри row group каждая колонка хранится как непрерывный column chunk, сам разбитый на pages (единица кодирования и сжатия, ~1 МБ по умолчанию). Поскольку все значения одной колонки лежат вместе, следуют две вещи, невозможные в CSV. Первая — column pruning: запрос, которому нужны user_id и country, читает только эти два column chunk и никогда не касается остальных 38. Вторая — кодирование становится жёстко эффективным: миллион строк country — это в основном одни и те же несколько строк, а колонка похожих значений сжимается куда лучше, чем строка из смешанных типов.

Скорость живёт в футере

Каждый Parquet-файл заканчивается футером: схема плюс метаданные по каждой row group и каждой колонке — и, что критично, min/max/null-count статистики для каждой колонки в каждой row group. Это самая важная структура для производительности запросов.

Когда ты выполняешь WHERE day = '2024-02-01', движок сначала читает футер, смотрит min и max колонки day каждой row group и пропускает любую row group, диапазон которой не может содержать значение, не читая ни байта данных. Это predicate pushdown плюс row-group skipping. На годе дневных данных, отсортированных по дню, такой фильтр касается примерно 1 из 365 row groups. Статистики на уровне pages (и опциональные page indexes) толкают ту же идею на уровень глубже, пропуская pages внутри уцелевшей row group. CSV не умеет ничего из этого — без статистик единственный способ узнать, подходит ли строка, — прочитать и распарсить её.

Почему это работает

Пропуск работает только если данные разложены так, что диапазоны min/max колонки фильтра узкие и непересекающиеся. Сортируй или партиционируй по колонкам, по которым фильтруешь чаще всего. Если значения day случайно разбросаны по row groups, [min, max] каждой row group охватывает весь год, ни одну row group нельзя пропустить, и ты заплатил за статистики, которые ничего не дают. Статистики необходимы, но не достаточны — окупает их кластеризация.

Кодирование и сжатие: два отдельных выигрыша

Parquet сжимает данные в двух наложенных слоях, и сеньоры держат их в голове раздельно. Кодирование учитывает тип и lossless по построению: dictionary encoding заменяет повторяющиеся значения короткими целыми кодами (идеально для строк низкой кардинальности вроде country, status), run-length encoding (RLE) схлопывает прогоны одинаковых значений, bit-packing использует ровно столько бит, сколько нужно диапазону значений, а delta encoding хранит разности для отсортированных целых и таймстемпов. Сжатие (snappy, zstd, gzip) затем прогоняет универсальный кодек поверх уже закодированных байтов.

Числа — вот почему формат победил. CSV на 1.9 ГБ превращается примерно в 1.2 ГБ сырого несжатого Parquet (одно кодирование), а со сжатием обычно в 9–15% от исходного размера CSV. snappy — частый дефолт: быстрый, дешёвый по CPU, ~8x. zstd даёт примерно на 15–20% меньше файлы, чем snappy, при потере чтения меньше 1%, поэтому он лучше для холодных или архивных данных; gzip сжимает ещё плотнее, но заметно медленнее на чтении.

СвойствоCSV / JSONParquet
РаскладкаСтроковаяКолоночная (row group → column chunk → page)
Читать только часть колонокНет — полный парсингДа — column pruning
Пропуск строк по фильтруНет статистик → читаешь всёmin/max в футере → пропуск row groups + pages
Схема / типыНет — угадываются при чтенииВстроены, типизированы, самоописывающие
Размер vs CSV1x (база)~0.09–0.15x в сжатии

Где это кусает в проде

Parquet не свободен от граблей, и каждые из них вылезают на масштабе, а не в демо.

Первая — проблема мелких файлов. Каждый Parquet-файл несёт свой футер и стоит одного I/O, чтобы открыть и прочитать. Стриминговый джоб, пишущий файл по 4 КБ каждые несколько секунд, производит миллионы крошечных файлов, и теперь самая медленная часть каждого запроса — это их листинг: рекурсивно перечислять миллионы объектов в S3 печально медленно и дорого, а планировщику нужно прочитать каждый футер, прежде чем строить план. Фикс — компакция: периодически переписывать рой в файлы диапазона 128 МБ – 1 ГБ.

Вторая — размер row group. Слишком большой — теряешь гранулярность для пропуска и нужна огромная память, чтобы записать одну группу; слишком маленький — раздувается оверхед метаданных, и ты читаешь больше футеров, чем данных. Третья — разбухание словаря: dictionary encoding великолепен для колонок низкой кардинальности, но на колонке высокой кардинальности (UUID, свободный текст) словарь перерастает свой бюджет размера page (~1 МБ), Parquet откатывается к plain encoding, и ты заплатил за построение словаря, который ничего не дал, — иногда получая больший файл. Четвёртая — эволюция схемы: добавление, переименование или перестановка колонок по тысячам файлов оставляет читателей примирять несовпадающие схемы, а переименованное-и-переиспользованное имя колонки может молча смешать два смысла.

Табличные форматы: Parquet плюс мозг

Сырой Parquet — это просто файлы в директории; у него нет понятия транзакции, снимка или «текущего набора файлов». Именно этот пробел заполняют Iceberg, Delta Lake и Hudi. Они держат данные в Parquet, но добавляют сверху слой метаданных/манифестов, который отслеживает, какие файлы принадлежат таблице прямо сейчас, давая ACID-коммиты, time travel, безопасную эволюцию схемы (переименование колонки становится metadata-only операцией, а не переписыванием данных) и manifest-level pruning — движок сверяется с per-file статистиками манифеста, чтобы исключить целые файлы, ещё не открывая ни одного футера. Когда говорят «data lakehouse», имеют в виду именно этот слой: Parquet для хранения, табличный формат для корректности и планирования.

Выбери лучший вариант

Kafka-консьюмер пишет события кликов в S3 как Parquet, сбрасывая крошечный файл каждые 10 секунд. Дашборды над этой таблицей стали мучительно медленными. Выбери фикс.

Викторина

Запрос WHERE day = '2024-02-01' на годе данных. Что позволяет Parquet читать куда меньше, чем CSV?

Викторина

Ты включаешь dictionary encoding на колонке случайных UUID. Какой вероятный исход?

Расставь шаги по порядку

Расставь физическую вложенность, снаружи внутрь:

  1. 1 Файл (заканчивается футером со схемой + статистиками на каждую row group)
  2. 2 Row group (горизонтальный срез строк, цель ~128 МБ)
  3. 3 Column chunk (все значения одной колонки внутри этой row group, непрерывно)
  4. 4 Page (единица кодирования + сжатия, ~1 МБ)
  5. 5 Закодированные + сжатые значения (dictionary / RLE / delta, затем snappy/zstd)
Вспомните перед уходом
  1. 01
    Объясни коллеге, от и до, почему отфильтрованный запрос на Parquet читает куда меньше, чем тот же запрос на CSV.
  2. 02
    Что такое проблема мелких файлов, почему она калечит планирование запросов и как помогают табличные форматы?
Итог

Parquet хранит данные колонка за колонкой, а не строка за строкой, что делает возможными две вещи, недоступные CSV и JSON: column pruning, где запрос читает только нужные column chunk, и row-group skipping, где движок использует min/max/null-count статистики в футере файла, чтобы пропустить целые row groups (и pages), которые не могут удовлетворить фильтр, — predicate pushdown. Внутри раскладка вложена: файл → row group (~128 МБ) → column chunk → page (~1 МБ), и каждый page сначала кодируется (dictionary, RLE, bit-packing, delta), а затем сжимается (snappy для горячих данных, zstd для ~15–20% меньших холодных файлов), приземляясь обычно на 9–15% от исходного размера CSV. Сеньорские режимы отказа — проблема мелких файлов (миллионы крошечных файлов делают листинг и чтение футеров доминантой планирования — фиксится компакцией), неверный размер row group, разбухание словаря на колонках высокой кардинальности (откат к plain encoding, иногда больший файл) и грязная эволюция схемы по многим файлам. Пропуск окупается только когда данные кластеризованы по колонкам фильтра, поэтому сортируй и партиционируй осознанно. А поскольку у сырого Parquet нет транзакций и снимков, табличные форматы — Iceberg, Delta Lake, Hudi — оборачивают его слоем манифестов ради ACID, time travel, безопасной эволюции схемы и manifest-level pruning, и именно это превращает кучу Parquet-файлов в таблицу lakehouse.

Продолжить восхождение ↑Parquet: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.