Learning Platform
Глоссарий Troubleshooting
Урок 16.05 · 24 мин
Средний
partitioningfile-formatssmall-filestuning

Сквозной тюнинг: партиционирование, форматы и размеры файлов

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

Три рычага этого слоя: схема партиционирования (как данные разбиты на куски), файловый формат (как байты внутри файла организованы) и размер файлов (проблема мелких файлов). Этот урок разбирает все три и показывает, как Trino-инструменты помогают их настроить.


Партиционирование: меньше читать

Партиционирование — это разбиение таблицы на физические части по значению одной или нескольких колонок. Таблица событий, партиционированная по дню, физически разложена по каталогам-партициям: данные за каждый день — отдельная группа файлов. Когда запрос фильтрует по дню, Trino применяет partition pruning — пропускает целые партиции, не открывая ни одного их файла.

Эффект прямой: запрос с WHERE event_date = DATE '2026-05-11' по таблице за три года читает 1 партицию из примерно 1100. Это не оптимизация выполнения — это сокращение работы на порядки ещё до того, как выполнение началось.

В Iceberg партиционирование задаётся при создании таблицы и поддерживает transforms — функции, превращающие значение колонки в значение партиции:

CREATE TABLE iceberg.analytics.events (
  event_id   BIGINT,
  user_id    BIGINT,
  event_type VARCHAR,
  event_time TIMESTAMP(6),
  payload    VARCHAR
)
WITH (
  partitioning = ARRAY['day(event_time)', 'event_type']
);

Здесь day(event_time) — transform: партиция определяется днём из timestamp, отдельная колонка-дата не нужна. Это hidden partitioning Iceberg: запросы фильтруют по event_time, а Trino сам выводит партиции. Доступные transforms: year, month, day, hour, bucket(col, n), truncate(col, n), identity.

Ключевое решение — гранулярность. Здесь баланс, и обе крайности плохи.

Гранулярность партиционирования: баланс крайностей
Слишком крупноПартиционирование по году. Партиций мало, но каждая огромна — pruning почти не сужает скан, запрос всё равно читает много.
ищем баланс
УдачноПартиция соответствует типичному фильтру запросов и содержит файлы разумного размера — сотни мегабайт каждый.
перебор
Слишком мелкоПартиционирование по часу и ещё по двум колонкам. Партиций десятки тысяч, в каждой крошечные файлы — взрыв метаданных и мелких файлов.

Слишком крупные партиции (по году) почти не дают pruning — partition pruning не сужает скан. Слишком мелкие (по часу плюс ещё две колонки) порождают десятки тысяч партиций с крошечными файлами: метаданных становится больше, чем данных, а планирование запроса само по себе замедляется. Практическое правило: партиция должна соответствовать типичному фильтру запросов и при этом содержать файлы разумного размера. Для большинства событийных таблиц это партиционирование по дню; колонку с очень высокой кардинальностью (user_id) в партиционирование не берут — берут bucket(user_id, N), который раскладывает значения по фиксированному числу бакетов.

TIP
Apache Iceberg: hidden partitioning и partition evolution

Partition pruning срабатывает, только когда фильтр приложен к партиционирующему выражению напрямую. WHERE event_time >= TIMESTAMP '2026-05-01 00:00:00' сужает скан; WHERE cast(event_time AS date) = DATE '2026-05-11' или WHERE date_format(event_time, '%Y') = '2026' могут помешать pruning, потому что фильтр обёрнут в функцию над колонкой. Проверяйте через EXPLAIN, сколько партиций реально остаётся после фильтра.


Файловый формат: как организованы байты

Партиционирование решает, какие файлы открыть. Формат файла решает, насколько эффективно читается то, что внутри. Для аналитики на Trino стандарт — колоночные форматы: Parquet и ORC. Понимать, почему они быстрее построчных, важно для тюнинга.

Parquet: row groups, страницы и min/max статистика

Колоночный файл хранит значения одной колонки подряд. Это даёт три механизма ускорения. Первый — колоночное чтение: запрос SELECT user_id, event_type физически читает с диска только эти две колонки, а не весь файл; projection pushdown работает на уровне байт. Второй — сжатие: соседние значения одной колонки однородны, поэтому сжимаются в разы лучше разнотипных строк. Третий — пропуск блоков по статистике: и Parquet, и ORC бьют файл на крупные блоки (row groups в Parquet, stripes в ORC) и хранят для каждого блока min/max значений колонок. Если фильтр WHERE amount > 1000 не пересекается с диапазоном min/max блока, блок не читается вообще.

Уровни отсева данных при чтении lakehouse-таблицы
Уровень партицийPartition pruning: по фильтру отбрасываются целые партиции. Самый крупный отсев — файлы даже не открываются.
Уровень блоков файлаRow group / stripe pruning: по min/max статистике блока внутри Parquet или ORC пропускаются блоки, не пересекающиеся с фильтром.
Уровень колонокProjection pushdown: с диска читаются байты только запрошенных колонок, остальные колонки не трогаются.
Dynamic filteringРантайм-фильтр с build-стороны join проталкивается в reader и дополнительно отсекает row groups уже во время исполнения.

Эти уровни работают вместе: партиции отсекают группы файлов, статистика блоков — куски внутри файла, projection — ненужные колонки, dynamic filtering добавляет рантайм-отсев по join-ключу. Колоночный формат — то, что делает три нижних уровня возможными; на построчном CSV или JSON не работает ни один из них, поэтому такие форматы для горячих аналитических таблиц непригодны. CSV и JSON остаются разумны только для зоны приёма сырых данных, откуда их сразу перекладывают в Parquet или ORC.

ФорматТипАналитика на Trino
ParquetКолоночныйСтандарт для lakehouse, широкая совместимость
ORCКолоночныйСильная компрессия и статистика, исторически из Hive
AvroПострочный, со схемойДля приёма и потоков, не для горячих сканов
CSV / JSONПострочный, без статистикиТолько зона приёма сырых данных

Проблема мелких файлов

Третий рычаг — размер файлов, и это самая частая болезнь продакшен-lakehouse. Каждое чтение файла из object storage — это отдельный сетевой запрос с фиксированными накладными расходами: установка соединения, round-trip, открытие, чтение футера с метаданными. На файле в 256 MB эти накладные расходы незаметны. На файле в 200 KB они доминируют — Trino тратит больше времени на «открыть-закрыть», чем на полезное чтение.

Мелкие файлы накапливаются естественно. Каждый частый INSERT, каждый микробатч стрима, каждая запись в партицию добавляет новые файлы — и почти всегда мелкие. Через месяц частых вставок партиция вместо нескольких крупных файлов содержит тысячи крошечных. Симптомы в диагностике, которую вы уже умеете снимать: в EXPLAIN ANALYZE у source-стадии огромное число splits при скромном объёме данных; в Web UI source-стадия долго планируется и копит wall-время; растёт нагрузка на metastore.

Лечение — компакция: переписать много мелких файлов в несколько крупных. В Iceberg это процедура OPTIMIZE:

-- Скомпактить файлы мельче порога во всей таблице
ALTER TABLE iceberg.analytics.events EXECUTE optimize;

-- Скомпактить только свежую партицию (дешевле и точечнее)
ALTER TABLE iceberg.analytics.events EXECUTE optimize
WHERE event_time >= TIMESTAMP '2026-05-01 00:00:00';

OPTIMIZE читает мелкие файлы и пишет вместо них крупные; параметр file_size_threshold (по умолчанию около 100 MB) задаёт, что считать мелким. У Delta Lake есть аналогичная процедура optimize. Цель компакции — файлы в диапазоне примерно от сотни мегабайт до нескольких сотен: достаточно крупные, чтобы амортизировать накладные расходы на открытие, но не настолько, чтобы помешать параллелизму, ведь один файл — это обычно один или несколько splits.

WARNING

Компакция — не разовое действие. В активно пополняемой таблице мелкие файлы появляются постоянно, поэтому OPTIMIZE и сопутствующие операции обслуживания (expire_snapshots для удаления старых снапшотов, remove_orphan_files) ставят на расписание — обычно компактят свежие партиции по фильтру WHERE, а не всю таблицу целиком. Без регулярного обслуживания lakehouse деградирует постепенно и незаметно, пока запросы не станут ощутимо медленными.


Bucketing и порядок данных внутри файла

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

Колонку с очень высокой кардинальностью — например, user_id, где миллионы различных значений, — нельзя брать в партиционирование: получится взрыв партиций. Но если запросы часто фильтруют или джойнят по этой колонке, помогает bucketing. Bucketing раскладывает строки по фиксированному числу бакетов по хэшу значения: bucket(user_id, 64) в Iceberg-партиционировании означает 64 бакета вместо миллионов партиций. Запрос с фильтром по user_id тогда обращается только к нужным бакетам, а не ко всем файлам. Bucketing особенно ценен для join: если обе таблицы забакечены по join-ключу, Trino может соединять соответствующие бакеты без полного repartition.

Второй приём — порядок строк внутри файла. Вспомните механизм пропуска блоков по min/max статистике: row group пропускается, если диапазон значений колонки не пересекается с фильтром. Этот механизм работает тем лучше, чем плотнее значения сгруппированы. Если данные физически отсортированы по часто фильтруемой колонке, диапазоны min/max соседних row groups узкие и почти не перекрываются — фильтр отсекает много блоков. Если же та же колонка разбросана по файлу случайно, каждый row group покрывает почти весь диапазон значений, и min/max-статистика бесполезна — пропустить нельзя ничего. Поэтому при компакции через OPTIMIZE или при загрузке данных имеет смысл задавать сортировку по ключевой для фильтрации колонке.

ПриёмКогда применятьЧто даёт
ПартиционированиеКолонка средней кардинальности, частый фильтрPartition pruning: пропуск целых партиций
BucketingКолонка высокой кардинальности, фильтр или joinОбращение к нужным бакетам, эффективный join
Сортировка внутри файлаЧастый фильтр по диапазонуУзкие min/max, эффективный пропуск row groups

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


Сквозной чек-лист тюнинга хранения

Три рычага складываются в порядок действий: сначала убедиться, что запрос вообще читает только нужное, и лишь потом тюнить выполнение.

РычагВопросИнструмент проверки
ПартиционированиеСоответствует ли схема партиций реальным фильтрам?EXPLAIN: сколько партиций после фильтра
Формат файлаКолоночный ли формат у горячих таблиц?DDL таблицы, метаданные
Размер файловНет ли тысяч мелких файлов в партиции?EXPLAIN ANALYZE: число splits; $files
ОбслуживаниеРегулярно ли идёт OPTIMIZE и очистка снапшотов?История EXECUTE, расписание

Этот слой первичен. Filesystem caching из прошлого урока, статистика и dynamic filtering из модуля по CBO — всё это ускоряет работу с данными, которые запрос читает. Партиционирование, формат и размер файлов определяют, сколько данных он вообще читает. Сократить объём чтения почти всегда дешевле и эффективнее, чем ускорять чтение лишнего.


Попробуй сам

На Iceberg-каталоге (или используя tpch как источник данных) поэкспериментируйте с физической раскладкой.

  1. Создайте две версии таблицы из tpch.sf1.lineitem: одну без партиционирования, вторую с partitioning = ARRAY['month(shipdate)']. Залейте данные через CREATE TABLE ... AS SELECT.
  2. Выполните EXPLAIN ANALYZE для запроса с фильтром по shipdate на обеих таблицах. Сравните число splits и объём прочитанных данных на source-стадии.
  3. Сделайте в партиционированную таблицу несколько отдельных мелких INSERT (по несколько тысяч строк). Посмотрите метадату-таблицу $files — сколько файлов и какого размера появилось.
  4. Выполните ALTER TABLE ... EXECUTE optimize и снова посмотрите $files: как изменилось число и размер файлов.
  5. Повторите EXPLAIN ANALYZE того же запроса после компакции и сравните число splits с шагом 3.

Цель — увидеть своими глазами, как партиционирование сокращает скан, а компакция убирает накладные расходы мелких файлов.


Проверка знанийKnowledge check
Аналитики жалуются: запросы по событийной таблице в Iceberg стали медленными. EXPLAIN ANALYZE показывает, что source-стадия порождает около 40000 splits при всего 12 GB прочитанных данных. Что это за проблема, почему она замедляет запрос и как её устранить, не меняя сам SQL?
ОтветAnswer
Это классическая проблема мелких файлов. Сорок тысяч splits на двенадцать гигабайт данных означает средний размер куска порядка трёхсот килобайт — таблица состоит из огромного числа крошечных файлов вместо разумного числа крупных. Замедление возникает потому, что каждое чтение файла из object storage несёт фиксированные накладные расходы: отдельный сетевой запрос, round-trip, открытие файла, чтение футера с метаданными. На крупном файле эти расходы амортизируются полезным чтением, а на файле в триста килобайт они доминируют — Trino тратит больше времени на открытие и закрытие файлов, чем на сам разбор данных. Кроме того, огромное число splits раздувает фазу планирования и создаёт нагрузку на metastore. Накопились мелкие файлы естественным путём: каждый частый INSERT или микробатч стрима добавляет новые мелкие файлы, и за время эксплуатации партиции заросли тысячами крошечных файлов. Устраняется это компакцией, без единой правки SQL: процедура ALTER TABLE ... EXECUTE optimize читает мелкие файлы и переписывает их в несколько крупных, в диапазоне примерно от сотни мегабайт до нескольких сотен — достаточно больших, чтобы амортизировать накладные расходы на открытие, но не настолько, чтобы помешать параллелизму. На активно пополняемой таблице важно понимать, что компакция не разовая: мелкие файлы появляются постоянно, поэтому OPTIMIZE ставят на расписание, обычно компактуя свежие партиции по фильтру WHERE вместе с очисткой старых снапшотов. После компакции тот же запрос породит на порядок меньше splits и заметно ускорится — при том что текст запроса не изменился.

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

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

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

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

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

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