Learning Platform
Глоссарий Troubleshooting
Урок 03.05 · 30 мин
Продвинутый
ParquetMetadataStatisticsColumnIndexOffsetIndexBloom FilterPredicate Pushdown

Metadata и Statistics

Иерархия метаданных

Parquet хранит метаданные в Thrift-сериализованной структуре в конце файла. Эта структура — трёхуровневая иерархия: FileMetaDataRowGroupMetaDataColumnChunkMetaData. Ридер читает только футер, чтобы понять: какие колонки в файле, сколько row groups, где лежит каждый chunk, и какие статистики доступны.

Иерархия метаданных Parquet

FileMetaData

Корневая структура. Содержит версию формата, Thrift-схему, список RowGroupMetaData, key-value metadata (Spark schema, pandas metadata), и строку created_by.
├── version: 2
├── schema: Thrift-encoded column definitions
├── num_rows: 200 000
├── key_value_metadata: [{"org.apache.spark.sql.parquet.row.metadata": "..."}]
└── row_groups: [RowGroupMetaData × 2]

RowGroupMetaData

Метаданные одной row group. Содержит общий размер в байтах, количество строк, и список ColumnChunkMetaData — по одному на каждую колонку.
├── total_byte_size: 2 621 440
├── num_rows: 100 000
├── ordinal: 0
└── columns: [ColumnChunkMetaData × 3]

ColumnChunkMetaData

Метаданные одного column chunk. Включает путь к колонке, кодек, смещение в файле, размеры (сжатый/несжатый), кодировки и статистики min/max/null_count.
├── file_path: null (данные в этом же файле)
├── file_offset: 4
├── codec: ZSTD
├── type: INT64
├── encodings: [RLE_DICTIONARY, PLAIN]
├── total_compressed_size: 412 672
├── total_uncompressed_size: 800 000
└── statistics: Statistics
TIP

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-based pruning
SELECT * FROM users WHERE user_id > 150000
Row Group 0Statistics показывают max_value=100000. Все значения ≤ 100000, а запрос ищет > 150000. Группа безопасно пропускается — 0 I/O.
Row Group 1Statistics показывают min=100001, max=200000. Диапазон пересекается с условием > 150000 — группу нужно читать.
WARNING

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 + OffsetIndex: page-level pruning

ColumnIndex (user_id)

Page 0Статистики первой data page. min=1, max=50000. Ридер сопоставляет эти значения с предикатом запроса.
Page 1Статистики второй data page. min=50001, max=100000. Если запрос WHERE user_id > 80000, Page 0 пропускается.

OffsetIndex (user_id)

Page 0Физическое расположение Page 0 в файле. offset — начало, compressed_size — сколько байт прочитать. first_row_index — для корреляции со строками.
Page 1Физическое расположение Page 1. По offset + size ридер может выполнить точный Range Read на S3/GCS без чтения лишних байт.

Комбинация работает так:

  1. Ридер читает ColumnIndex из footer → определяет, какие страницы содержат нужные значения
  2. Ридер читает OffsetIndex → определяет byte ranges для нужных страниц
  3. Выполняет точечные чтения только нужных байтов (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-фильтр: point lookup workflow
WHERE city = 'Berlin'Входящий предикат — точное совпадение. Range-предикаты (>, <) не поддерживаются bloom-фильтрами — для них используйте Statistics.

Bloom Filter (city chunk)

Bloom-фильтр для column chunk колонки city. Split Block Bloom Filter (SBBF) — реализация в Parquet. Хэширует значение и проверяет биты.
Результат: NOT PRESENTBloom-фильтр гарантирует: значения 'Berlin' нет в этом chunk. Можно безопасно пропустить — 0% false negatives.
Результат: MAYBE PRESENTBloom-фильтр говорит 'возможно есть'. False positive rate зависит от FPP (false positive probability) — обычно 0.01–0.05. Нужно читать chunk и проверять.

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
 )
 }
)
NOTE

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.52018Page-level pruning
Bloom Filter (SBBF) спецификация2.62019Point-lookup pruning, повсеместная поддержка только с PyArrow 11+ / parquet-mr 1.13+ (~2023–2024)
VARIANT logical type2.11март 2025Semi-structured данные (см. урок про Dremel)
GEOMETRY / GEOGRAPHY logical types2.11март 2025Native geospatial типы и статистики
Statistics V2 (geospatial bbox, distinct count)2.11+март 2025 → 2026Histogram-based и геоориентированные статистики
NOTE

До 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.
GEOMETRY vs GEOGRAPHY logical types
Schema declarationLogical type аннотирует физический BYTE_ARRAY. Поле edge_interpolation определяет геометрию рёбер: PLANAR для GEOMETRY, SPHERICAL/VINCENTY для GEOGRAPHY.
GEOMETRYПланарная геометрия: рёбра — прямые в координатах x,y. Подходит для UTM, локальных проекций, картографии в плоскости.
GEOGRAPHYСферическая/эллипсоидальная геометрия: рёбра — большие круги. Подходит для глобальных датасетов (lat/lon в WGS84). Учитывает кривизну Земли.
WARNING

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
TIP

До 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’ам точнее оценивать селективность.

Ключевые выводы

  1. FileMetaData хранится в footer файла и содержит полную иерархию: схема → row groups → column chunks → statistics
  2. Statistics (min/max/null_count) — chunk-level, позволяют пропускать целые row groups
  3. ColumnIndex + OffsetIndex = Page Index (с Parquet 2.5, 2018) — page-level pruning
  4. Bloom-фильтры — для point lookups, вероятностное «точно нет / возможно есть» (повсеместная поддержка с PyArrow 11+ / parquet-mr 1.13+ ~2023–2024)
  5. Parquet 2.11 (март 2025) добавил VARIANT, GEOMETRY, GEOGRAPHY logical types и Statistics V2 (bbox, distinct count histogram)
  6. Многоступенчатая фильтрация: projection → row group pruning → page pruning → bloom filter → spatial pruning → чтение
Spark Catalyst: predicate pushdown и Parquet pruning DataFusion: pruning, projection и filter pushdown

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. ColumnIndex и OffsetIndex — дополнение к Footer statistics. Какую проблему они решают?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7