Learning Platform
Глоссарий Troubleshooting
Урок 15.03 · 23 мин
Средний
partitioninghive-layoutpartition-pruningcopy

Партиционированная запись: COPY … PARTITION_BY и Hive-layout

В прошлом уроке COPY писала результат в один файл. Для умеренных объёмов этого достаточно. Но когда данных много и их будут многократно читать с фильтрами, один монолитный файл — не лучшая раскладка. Лучше разложить данные на диске по партициям, так чтобы запрос с фильтром мог пропустить целые куски, даже не открывая их. Этот урок — про COPY ... PARTITION_BY и Hive-layout.

Партиционированная запись — это не «ещё одна опция COPY», а способ заранее организовать данные под будущие чтения. Понимание этой темы напрямую связывает запись данных с производительностью запросов.


Проблема монолитного файла

Представьте таблицу events за три года — сотни миллионов строк, один Parquet-файл. Типичный запрос смотрит один месяц: WHERE event_date BETWEEN '2026-03-01' AND '2026-03-31'.

Что делает движок? Открывает единственный файл и сканирует его. Filter pushdown и zonemap-статистика Parquet помогут пропустить часть row group — но только если данные внутри файла удачно упорядочены по дате. Если строки за март разбросаны по всему файлу, пропустить почти нечего: придётся прочитать большую часть файла, чтобы выловить мартовские строки.

Корень проблемы: физическая раскладка данных не отражает то, как их фильтруют. Запросы режут по дате, а файл по дате никак не структурирован.


Идея партиционирования: раскладка под фильтр

Партиционирование решает это так: данные физически раскладываются по отдельным файлам/каталогам по значению некой колонки — колонки партиционирования. Все строки за март лежат в одном месте, все за апрель — в другом, и так далее.

Теперь запрос WHERE event_date в марте может полностью пропустить каталоги всех остальных месяцев — даже не открывая эти файлы. Читается только то, что относится к нужной партиции. Это называется partition pruning (отсечение партиций).

Монолит против партиционирования при запросе одного месяца
Один большой файлВсе три года в одном файле; строки за март разбросаны по всему объёму
запрос марта
Сканируется почти весь файлМартовские строки разбросаны, пропустить мало что можно
Партиции по месяцамКаждый месяц — отдельный каталог с файлами
запрос марта
Читается только каталог мартаКаталоги других месяцев пропущены целиком — partition pruning

COPY … PARTITION_BY

DuckDB пишет партиционированные данные опцией PARTITION_BY:

COPY events
TO 'events_dataset'              -- путь к каталогу набора данных
(FORMAT parquet, PARTITION_BY (year, month));

Здесь events_dataset — это не файл, а каталог. DuckDB создаст внутри него структуру подкаталогов по уникальным комбинациям year и month, и в каждый положит Parquet-файлы со строками соответствующей партиции.

Можно партиционировать по результату выражения — например, выделить год и месяц из даты прямо в COPY:

COPY (
  SELECT *,
         year(event_date) AS year,
         month(event_date) AS month
  FROM events
)
TO 'events_dataset'
(FORMAT parquet, PARTITION_BY (year, month));
WARNING

Выбор колонки партиционирования критичен. Хорошая колонка — та, по которой часто фильтруют, и с умеренным числом уникальных значений (год, месяц, регион, категория). Плохой выбор — колонка высокой кардинальности (user_id, timestamp с точностью до секунды): получатся миллионы крошечных файлов, и это убьёт производительность сильнее, чем монолит. Правило: партиций должно быть достаточно для отсечения, но не настолько много, чтобы каждая стала микрофайлом.


Hive-layout: соглашение об именах каталогов

DuckDB именует партиционные каталоги по соглашению Hive partitioning — стандарту, пришедшему из экосистемы Apache Hive и понятному множеству инструментов (Spark, Presto/Trino, Arrow, pandas).

Суть проста: каталог называется имя_колонки=значение. Для PARTITION_BY (year, month) структура на диске выглядит так:

events_dataset/
├── year=2024/
│   ├── month=1/
│   │   └── data_0.parquet
│   ├── month=2/
│   │   └── data_0.parquet
│   └── ...
├── year=2025/
│   ├── month=1/
│   │   └── data_0.parquet
│   └── ...
└── year=2026/
    └── ...

Ключевая выгода Hive-layout: значение партиции закодировано прямо в пути. Каталог year=2026/month=3 сам говорит, что внутри — данные за март 2026. Это даёт две вещи:

  1. Колонки партиционирования не дублируются в файлах. year и month не хранятся в Parquet-файлах — они и так в пути. Экономия места.
  2. Движок выводит значение партиции из пути. Читая year=2026/month=3/data_0.parquet, DuckDB знает: все строки здесь имеют year=2026, month=3, и подставляет эти колонки автоматически.
Hive-layout: значение партиции в пути каталога
year=2026Каталог первого уровня партиционирования; колонка year, значение 2026
month=3Каталог второго уровня; колонка month, значение 3
data_0.parquetФайл данных; колонки year и month в нём не хранятся — они в пути

Чтение партиционированных данных и pruning

Записанный с PARTITION_BY набор читается так же просто, как обычный файл — через glob по каталогу:

-- читаем весь партиционированный набор
SELECT count(*) FROM 'events_dataset/**/*.parquet';

-- DuckDB распознаёт Hive-структуру: year и month доступны как колонки,
-- хотя в файлах их нет
SELECT year, month, count(*)
FROM 'events_dataset/**/*.parquet'
GROUP BY year, month;

Главное происходит при фильтре по колонке партиционирования:

SELECT count(*)
FROM 'events_dataset/**/*.parquet'
WHERE year = 2026 AND month = 3;
-- DuckDB прочитает ТОЛЬКО каталог year=2026/month=3,
-- остальные партиции пропущены целиком — partition pruning

DuckDB видит из путей, какие партиции существуют, сопоставляет фильтр year = 2026 AND month = 3 с именами каталогов и открывает только подходящий. Все файлы других месяцев и лет даже не читаются. На наборе за три года запрос одного месяца коснётся примерно 1/36 данных вместо всего объёма.

Это можно увидеть в плане:

EXPLAIN
SELECT count(*) FROM 'events_dataset/**/*.parquet'
WHERE year = 2026 AND month = 3;

В выводе PARQUET_SCAN (или READ_PARQUET) покажет, что в чтение попал лишь ограниченный набор файлов — следов всех 36 партиций там не будет.

TIP

Partition pruning срабатывает, только если фильтр идёт по колонке партиционирования. Запрос WHERE year = 2026 отсекает партиции; запрос WHERE amount > 1000 (amount не колонка партиционирования) отсечь партиции не может — придётся читать все, и внутри них работать уже filter pushdown по zonemap. Поэтому партиционировать имеет смысл именно по тем колонкам, по которым фильтруют чаще всего.


Партиционирование как часть дизайна данных

Партиционированная запись — это решение, которое принимают на этапе проектирования датасета, заранее зная профиль будущих запросов. Несколько практических ориентиров.

Партиционируйте по колонкам частых фильтров. Дата (год/месяц), регион, категория, тип события — типичные удачные кандидаты. Профиль запросов первичен: смотрите, по чему режут чаще всего.

Следите за размером партиций. Идеал — партиции, дающие файлы разумного размера (условно — от десятков до сотен мегабайт каждый). Слишком крупная партиция теряет смысл отсечения; слишком мелкая порождает проблему «множества крошечных файлов», когда накладные расходы на открытие каждого файла перевешивают выгоду.

Не партиционируйте по высококардинальным колонкам. user_id, точный timestamp, order_id — по ним получится столько партиций, что каждая станет микрофайлом. Это антипаттерн.

Несколько уровней — для иерархичных фильтров. PARTITION_BY (year, month) оправдан, когда фильтруют и по году, и по году+месяцу. Лишние уровни без нужды только дробят данные.

Колонка партиционированияУдачно?Почему
year, monthдачастый фильтр, умеренная кардинальность
region, countryдачастый фильтр, мало уникальных значений
event_type, categoryобычно даесли по ним фильтруют и значений немного
user_idнетвысокая кардинальность -> миллионы микрофайлов
точный timestampнетпочти уникальна -> партиция на строку

Партиционирование и дозапись

Партиционированный набор — не «слепок раз и навсегда». В него можно дописывать новые данные, и Hive-layout делает это естественным.

Когда приходит новая порция данных — скажем, события за свежий месяц, — повторный COPY ... PARTITION_BY с этими данными добавит новые каталоги партиций (year=2026/month=6/...), не трогая существующие. Каждая партиция — это отдельный набор файлов в своём каталоге, поэтому добавление новой партиции не требует переписывать старые. Это удобно для инкрементальных пайплайнов: каждый запуск дописывает партицию за новый период.

Тонкость касается дозаписи в уже существующую партицию. Если новые данные относятся к месяцу, для которого каталог уже есть, в этот каталог просто добавятся новые файлы рядом со старыми. Партиция при этом может накопить много файлов — и здесь возникает та же проблема мелких файлов, но уже внутри одной партиции. Лечится она периодической перезаписью партиции: прочитать всё её содержимое и записать обратно в меньшее число файлов покрупнее (операция compaction).

NOTE

Партиционированная запись особенно хорошо сочетается с временными данными: партиция на период (день, месяц) — и каждый инкрементальный запуск пайплайна дописывает свою партицию, не трогая историю. Старые партиции при этом легко и удалять — это просто удаление каталога. Управление жизненным циклом данных «по периодам» становится операцией над каталогами файловой системы.


Попробуй сам

  1. Создай таблицу с датами за несколько лет: CREATE TABLE events AS SELECT range AS id, DATE '2024-01-01' + INTERVAL (range % 900) DAY AS d, random()*100 AS amount FROM range(2000000);
  2. Добавь колонки партиционирования и запиши партиционированный набор: COPY (SELECT *, year(d) AS y, month(d) AS m FROM events) TO 'ev_ds' (FORMAT parquet, PARTITION_BY (y, m));
  3. Посмотри структуру каталога ev_ds в файловой системе. Видишь ли подкаталоги вида y=2024/m=1/? Это Hive-layout.
  4. Прочитай набор и проверь, что y и m доступны как колонки: SELECT y, m, count(*) FROM 'ev_ds/**/*.parquet' GROUP BY y, m; Хотя в файлах этих колонок нет.
  5. Выполни EXPLAIN для SELECT count(*) FROM 'ev_ds/**/*.parquet' WHERE y = 2025 AND m = 6; и для SELECT count(*) FROM 'ev_ds/**/*.parquet' WHERE amount > 50; Сравни, сколько файлов попадает в чтение в каждом случае. Объясни разницу через partition pruning.
Hive partitioning в Parquet: соглашение о директориях
Проверка знанийKnowledge check
Как COPY ... PARTITION_BY с Hive-layout ускоряет последующие запросы, и почему партиционировать по высококардинальной колонке (например, user_id) — антипаттерн?
ОтветAnswer
COPY ... PARTITION_BY раскладывает данные физически по отдельным каталогам по значению колонки партиционирования: все строки за март в одном каталоге, за апрель в другом и так далее. Имена каталогов следуют соглашению Hive partitioning — каталог называется имя_колонки=значение (например, year=2026/month=3). Это даёт две вещи. Во-первых, значение партиции закодировано прямо в пути, поэтому колонки партиционирования не дублируются в самих файлах (экономия места), а DuckDB выводит их значения из пути автоматически. Во-вторых и главное — partition pruning: когда запрос фильтрует по колонке партиционирования (WHERE year = 2026 AND month = 3), DuckDB сопоставляет фильтр с именами каталогов и открывает только подходящий каталог, полностью пропуская файлы всех остальных партиций — они даже не читаются. На наборе за три года запрос одного месяца коснётся примерно 1/36 данных. Партиционировать по высококардинальной колонке вроде user_id — антипаттерн, потому что число партиций равно числу уникальных значений: для user_id это миллионы партиций, то есть миллионы крошечных файлов. Накладные расходы на открытие каждого микрофайла перевешивают любую выгоду от отсечения, и производительность падает сильнее, чем у монолитного файла. Правило: колонка партиционирования должна быть частым фильтром с умеренной кардинальностью (год, месяц, регион, категория) — партиций достаточно для отсечения, но не настолько много, чтобы каждая стала микрофайлом.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое partition pruning при чтении партиционированного набора данных?

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

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

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

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