Metadata и Statistics
Иерархия метаданных
Parquet хранит метаданные в Thrift-сериализованной структуре в конце файла. Эта структура — трёхуровневая иерархия: FileMetaData → RowGroupMetaData → ColumnChunkMetaData. Ридер читает только футер, чтобы понять: какие колонки в файле, сколько row groups, где лежит каждый chunk, и какие статистики доступны.
FileMetaData
Корневая структура. Содержит версию формата, Thrift-схему, список RowGroupMetaData, key-value metadata (Spark schema, pandas metadata), и строку created_by.RowGroupMetaData
Метаданные одной row group. Содержит общий размер в байтах, количество строк, и список ColumnChunkMetaData — по одному на каждую колонку.ColumnChunkMetaData
Метаданные одного column chunk. Включает путь к колонке, кодек, смещение в файле, размеры (сжатый/несжатый), кодировки и статистики min/max/null_count.key_value_metadata — это место, куда движки сохраняют свои расширения. Spark записывает туда JSON-схему с nullable, типами Decimal, nested structures. Pandas — метаданные индексов и типов колонок. Это позволяет воссоздать оригинальную схему DataFrame при чтении.
Statistics: min/max/null_count
Каждый ColumnChunkMetaData содержит структуру Statistics с агрегированными значениями по всему chunk:
- min_value / max_value — минимальное и максимальное значение в chunk
- null_count — количество null-значений
- distinct_count — количество уникальных значений (опционально, часто отсутствует)
- is_min_value_exact / is_max_value_exact — флаги точности (для усечённых строк)
Это позволяет row group pruning: если запрос WHERE user_id > 150000, а статистики Row Group 0 показывают max_value = 100000, вся группа пропускается без чтения данных.
Statistics в footer — это chunk-level статистики. Они работают на гранулярности row group. Для page-level pruning (пропуск отдельных страниц внутри column chunk) нужны ColumnIndex и OffsetIndex — современные структуры, добавленные в спецификацию позже.
ColumnIndex и OffsetIndex
Chunk-level статистики — грубый инструмент. Если row group содержит 1 000 000 строк на 20 страниц, мы можем пропустить ненужные страницы, а не только row groups. Для этого Parquet определяет два индекса:
ColumnIndex хранит min/max/null_count для каждой страницы column chunk. Читается из footer, не требует доступа к самим страницам.
OffsetIndex хранит offset и compressed_page_size для каждой страницы. Позволяет вычислить, какие байты файла нужно прочитать для конкретной страницы.
ColumnIndex (user_id)
OffsetIndex (user_id)
Комбинация работает так:
- Ридер читает ColumnIndex из footer → определяет, какие страницы содержат нужные значения
- Ридер читает OffsetIndex → определяет byte ranges для нужных страниц
- Выполняет точечные чтения только нужных байтов (Range Reads на S3/GCS)
Это превращает Parquet из «скипаем целые row groups» в «скипаем отдельные страницы» — на порядок точнее.
Bloom-фильтры
Statistics хорошо работают для range-предикатов (>, <, BETWEEN), но для point lookups (WHERE city = 'Berlin') min/max бесполезны, если chunk содержит значения от ‘Amsterdam’ до ‘Zurich’ — диапазон слишком широкий.
Bloom-фильтр — вероятностная структура данных, которая отвечает на вопрос «содержит ли chunk это значение?» с гарантиями:
- Нет → точно нет (false negative невозможен)
- Да → возможно (false positive возможен, но контролируем)
Bloom Filter (city chunk)
Bloom-фильтр для column chunk колонки city. Split Block Bloom Filter (SBBF) — реализация в Parquet. Хэширует значение и проверяет биты.Parquet использует Split Block Bloom Filter (SBBF) — эффективный вариант, оптимизированный под кэш-линии CPU. Фильтр хранится отдельно от данных и читается по offset из footer metadata.
Настройки в PyArrow:
import pyarrow.parquet as pq
pq.write_table(
table,
'users.parquet',
write_statistics=True,
# Bloom-фильтр для колонки city с FPP 1%
column_config={
'city': pq.ColumnConfig(
bloom_filter_enabled=True,
bloom_filter_fpp=0.01
)
}
)
Bloom-фильтры увеличивают размер файла (несколько KB на chunk) и время записи. Включайте их только для колонок, по которым часто фильтруют через = или IN. Для range-запросов достаточно Statistics + ColumnIndex.
Порядок чтения метаданных
Когда движок (Spark, DuckDB, Trino) открывает Parquet-файл, происходит многоступенчатый процесс фильтрации — каждый шаг сужает объём данных, которые нужно реально прочитать:
1. Читаем footer (последние N байт файла)
└── FileMetaData → знаем схему и список row groups
2. Projection pushdown → отбрасываем ненужные колонки
└── SELECT user_id, score → skip column chunk "username"
3. Row Group pruning по Statistics
└── WHERE user_id > 150000 → skip Row Group 0 (max=100000)
4. Page-level pruning по ColumnIndex + OffsetIndex
└── skip Page 0 в Row Group 1 (max=150000)
5. Bloom-фильтр для point lookups
└── WHERE city = 'Berlin' → skip chunks без 'Berlin'
6. Читаем только нужные байты → декодируем → возвращаем результат
Эволюция метаданных: Page Index и Parquet 2.11+ logical types
Структуры метаданных Parquet эволюционировали постепенно. Понимание timeline помогает читать чужой код и спецификации:
| Фича | Версия Parquet | Дата | Что добавляет |
|---|---|---|---|
| ColumnIndex / OffsetIndex (часто называют Page Index) | 2.5 | 2018 | Page-level pruning |
| Bloom Filter (SBBF) спецификация | 2.6 | 2019 | Point-lookup pruning, повсеместная поддержка только с PyArrow 11+ / parquet-mr 1.13+ (~2023–2024) |
| VARIANT logical type | 2.11 | март 2025 | Semi-structured данные (см. урок про Dremel) |
| GEOMETRY / GEOGRAPHY logical types | 2.11 | март 2025 | Native geospatial типы и статистики |
| Statistics V2 (geospatial bbox, distinct count) | 2.11+ | март 2025 → 2026 | Histogram-based и геоориентированные статистики |
До 2018 года pruning в Parquet работал только на уровне row group через chunk-level Statistics. Page Index (ColumnIndex + OffsetIndex) добавлен в Parquet 2.5 именно для page-level pruning — без него page-level skip невозможен. Многие старые файлы (созданные до ~2019) Page Index не содержат — у читателя нет выбора, кроме чтения целых row groups.
GEOMETRY и GEOGRAPHY logical types (Parquet 2.11+)
Parquet 2.11 (март 2025) ратифицировал нативную поддержку геопространственных типов. До этого геоданные хранились через расширения GeoParquet (1.x — Well-Known Binary в обычном BYTE_ARRAY колонке + sidecar метаданные). Теперь типы — first-class:
- GEOMETRY — features в WKB-формате (Well-Known Binary) с линейной/планарной интерполяцией рёбер. Аннотирует
BYTE_ARRAY. - GEOGRAPHY — features в WKB-формате с нелинейной (геодезической) интерполяцией — рёбра идут по большим кругам сферы/эллипсоида. Тоже
BYTE_ARRAY.
Parquet 2.11 GEOMETRY-колонка — это не GeoParquet. GeoParquet 1.x — отдельная спецификация с метаданными в key_value_metadata файла, GeoParquet 2.0+ постепенно мигрирует на nativные типы Parquet 2.11. Совместимость поддерживается на стороне библиотек (GDAL, geoarrow, geopandas).
Для GEOMETRY и GEOGRAPHY обычные min/max не вычисляются — они бессмысленны для WKB-байтов. Если такие статистики записаны не-compliant писателем, ридер обязан их игнорировать.
Statistics V2: bounding box и distinct count
Parquet 2.11+ добавляет в ColumnChunkMetaData опциональное поле geospatial statistics — bounding box на уровне row group:
GeospatialStatistics {
bbox: BoundingBox { xmin, xmax, ymin, ymax, zmin?, zmax?, mmin?, mmax? }
geospatial_types: [POINT, LINESTRING, POLYGON, ...] // какие WKB-типы встречаются
}
Это даёт spatial pruning:
-- Запрос: точки внутри Берлина
SELECT * FROM events
WHERE ST_Within(location, ST_GeomFromText('POLYGON((13.0 52.3, 13.8 52.3, 13.8 52.7, 13.0 52.7, 13.0 52.3))'));
-- Pruning: bbox row group [10.0..11.5, 50.0..51.5] не пересекается с Берлином → skip
До Parquet 2.11 для spatial pruning приходилось хранить отдельные колонки lat_min, lat_max, lon_min, lon_max и фильтровать по ним вручную. Native bbox в Statistics V2 делает это прозрачным — engine получает pruning без участия пользователя.
Помимо геостатистик, Statistics V2 также формализует distinct count histogram для не-геометрических типов — раньше distinct_count был просто числом и часто отсутствовал. Histogram-based статистики позволяют cost-based optimizer’ам точнее оценивать селективность.
Ключевые выводы
- FileMetaData хранится в footer файла и содержит полную иерархию: схема → row groups → column chunks → statistics
- Statistics (min/max/null_count) — chunk-level, позволяют пропускать целые row groups
- ColumnIndex + OffsetIndex = Page Index (с Parquet 2.5, 2018) — page-level pruning
- Bloom-фильтры — для point lookups, вероятностное «точно нет / возможно есть» (повсеместная поддержка с PyArrow 11+ / parquet-mr 1.13+ ~2023–2024)
- Parquet 2.11 (март 2025) добавил VARIANT, GEOMETRY, GEOGRAPHY logical types и Statistics V2 (bbox, distinct count histogram)
- Многоступенчатая фильтрация: projection → row group pruning → page pruning → bloom filter → spatial pruning → чтение