Learning Platform
Глоссарий Troubleshooting
Урок 10.06 · 23 мин
Средний
hive-partitioningpartition-pruningparquetexternal-data

Hive-партиционированные датасеты и partition pruning

В прошлых уроках мы пропускали лишние row groups внутри Parquet-файла через filter pushdown. Hive-партиционирование поднимает ту же идею на уровень выше: пропускать не куски файла, а целые файлы и папки, даже не открывая их. Если датасет правильно разложен по папкам, запрос с фильтром по дате может потрогать один файл из тысячи. Этот урок — про то, как именно это устроено, потому что partition pruning не магия: он работает только при конкретной раскладке файлов и конкретной форме запроса.

Что такое Hive-партиционирование

Trino + Iceberg: hidden partitioning и partition evolution ClickHouse: PARTITION BY — жизненный цикл, не оптимизация запросов

Hive-партиционирование — это соглашение об именовании папок, пришедшее из Apache Hive и ставшее де-факто стандартом для озёр данных. Суть: значение колонки кодируется прямо в имени папки в виде ключ=значение. Файлы датасета раскладываются по дереву таких папок.

events/
  year=2025/
    month=11/
      data_0.parquet
      data_1.parquet
    month=12/
      data_0.parquet
  year=2026/
    month=01/
      data_0.parquet
    month=02/
      data_0.parquet

Здесь year и month — партиционные колонки. Их значений нет внутри Parquet-файлов — они есть только в путях. Файл events/year=2026/month=01/data_0.parquet содержит все колонки события (user_id, event_type, …), кроме year и month: эти двое закодированы папками. Партиционирование экономит место (значение не дублируется в каждой строке файла) и, главное, делает структуру датасета видимой по одним именам путей.

Hive-партиционирование: значение колонки в имени папки
events/year=2026/month=01/data_0.parquetПуть файла кодирует year=2026 и month=01; этих колонок внутри файла нет
разбор пути
Партиционные колонки year, monthDuckDB извлекает пары ключ=значение из пути и добавляет их как обычные колонки таблицы
плюс колонки из файла
Полная строка таблицыКолонки из файла плюс партиционные колонки из пути — единая реляционная строка

Разбор путей: партиционные колонки из имён папок

Когда DuckDB читает датасет глобом read_parquet('events/**/*.parquet'), он смотрит на пути совпавших файлов. Увидев в пути сегменты вида ключ=значение, он распознаёт Hive-раскладку: автоматически добавляет к таблице колонки year и month и заполняет их значениями, разобранными из пути каждого файла.

-- Hive-партиционирование определяется автоматически
SELECT user_id, event_type, year, month
FROM read_parquet('events/**/*.parquet')
LIMIT 3;
┌─────────┬────────────┬───────┬───────┐
│ user_id │ event_type │ year  │ month │
│  int64  │  varchar   │ int64 │ int64 │
├─────────┼────────────┼───────┼───────┤
│    4012 │ click      │  2026 │     1 │
│    4012 │ purchase   │  2026 │     1 │
│    5530 │ view       │  2026 │     2 │
└─────────┴────────────┴───────┴───────┘

Колонки year и month ведут себя как обычные: по ним можно фильтровать, группировать, джойнить — хотя физически они выведены из путей, а не прочитаны из файлов. Управляет распознаванием опция hive_partitioning. По умолчанию авто; hive_partitioning = true форсирует разбор, false — отключает (тогда year=2026 останется просто частью имени файла и колонки не появятся).

Типы партиционных колонок DuckDB по умолчанию пытается вывести: year=2026 станет числом. Опция hive_types позволяет задать типы явно: hive_types = {'year': 'INTEGER', 'month': 'INTEGER'} — это убирает неоднозначность и ускоряет разбор.

NOTE

Парный к чтению механизм — запись. COPY (...) TO 'events' (FORMAT parquet, PARTITION_BY (year, month)) сама разложит данные по папкам year=.../month=.... То есть Hive-раскладку DuckDB и читает, и создаёт. Запись с PARTITION_BY детально разбирается в модуле про запись данных; здесь важно, что датасет такой формы — естественный результат экспорта из DuckDB.

Partition pruning: пропуск целых партиций

Вот ради чего всё затевалось. Раз партиционные колонки выводятся из путей, DuckDB знает значение year и month каждого файла до того, как файл открыт — достаточно посмотреть на путь. Значит, фильтр по партиционной колонке можно применить к списку путей и выкинуть неподходящие файлы целиком.

Запрос:

SELECT COUNT(*)
FROM read_parquet('events/**/*.parquet')
WHERE year = 2026 AND month = 1;

DuckDB при планировании:

  1. Раскрывает glob — получает список всех путей датасета.
  2. Из каждого пути извлекает year и month.
  3. Применяет к этому списку предикат year = 2026 AND month = 1.
  4. Оставляет только пути, где предикат истинен — здесь это единственный файл events/year=2026/month=01/data_0.parquet.
  5. Сканирует только его. Файлы за 2025 год и за month=02 не открываются вообще — ни footer, ни данные.

Это и есть partition pruning (отсечение партиций). Разница с filter pushdown по row groups — в уровне: pushdown отбрасывает куски внутри открытого файла, pruning отбрасывает файлы и папки до открытия.

Partition pruning: фильтр отсекает файлы по путям
WHERE year = 2026Предикат по партиционной колонке проверяется на путях файлов на этапе планирования
разбор путей
year=2025/...parquetПуть даёт year=2025, предикат ложен — файл не открывается совсем
year=2026/...parquetПуть даёт year=2026, предикат истинен — только этот файл идёт в сканирование

Две ступени отсечения работают вместе

Partition pruning и filter pushdown не исключают друг друга — они складываются в две ступени одной воронки:

  1. Partition pruning по фильтру на партиционных колонках выкидывает целые файлы по путям, не открывая их.
  2. Среди оставшихся файлов filter pushdown по фильтру на обычных колонках сверяет предикат со статистикой row groups в footer и пропускает лишние row groups внутри файла.
SELECT user_id, event_type
FROM read_parquet('events/**/*.parquet')
WHERE year = 2026          -- ступень 1: partition pruning, отсекает файлы
  AND user_id = 4012;      -- ступень 2: filter pushdown, отсекает row groups

Фильтр year = 2026 оставит файлы только за 2026-й. Фильтр user_id = 4012 внутри этих файлов отсечёт row groups, чьи диапазоны min/max не покрывают 4012. Вместе они сводят объём чтения к минимуму. Удачное проектирование датасета — выбрать партиционными те колонки, по которым чаще всего фильтруют (обычно дата), а файлы внутри партиций сортировать по второй частой колонке фильтра.

Как доказать, что pruning сработал

EXPLAIN покажет, сколько файлов попало в сканирование после отсечения. У оператора PARQUET_SCAN (или READ_PARQUET) в плане видно число файлов и список путей. Сравните EXPLAIN запроса с фильтром по партиции и без него: с фильтром файлов в плане должно быть кратно меньше.

EXPLAIN ANALYZE добавит фактические цифры исполнения — прочитанные строки. Если partition pruning работает, запрос с предикатом year = 2026 прочитает строки только из файлов 2026 года, и Rows scanned будет соответствовать их объёму, а не объёму всего датасета.

EXPLAIN
SELECT COUNT(*) FROM read_parquet('events/**/*.parquet')
WHERE year = 2026 AND month = 1;
-- в плане PARQUET_SCAN -> Total Files Read: 1 (а не все файлы датасета)

Где partition pruning ломается

Это главная практическая часть. Pruning отсекает файлы только если может вычислить предикат по пути на этапе планирования. Несколько ситуаций, где он молча перестаёт работать, а запрос всё равно даёт верный результат — просто медленно:

  • Фильтр по обычной колонке, не партиционной. WHERE event_type = 'click' не отсекает ни одного файла — event_type не закодирован в путях. Здесь работает только filter pushdown по row groups, и то если файлы кластеризованы по event_type.
  • Партиционная колонка обёрнута функцией. WHERE CAST(year AS VARCHAR) = '2026' или WHERE year + 0 = 2026 — DuckDB не всегда может протолкнуть предикат через функцию, и pruning отключается. Сравнивайте партиционную колонку напрямую: WHERE year = 2026.
  • Раскладка не Hive. Если папки названы просто 2026/01/ без ключ=значение, DuckDB не распознает партиционирование и колонки не выведет. Нужна либо Hive-раскладка, либо ручной разбор пути через колонку filename.
  • hive_partitioning = false или формат путей, который парсер не распознал — партиционных колонок не будет, отсекать нечего.
WARNING

Главная коварность partition pruning: когда он не срабатывает, ошибки нет. Запрос возвращает абсолютно правильный результат — просто читает весь датасет вместо одной партиции. На маленьких данных вы этого не заметите, на больших получите запрос, который «почему-то медленный». Поэтому не доверяйте предположениям: проверяйте число файлов в EXPLAIN после каждого изменения фильтра. Если добавили WHERE по партиции, а число файлов в плане не упало — pruning не сработал, ищите причину в списке выше.

Попробуй сам

Постройте партиционированный датасет и проверьте pruning измерением.

  1. Сгенерируйте данные с колонками year, month и запишите их партиционированно: COPY (SELECT ...) TO 'events' (FORMAT parquet, PARTITION_BY (year, month)). Посмотрите получившееся дерево папок — убедитесь, что папки названы year=.../month=....
  2. Выполните SELECT DISTINCT year, month FROM read_parquet('events/**/*.parquet') и убедитесь, что партиционные колонки выведены из путей и доступны как обычные.
  3. Сравните два EXPLAIN: ... WHERE year = 2026 AND month = 1 и запрос вообще без WHERE. Найдите у PARQUET_SCAN число прочитанных файлов — с фильтром оно должно быть кратно меньше.
  4. Теперь выполните EXPLAIN ... WHERE CAST(year AS VARCHAR) = '2026'. Сравните число файлов с вариантом WHERE year = 2026. Объясните, почему оборачивание партиционной колонки в CAST ломает partition pruning.

Hive partitioning в Parquet: стандарт и его ограничения
Проверка знанийKnowledge check
Что такое partition pruning при чтении Hive-партиционированного датасета, чем он отличается от filter pushdown по row groups, и почему он может незаметно не сработать?
ОтветAnswer
Hive-партиционирование — это соглашение, при котором значение колонки кодируется в имени папки в виде ключ=значение (например events/year=2026/month=01/), и файлы датасета раскладываются по дереву таких папок. DuckDB при чтении такого датасета разбирает пути файлов, извлекает из них партиционные колонки (year, month) и добавляет их к таблице как обычные колонки. Partition pruning — это отсечение целых файлов и папок по фильтру на партиционных колонках: поскольку DuckDB знает значение year и month каждого файла из его пути ещё до открытия файла, фильтр WHERE year = 2026 применяется к списку путей на этапе планирования, и не подходящие файлы не открываются вообще — ни их footer, ни данные. Отличие от filter pushdown по row groups в уровне отсечения: filter pushdown работает внутри уже открытого файла, сверяя предикат со статистикой min/max row groups в footer и пропуская лишние row groups; partition pruning работает на уровень выше, отбрасывая целые файлы до их открытия. На практике обе ступени складываются: pruning по партиционным колонкам выкидывает файлы, затем pushdown по обычным колонкам пропускает row groups внутри оставшихся файлов. Partition pruning может незаметно не сработать в нескольких случаях: фильтр по обычной, не партиционной колонке; партиционная колонка обёрнута функцией (CAST(year AS VARCHAR) = '2026') — предикат не проталкивается через функцию; раскладка папок не в формате ключ=значение; hive_partitioning отключён. Коварность в том, что при несработавшем pruning ошибки нет — запрос возвращает правильный результат, просто читает весь датасет вместо одной партиции и оказывается медленным. Поэтому эффект надо проверять числом прочитанных файлов в выводе EXPLAIN.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Как устроено Hive-партиционирование и откуда DuckDB берёт значения партиционных колонок?

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

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

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

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