Learning Platform
Глоссарий Troubleshooting
Урок 17.05 · 25 мин
Продвинутый
tuningstatisticsdynamic-filteringresource-groups

Тюнинг: EXPLAIN ANALYZE, статистика, resource groups

Платформа RetailScope работает: данные в Iceberg, справочники федерированы, витрины строятся. Но «работает» и «работает быстро под нагрузкой» — разные состояния. Этот урок — про тюнинг: мы соберём статистику таблиц, прочитаем EXPLAIN ANALYZE ключевого запроса, убедимся, что работает dynamic filtering, и настроим resource groups, чтобы дашборды и тяжёлая аналитика не мешали друг другу.

Тюнинг — это не угадывание. Это цикл: измерить, найти узкое место, применить рычаг, измерить снова. Все инструменты для него дал модуль 15 — здесь мы применяем их к реальному проекту.


Шаг 1: статистика таблиц

Cost-based optimizer Trino выбирает порядок join и тип распределения по статистике таблиц. После CTAS и серии INSERT статистика raw_sales может быть неполной — а без неё CBO работает вслепую и принимает плохие решения.

Сначала смотрим, что есть:

SHOW STATS FOR iceberg.retail.raw_sales;
-- column_name | data_size | distinct_values_count | nulls_fraction | row_count
-- product_id  |    ...    |        200000         |      0.0       |
-- store_id    |    ...    |          50           |      0.0       |
-- sale_ts     |    ...    |         2400          |      0.0       |
-- (all)       |           |                       |                | 59986052

Если NDV (distinct_values_count) или row count пусты, собираем статистику через ANALYZE:

ANALYZE iceberg.retail.raw_sales;
ANALYZE iceberg.retail.mart_sales_enriched;

ANALYZE вычисляет статистику и сохраняет в метаданные Iceberg. Iceberg обновляет базовую статистику и при записи через Trino, но ANALYZE нужен, когда данные меняли в обход Trino или когда нужна полная расширенная статистика. На что влияет статистика: row count и NDV дают оценку кардинальности; по ней CBO решает порядок join (сначала соединять то, что даст меньше строк) и тип распределения (BROADCAST для маленькой стороны, PARTITIONED для большой).

TIP

ANALYZE по большой таблице сам по себе стоит ресурсов. В RetailScope его ставят на расписание после регулярных загрузок данных — статистика должна отражать актуальное состояние таблицы. Устаревшая статистика иногда хуже отсутствующей: CBO уверенно строит план по цифрам, которые больше не верны.

Airflow: ANALYZE после загрузки — автоматизация через TrinoOperator

Шаг 2: EXPLAIN ANALYZE ключевого запроса

Берём типичный аналитический запрос RetailScope и снимаем EXPLAIN ANALYZE — реальный план с измеренными числами.

EXPLAIN ANALYZE
SELECT st.region, p.category,
       sum(s.quantity * s.unit_price - s.discount) AS net_revenue
FROM iceberg.retail.raw_sales s
JOIN postgresql.public.dim_product p ON s.product_id = p.product_id
JOIN postgresql.public.dim_store   st ON s.store_id   = st.store_id
WHERE s.sale_ts >= TIMESTAMP '2026-01-01 00:00:00'
GROUP BY st.region, p.category;

Сокращённый вывод горячего фрагмента:

Fragment 2 [SOURCE]
    CPU: 28.40m, Scheduled: 41.10m, Input: 14920183 rows (412MB)
    InnerJoin[criteria = ("product_id" = "product_id_0"), distribution = REPLICATED]
    │   CPU: 9.81m (34.54%), Output: 14920183 rows
    │   dynamicFilterAssignments = {product_id_0 -> #df_512}
    ├─ ScanFilterProject[table = iceberg:retail.raw_sales,
    │                    constraint = (sale_ts >= TIMESTAMP '2026-01-01 ...'),
    │                    dynamicFilters = {"product_id" = #df_512}]
    │      CPU: 12.06m (42.46%), Output: 14920183 rows
    │      Input: 14920183 rows
    └─ RemoteSource[sourceFragmentIds = [4]]
           Output: 200000 rows (3.4MB)

Читаем по алгоритму из модуля 15. Горячий фрагмент — Fragment 2 (максимум CPU). Горячий оператор — ScanFilterProject скана raw_sales (42.46% CPU) и InnerJoin (34.54%). Это ожидаемо — скан фактов и join тяжелее всего.

Важные позитивные сигналы. Первый — constraint = (sale_ts >= ...) в скане: фильтр по дате протолкнулся, partition pruning работает — скан вернул около 15 млн строк, а не все 60 млн. Второй — distribution = REPLICATED у join со справочником: dim_product мал (200 тыс. строк, 3.4 MB), Trino разослал его на все воркеры вместо дорогого repartition. Третий — dynamicFilters = {"product_id" = #df_512} в скане: dynamic filtering включился.


Шаг 3: dynamic filtering в действии

dynamicFilters в выводе выше — не деталь, а ключевой механизм. Разберём, что он даёт RetailScope.

Dynamic filtering собирает значения join-ключа с build-стороны (отфильтрованного справочника) в рантайм-предикат и применяет его к скану fact-таблицы. Без него скан raw_sales прочитал бы все строки за период, а лишние отсеялись бы только на join. С ним рантайм-фильтр по product_id проталкивается прямо в Parquet-reader — и Trino пропускает row groups, где нужных product_id нет, ещё на этапе чтения.

Dynamic filtering в запросе RetailScope
Build: dim_productСправочник из PostgreSQL, при необходимости с фильтром. Trino собирает с него множество значений product_id.
собрать product_id
Рантайм-фильтрДинамический предикат: множество product_id с build-стороны. Формируется во время исполнения запроса.
фильтр проталкивается в reader
Скан raw_salesParquet-reader применяет рантайм-фильтр: row groups без нужных product_id пропускаются, лишний I/O не тратится.

Особенно сильно dynamic filtering работает в RetailScope, когда аналитик фильтрует справочник: запрос WHERE p.category = 'Electronics' оставляет малую часть товаров — dynamic filter получается узким, и скан raw_sales пропускает огромную долю row groups. Чем избирательнее фильтр по dimension, тем больше выигрыш на скане фактов. Если в плане у скана фактов dynamicFilters нет, а join со справочником есть — это сигнал разобраться: тип join, тип ключа (для DOUBLE/REAL min/max-фильтр не строится).


Join distribution: BROADCAST против PARTITIONED

В выводе выше у join со справочником стояло distribution = REPLICATED. Это решение CBO стоит разобрать — оно одно из самых влиятельных для производительности RetailScope.

Когда Trino соединяет две таблицы, ему надо свести строки с одинаковым join-ключом на один воркер. Есть два способа. BROADCAST (в выводе — REPLICATED): build-сторона join — обычно меньшая таблица — копируется целиком на каждый воркер как хэш-таблица, после чего probe-сторона сканируется локально, без перемещения по сети. Быстро, но build-сторона должна влезть в память каждого воркера. PARTITIONED: обе таблицы перераспределяются по сети по хэшу join-ключа, так что соответствующие строки сходятся на одном воркере. Медленнее из-за шаффла обеих сторон, зато позволяет соединять две большие таблицы, используя суммарную память кластера.

В федеративных запросах RetailScope почти всегда работает BROADCAST, и это правильно: справочники из PostgreSQL малы, скопировать их на воркеры дёшево, а большая raw_sales остаётся на месте. Если бы CBO выбрал PARTITIONED здесь, он зря репартиционировал бы по сети миллиарды строк фактов. Именно поэтому статистика так важна: CBO выбирает BROADCAST, когда оценивает build-сторону достаточно маленькой, а оценку он берёт из статистики. Нет актуальной статистики — CBO может ошибиться и выбрать дорогой PARTITIONED там, где хватило бы BROADCAST.

Тип распределенияКак работаетКогда оптимален
BROADCAST (REPLICATED)Build-сторона копируется на все воркерыОдна сторона мала и влезает в память воркера
PARTITIONEDОбе стороны репартиционируются по хэшу ключаОбе стороны велики

По умолчанию join_distribution_type стоит в AUTOMATIC — CBO решает сам по статистике, и в RetailScope это правильный режим. Принудительно задавать тип через session property стоит только как точечный обход, когда вы уверены, что CBO ошибается, и понимаете почему.


Шаг 4: resource groups — изоляция нагрузки

Платформа обслуживает два класса нагрузки с разными требованиями. Дашборды — частые, лёгкие, требуют низкой задержки. Тяжёлая аналитика и пересборка витрин — редкие, ресурсоёмкие, терпят ожидание. Без изоляции один тяжёлый ad-hoc-запрос займёт кластер, и все дашборды встанут.

Решение — resource groups: иерархия групп с лимитами и приоритетами; каждый запрос попадает в группу, и группа ограничивает его долю кластера.

Файл etc/resource-groups.json:

{
  "rootGroups": [
    {
      "name": "dashboards",
      "softMemoryLimit": "40%",
      "hardConcurrencyLimit": 20,
      "maxQueued": 100,
      "schedulingPolicy": "fair"
    },
    {
      "name": "analytics",
      "softMemoryLimit": "60%",
      "hardConcurrencyLimit": 4,
      "maxQueued": 20
    }
  ],
  "selectors": [
    { "source": ".*dashboard.*", "group": "dashboards" },
    { "group": "analytics" }
  ]
}

Подключается через etc/resource-groups.properties:

resource-groups.configuration-manager=file
resource-groups.config-file=etc/resource-groups.json

Логика. Группа dashboards — высокий лимит конкурентности (20 одновременных запросов): дашбордов много, но каждый лёгкий. Группа analytics — низкий лимит (4): тяжёлые запросы не должны заполонить кластер; пятый встанет в очередь. softMemoryLimit делит память кластера между классами. selectors маршрутизируют: запрос с dashboard в источнике идёт в dashboards, остальные — в analytics.

Resource groups: изоляция двух классов нагрузки
Запросы дашбордовЧастые лёгкие запросы. Selector по источнику направляет их в группу dashboards.
selector
Группа dashboardsЛимит конкурентности 20, доля памяти 40%. Много лёгких запросов исполняются параллельно.
Тяжёлая аналитикаРедкие ресурсоёмкие запросы и пересборка витрин. Selector по умолчанию направляет их в analytics.
selector
Группа analyticsЛимит конкурентности 4, доля памяти 60%. Тяжёлые запросы не заполоняют кластер, лишние ждут в очереди.

Эффект: тяжёлый ad-hoc-запрос больше не блокирует дашборды — он ограничен своей группой analytics, а dashboards со своей долей памяти и слотов продолжает работать. В Web UI у каждого запроса видна его resource group, а запрос в состоянии QUEUED — это запрос, упёршийся в лимит своей группы.

WARNING

Resource groups управляют тем, как кластер делится между запросами, но не делают сами запросы быстрее. Если дашбордный запрос медленный из-за плохого плана, отсутствия статистики или мелких файлов — его лечат статистикой, EXPLAIN ANALYZE и компакцией, а не resource group. Resource groups решают другую задачу — изоляцию: чтобы один класс нагрузки не отъедал ресурсы у другого. Это разные рычаги для разных проблем.


Цикл тюнинга RetailScope

Сведём шаги в единый цикл — порядок, в котором тюнят платформу.

ШагДействиеЧем проверяем
1Собрать и поддерживать статистику таблицSHOW STATS, план до/после ANALYZE
2Снять EXPLAIN ANALYZE, найти горячий фрагмент и операторПроценты CPU в выводе
3Убедиться, что работают pruning и dynamic filteringconstraint и dynamicFilters в скане
4Проверить физический слой: партиции, размеры файлов$files, число splits
5Настроить resource groups для изоляции классов нагрузкиГруппа и state запроса в Web UI

Тюнинг — итеративен. После каждого изменения снимают EXPLAIN ANALYZE заново и сравнивают CPU горячего фрагмента с предыдущим замером. Цифры, а не ощущения, подтверждают, что рычаг сработал.


Попробуй сам

Проведите цикл тюнинга на платформе RetailScope.

  1. Выполните SHOW STATS FOR iceberg.retail.raw_sales. Заполнены ли row count и NDV? Если нет — выполните ANALYZE и посмотрите статистику снова.
  2. Снимите EXPLAIN ANALYZE федеративного запроса выручки по региону и категории. Найдите горячий фрагмент и горячий оператор по проценту CPU.
  3. В выводе найдите constraint (partition pruning по дате) и dynamicFilters на скане raw_sales. Оба механизма работают?
  4. Добавьте в запрос узкий фильтр по справочнику, например WHERE p.category = 'Electronics', и снова снимите EXPLAIN ANALYZE. Сравните число строк на скане raw_sales — как изменился эффект dynamic filtering?
  5. Создайте etc/resource-groups.json и etc/resource-groups.properties из урока, перезапустите кластер. Запустите тяжёлый запрос и параллельно несколько лёгких; в Web UI посмотрите, в какие группы они попали и какие встали в QUEUED.

Цель — пройти полный цикл «измерить, найти, применить, измерить» на реальном проекте и увидеть, как resource groups изолируют нагрузку.


Проверка знанийKnowledge check
Аналитик RetailScope жалуется, что его дашборд стал медленным. Коллега советует «настроить resource groups, это ускорит». Почему этот совет, скорее всего, не решит проблему, и какой будет правильный порядок диагностики?
ОтветAnswer
Совет, скорее всего, не решит проблему, потому что resource groups и скорость запроса — это про разные вещи. Resource groups управляют тем, как ресурсы кластера делятся между запросами: они задают лимиты конкурентности и долю памяти для классов нагрузки, чтобы один класс не отъедал ресурсы у другого. Это инструмент изоляции, а не ускорения: сам по себе он не делает ни один отдельный запрос быстрее — он лишь определяет, сколько запросов одновременно пустить и какую долю кластера им дать. Если дашбордный запрос медленный из-за своих внутренних причин, resource group эти причины не устранит. Поэтому сначала нужно понять, в чём именно медленность. Правильный порядок диагностики опирается на инструменты наблюдаемости. Первым делом — Web UI: посмотреть state запроса. Если запрос в состоянии QUEUED, он вообще не начал исполнение и ждёт слот в своей resource group — вот единственный случай, когда настройка групп уместна, и означает он, что кластеру или группе не хватает ёмкости под конкурентную нагрузку. Но если запрос в состоянии RUNNING, проблема в самом запросе, и тогда снимают EXPLAIN ANALYZE: находят горячий фрагмент по CPU и горячий оператор по проценту CPU. Дальше проверяют типичные причины медленного запроса RetailScope: собрана ли актуальная статистика таблиц — без неё CBO выбирает плохой порядок join и тип распределения, лечится через ANALYZE; сработал ли partition pruning по дате — в плане это constraint на скане; работает ли dynamic filtering — это dynamicFilters на скане фактов; нет ли проблемы мелких файлов — по метадате $files и числу splits, лечится компакцией через OPTIMIZE. Каждый рычаг применяют и проверяют, снимая EXPLAIN ANALYZE заново и сравнивая CPU горячего фрагмента. Resource groups в этой картине решают отдельную, не связанную со скоростью задачу — чтобы тяжёлый ad-hoc-запрос не блокировал дашборды; это полезно для платформы в целом, но конкретный медленный дашборд лечится статистикой, чтением плана и обслуживанием таблиц, а не настройкой групп.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Зачем в RetailScope выполняют ANALYZE для таблиц после загрузки данных?

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

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

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

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