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-4x | Parquet 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 и могут потерять точность
| Характеристика | Parquet | ORC |
|---|---|---|
| Создан в | 2013, Twitter+Cloudera | 2013, Hortonworks для Hive |
| Экосистема | Spark, Flink, Presto (де-факто стандарт) | Hive (первичный), Spark |
| Сжатие | Snappy/Gzip/Zstd | Zlib/Snappy/LZ4 |
| Вложенные типы | Excellent (Dremel encoding) | Хорошо |
| Bloom filters | Parquet 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 статистики для точечных запросов по высококардинальным колонкам?