Big Data

Columnar Formats: Parquet и ORC

Facebook хранит триллионы событий пользовательских действий. Запрос «сколько раз кнопку Like нажали в январе в Бразилии» должен выполняться за секунды, а не часы. Секрет - не мощность кластера, а формат хранения. Parquet позволяет читать 2 колонки из 80 и пропускать 90% строк по min/max статистикам - задолго до того, как данные достигают CPU.

  • **Apache Spark** - нативно читает/пишет Parquet; это формат по умолчанию для `spark.write.save()`
  • **AWS Athena** - SQL поверх S3, оптимизирован для Parquet/ORC с predicate pushdown
  • **Apache Iceberg / Delta Lake** - используют Parquet как физический формат, добавляя ACID
  • **Pandas / PyArrow** - `pd.read_parquet()` - стандартный способ работы с большими датасетами в Python

Почему колоночное хранение быстрее при аналитике

Запрос `SELECT AVG(price) FROM orders` обращается только к одной колонке из, скажем, 50. В строчном хранении (PostgreSQL, CSV) для каждой строки читается полная запись - все 50 полей - даже если нужна только одна. При 1 миллиарде строк по 500 байт это 500 GB ввода-вывода вместо 10 GB при колоночном подходе. Разница в 50 раз - только за счёт другой раскладки данных.

Второй выигрыш - **однотипные данные** в одном блоке сжимаются значительно лучше. Колонка `country_code` содержит повторяющиеся строки типа "US", "DE", "GB" - RLE (Run-Length Encoding) сжимает это в разы. Смешанные строки в строчном хранении сжимаются хуже.

Parquet (разработан Twitter и Cloudera в 2013) и ORC (разработан в Hortonworks для Hive в 2013) - два доминирующих колоночных формата в big data экосистеме. Оба хранят данные на диске колоночно, но различаются в деталях кодирования и индексирования.

Запрос `SELECT user_id, revenue FROM events` обращается к 2 из 80 колонок. Во сколько раз колоночный формат прочитает меньше данных?

Кодирование в Parquet: dictionary, RLE, bit-packing

Parquet не просто переставляет данные колонками - он применяет несколько уровней кодирования перед сжатием. Выбор алгоритма происходит per-column автоматически, в зависимости от cardinality данных.

  • **Dictionary Encoding:** колонка `status` имеет 3 уникальных значения ("active", "inactive", "pending"). Parquet строит словарь {0:"active", 1:"inactive", 2:"pending"} и хранит индексы. Строки длиной 7-10 байт заменяются числами 0-2
  • **RLE (Run-Length Encoding):** последовательность 0,0,0,0,1,1,1,1,1 → (4×0, 5×1). Эффективно для отсортированных колонок или колонок с повторами
  • **Bit-packing:** если значения от 0 до 3 (2 бита), зачем хранить в 4 байтах? Bit-packing упаковывает N значений в минимальное число бит
АлгоритмСкорость декомпрессииRatioИспользование
SnappyОчень быстро1.5-2xПо умолчанию в Parquet, Kafka
GzipМедленно2.5-3xАрхивы, редкое чтение
ZstdБыстро2.5-4xParquet 2.0+, LZ4 альтернатива
LZ4Очень быстро1.5xГорячие данные, streaming

Dictionary encoding наиболее эффективен для:

Predicate Pushdown: пропускать данные, не читая их

Parquet делит данные на **row groups** (~128 MB по умолчанию). Для каждой row group и каждой колонки хранятся min/max статистики в **footer** файла. При запросе `WHERE date > '2024-01-01'` движок читает footer, сравнивает с min/max каждой row group и пропускает те, которые точно не содержат нужных данных - не читая их вовсе.

**Page-level statistics** идут дальше: внутри row group данные делятся на страницы (~1 MB), и для каждой страницы тоже хранятся min/max. **Bloom filters** (опционально, Parquet 2.0+) позволяют точно проверить `WHERE user_id = 'abc123'` без сканирования страницы - вероятностный фильтр с нулевым false negative.

Predicate pushdown работает, только если данные **отсортированы** или **партиционированы** по колонке фильтрации. Случайный порядок = случайные min/max в row groups = никакого пропуска. Поэтому при записи Parquet важно сортировать по часто используемым фильтрам (`ZORDER BY` в Delta Lake).

Parquet predicate pushdown наиболее эффективен, когда:

Schema Evolution: изменение схемы без миграций

В OLTP-базах изменение схемы - болезненная операция: ALTER TABLE на таблице из 1 миллиарда строк может занять часы. Parquet решает это иначе: каждый файл самодостаточен и содержит собственную схему в footer. Файлы с разными схемами могут сосуществовать в одной директории.

  • **Добавление колонки:** старые файлы не имеют колонки `loyalty_tier` - при чтении она заполняется `null`. Новые файлы записываются с колонкой. Оба типа читаются вместе без ошибок
  • **Удаление колонки:** новые файлы не содержат `deprecated_field`. Старые файлы его содержат - Spark просто игнорирует её при запросе без этого поля
  • **Переименование колонки:** здесь Parquet не помогает - это семантическое изменение. Нужно либо alias, либо перезапись файлов
  • **Изменение типа:** расширяющие преобразования (INT32 → INT64) возможны; сужающие (DOUBLE → FLOAT) требуют явного cast и могут потерять точность
ХарактеристикаParquetORC
Создан в2013, Twitter+Cloudera2013, Hortonworks для Hive
ЭкосистемаSpark, Flink, Presto (де-факто стандарт)Hive (первичный), Spark
СжатиеSnappy/Gzip/ZstdZlib/Snappy/LZ4
Вложенные типыExcellent (Dremel encoding)Хорошо
Bloom filtersParquet 2.0+Встроены (ORC 1.0+)
ACID поддержкаЧерез Delta Lake/IcebergНативно в Hive ACID

Delta Lake добавляет к Parquet **schema enforcement**: при записи данных с несовместимой схемой будет исключение. **Schema evolution** в Delta включается явно: `mergeSchema=true` при записи или `ALTER TABLE ADD COLUMN`. Это сочетает гибкость Parquet со строгостью DWH.

Parquet и ORC взаимозаменяемы - выбор не имеет значения

Parquet - стандарт де-факто в Spark/Presto/Flink экосистеме с лучшей поддержкой вложенных структур. ORC исторически сильнее в Hive и имеет нативный ACID без Delta Lake

ORC был создан специально для Hive и оптимизирован под его query engine. Parquet изначально проектировался под Dremel-модель вложенных данных и лучше поддерживается cross-engine. При выборе смотреть в первую очередь на query engine, а не на абстрактные benchmarks

При добавлении новой колонки в Parquet-файлы, что происходит при чтении старых файлов (без этой колонки)?

Columnar Formats

  • Колоночное хранение читает только нужные колонки - выигрыш пропорционален числу колонок в таблице
  • Parquet кодирует данные (dictionary, RLE, bit-packing) до сжатия - типичный ratio 10-20x против исходного CSV
  • Predicate pushdown пропускает row groups через min/max footer статистики - работает только при отсортированных/партиционированных данных
  • Schema evolution: добавление/удаление колонок без перезаписи файлов; переименование требует явной миграции

Связанные темы

Columnar formats - физический фундамент современных data pipelines.

  • Data Lake vs Data Warehouse — Parquet/ORC используются как физический формат в Lakehouse
  • Apache Spark — Основной движок для чтения/записи Parquet в распределённой среде

Вопросы для размышления

  • Почему predicate pushdown теряет эффективность при случайном порядке записи данных в Parquet?
  • В каких сценариях ORC предпочтительнее Parquet, несмотря на то что Parquet - стандарт де-факто в Spark?
  • Как bloom filters в Parquet 2.0 дополняют min/max статистики для точечных запросов по высококардинальным колонкам?

Связанные уроки

  • db-05-sql-basics
Columnar Formats: Parquet и ORC

0

1

Войти