Data Engineering и ETL Производительность и стоимость аналитической нагрузки
0%

Производительность и стоимость аналитической нагрузки

Производительность и стоимость аналитической нагрузки

Один и тот же ответ — «выручка по регионам за 90 дней» — можно получить за 0,8 секунды и полтора цента или за 4 минуты и двенадцать долларов. Разница почти никогда не в «мощности кластера». Она в том, сколько байт пришлось прочитать, сколько раз одно и то же было пересчитано и что из этого можно было приготовить заранее.

Это делает производительность в дата-платформе непохожей на производительность сервиса. Там мы боремся за миллисекунды латентности и упираемся в CPU и блокировки — об этом трек про производительность. Здесь главная величина одна: объём данных, прошедших через движок. Всё остальное — следствие. И у этой величины есть прямой ценник, что превращает оптимизацию из эстетики в арифметику: «мы ускорили запрос в 30 раз» здесь читается как «мы вернули компании 3 700 долларов в месяц».


1. Три ресурса и три модели биллинга

Любой аналитический запрос потребляет три разных ресурса, и в разных хранилищах вы платите за разные из них.

Ресурс Что это физически Как проявляется
Ввод-вывод байты, прочитанные из хранилища линейно растёт со сканированием, лечится отсечением
Вычисление CPU и память на распаковку, join, агрегацию, сортировку растёт с числом строк на выходе join’ов, лечится алгоритмом
Конкуренция доступ к общему пулу ресурсов очереди, рост p95 без роста нагрузки
Модель биллинга Примеры Что оптимизировать в первую очередь
По просканированным байтам BigQuery on-demand, Athena отсечение партиций и колонок; лишний SELECT * — прямой убыток
По времени работы кластера Snowflake, Databricks SQL утилизацию: не держать простаивающий кластер и не растягивать прогон
Свой кластер (фиксированный) ClickHouse, Trino на своих машинах пропускную способность на узел; счёт не зависит от запроса, но ёмкость конечна
Резервирование мощности BigQuery editions, Snowflake с фиксированным пулом распределение между нагрузками и предсказуемость очереди

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


2. Жизнь запроса: где именно уходит время

Из схемы видны четыре места, где сгорает бюджет, в порядке убывания частоты:

  1. Не сработало отсечение — движок читает всю таблицу вместо одной партиции.
  2. Читаются лишние колонкиSELECT * в колоночном хранилище означает «прочитать все 60 колонок вместо трёх».
  3. Данные едут по сети между стадиями — shuffle на join и агрегации, разобранный в главе про пакетную обработку.
  4. Не хватило памяти — сортировка или join разливается на диск, и запрос замедляется в разы.

3. Как читать план запроса

EXPLAIN в распределённом движке отличается от плана в OLTP-базе (см. «Индексы и планы запросов»): здесь нет индексов, а есть стадии, обмены и статистика по файлам.

Fragment 1 [HASH]                                  -- стадия после обмена
  Output: region, sum(amount)
  Aggregate(FINAL)                                  <- финальная агрегация
    RemoteExchange[REPARTITION][region]             <- ОБМЕН: данные едут по сети
      Aggregate(PARTIAL)                            <- частичная агрегация ДО обмена: хорошо
        InnerJoin[orders.customer_id = c.id]
          ScanFilter[table = lake.orders,
                     partitions = 3/1096,           <- отсечение сработало: 3 партиции из 1096
                     columns = 4/62,                <- читаем 4 колонки из 62
                     input rows = 41.2M,
                     bytes read = 612MB]
          LocalExchange[BROADCAST]                  <- маленькая сторона разослана: shuffle не нужен
            ScanFilter[table = lake.customers,
                       columns = 2/28,
                       input rows = 240K]

Что смотреть, в порядке важности:

  • partitions = X/Y. Если X близко к Y, отсечение не сработало — это самая дорогая и самая частая ошибка. Причины: функция над колонкой партиционирования (WHERE date(ts) = ...), несовпадение типов, предикат, зависящий от результата подзапроса.
  • columns = X/Y. Прямой множитель к объёму чтения.
  • bytes read. Единственное число, которое в модели «по байтам» равно деньгам.
  • Тип обмена. BROADCAST — маленькая таблица разослана целиком, обмена большой нет. REPARTITION — по сети едут обе стороны. Если broadcast не выбран для явно маленького справочника, скорее всего, у таблицы устарела статистика.
  • Разлив на диск (spill). Появление в плане или в метриках — сигнал, что запросу не хватило памяти; часто лечится не увеличением кластера, а уменьшением объёма до join’а.
  • Перекос. Одна задача из тысячи работает в сто раз дольше — значит, ключ распределения вырожденный. Разбор приёма с солью — в главе 03.

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


4. Семь рычагов по убыванию эффекта

Рычаг Типичный эффект Чем платите
1. Не читать (отсечение партиций, сортировка/кластеризация) 10–1000× дисциплина партиционирования, компакция
2. Читать меньше колонок 2–20× ничем, это чистая победа
3. Не пересчитывать (инкремент вместо full refresh) 5–100× сложность логики и backfill
4. Не шаффлить (broadcast, предварительная кластеризация) 2–10× память под broadcast, поддержка раскладки
5. Предагрегировать 10–100× для дашбордов свежесть и стоимость поддержки предагрегата
6. Кэшировать (результаты, локальный диск, BI-экстракты) 2–50× риск устаревших чисел
7. Дать больше ресурсов 1,5–4× линейный рост счёта

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

Отдельно про рычаг №1: его физика — раскладка данных на диске, и она подробно разобрана в «Хранилищах и форматах»: партиции, min/max в метаданных Parquet, сортировка внутри файлов, компакция мелких файлов. Здесь достаточно помнить следствие: сортировка данных по колонке, по которой чаще всего фильтруют, — самая недооценённая оптимизация в аналитике, потому что она одновременно улучшает и сжатие, и отсечение.


5. Лестница материализации

Каждую таблицу можно приготовить с разной степенью готовности: от «считается заново при каждом обращении» до «лежит готовым ответом в сервинг-хранилище».

Лестница материализации: от представления до сервинг-слоя

Уровень Когда уместен Задержка данных Риск
Представление (view) редкие чтения, логика меняется часто нулевая дорогое чтение, вложенные вью множат стоимость
Таблица с полным пересчётом небольшой объём, простая логика до расписания линейная стоимость пересчёта
Инкрементальная таблица большие факты, добавление и правки до расписания сложность backfill и дедупликации
Предагрегат дашборды с фиксированными разрезами до расписания комбинаторный взрыв разрезов
Сервинг-слой продуктовые сценарии, задержка в миллисекундах плюс задержка синхронизации вторая копия правды

Правило выбора — арифметика, а не вкус. Пример: витрина считается 4 минуты и сканирует 800 ГБ. При цене 5 USD за терабайт один пересчёт стоит около 4 USD.

  • Дашборд открывают 300 раз в день. Как представление: 300 × 4 = 1 200 USD в день. Как таблица с почасовым пересчётом: 24 × 4 = 96 USD в день. Материализация окупается в 12 раз.
  • Ту же витрину открывают 3 раза в день. Представление: 12 USD. Почасовая таблица: 96 USD. Материализация — убыток в 8 раз; правильный ответ — либо оставить представление, либо пересчитывать раз в сутки.

Важная и часто забываемая половина правила: движение вниз по лестнице так же законно, как вверх. Витрина, которую перестали открывать, продолжает пересчитываться годами. Отчёт «сколько стоит пересчёт моделей, к которым никто не обращался 90 дней» обычно находит 10–30 % бюджета платформы.


6. Конкуренция: почему всё замедлилось, хотя никто ничего не менял

Классическая история: в 9:30 дашборды открываются медленно, в 11:00 — быстро. Нагрузка выросла на 20 %, время ответа — втрое. Это не мистика, а свойство любой системы с очередью: чем ближе утилизация к 100 %, тем резче растёт время ожидания. Ту же нелинейность разбирает трек SRE в главе про ёмкость.

Практические выводы для хранилища:

  • Разделяйте нагрузки. ELT-пересчёт, BI-дашборды, ad-hoc-запросы аналитиков и выгрузки в внешние системы должны жить в разных пулах/варехаусах. Иначе один аналитик с CROSS JOIN останавливает утренние дашборды всей компании.
  • Ширина против количества. Больший кластер ускоряет один тяжёлый запрос. Несколько кластеров (или multi-cluster warehouse) увеличивают число одновременных запросов. Лечить очередь увеличением размера — типичная и дорогая ошибка.
  • Автосуспенд и автовозобновление. Простаивающий кластер в модели «по времени» — чистый убыток. Но слишком агрессивный суспенд убивает локальный кэш и минимальный биллинговый интервал начинает оплачиваться заново на каждый чих.
  • Приоритеты и таймауты. У ad-hoc-пула — жёсткий таймаут в несколько минут. Запрос, который должен идти час, обязан идти в другом пуле и с ведома владельца.
  • Метрика очереди — отдельная от метрики длительности. «Запрос шёл 40 секунд» и «запрос ждал 35 секунд в очереди и работал 5» требуют совершенно разных действий.

7. Кэши и где они врут

Уровень кэша Что кэширует Условия попадания Как врёт
Кэш результатов готовый ответ на точно такой же SQL тот же текст запроса, те же права, данные не изменились недетерминированные функции (current_timestamp) отключают его молча
Локальный кэш узлов прочитанные блоки файлов тот же кластер не был остановлен после автосуспенда прогрев начинается заново
Материализованные представления предвычисленный результат, подставляемый оптимизатором ограниченный набор форм запроса тихо не подставляется, и вы платите полную цену, не зная об этом
BI-экстракт копия данных внутри BI обновление по расписанию BI самая частая причина «в двух дашбордах разные числа»

Отдельная ловушка — вложенные представления. Вью поверх вью поверх вью выглядит как аккуратная декомпозиция, но движок разворачивает всю цепочку в один запрос; фильтр, который вы написали снаружи, может не «пролезть» внутрь через агрегацию или оконную функцию, и вместо трёх партиций читается вся история. Это стоит проверять планом, а не верой.


8. Атрибуция: чей это счёт

Общая цифра «хранилище стоит 42 000 USD в месяц» не приводит ни к каким действиям. Действия начинаются, когда счёт разложен по потребителям. Механика везде одинаковая: каждый запрос помечается тегом, история запросов доступна как таблица, стоимость считается по формуле биллинга.

-- Кто и на что тратит: разложение месячного счёта по тегам запросов.
-- (Snowflake: SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY; в BigQuery — INFORMATION_SCHEMA.JOBS.)
SELECT
    COALESCE(NULLIF(query_tag, ''), 'без тега')                    AS consumer,
    COUNT(*)                                                        AS queries,
    ROUND(SUM(total_elapsed_time) / 1000 / 3600, 1)                 AS wall_hours,
    ROUND(SUM(credits_used_cloud_services), 2)                      AS credits,
    ROUND(SUM(bytes_scanned) / POW(1024, 4), 2)                     AS tb_scanned,
    ROUND(100.0 * SUM(bytes_scanned)
          / SUM(SUM(bytes_scanned)) OVER (), 1)                     AS pct_of_scan
FROM snowflake.account_usage.query_history
WHERE start_time >= DATEADD('day', -30, CURRENT_TIMESTAMP())
GROUP BY 1
ORDER BY tb_scanned DESC
LIMIT 25;

Что делать с результатом:

  • Правило 20/80 работает почти всегда. Двадцать запросов дают 80 % сканирования. Оптимизировать надо их, а не «всё подряд».
  • Тег обязателен по умолчанию. Запросы без тега — это анонимный расход, который никогда не уменьшается; практика ровно та же, что для облачных ресурсов в «Стоимости платформы» и «Облачных затратах».
  • Стоимость дашборда — понятная владельцу величина. «Этот дашборд стоит 900 USD в месяц и его открывают четыре человека» — разговор, который заканчивается решением за пять минут.
  • Стоимость модели — часть код-ревью. Если PR-сборка знает, сколько сканирует новая модель, вопрос «а нужно ли ей читать три года?» задаётся до релиза, а не через квартал.

9. Разбор: дашборд за 3 800 USD в месяц

Реалистичный сценарий, собранный из типовых ошибок. Дашборд «Продажи» на 12 виджетов, обновление при каждом открытии, 400 открытий в день.

Шаг Что сделано Стало
Исходное состояние каждый виджет — свой запрос к факту за 3 года, SELECT * в подзапросе 3 800 USD/мес
1. Убрали SELECT *, оставили 6 колонок из 48 чтение колонок 1 050 USD
2. Починили отсечение: WHERE date(ts) = ...WHERE ts >= ... AND ts < ... план стал читать 90 партиций вместо 1096 390 USD
3. Свели 12 виджетов к одному предагрегату по (дата, регион, категория) один пересчёт в час вместо 400 чтений факта 150 USD
4. Отключили автообновление, оставили обновление по расписанию + кнопку убрали лишние прогоны 120 USD
5. Перенесли дашборд на отдельный пул перестал конкурировать с ELT те же 120 USD, но p95 — 1,2 с вместо 9 с

Наблюдения, которые повторяются от случая к случаю:

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

10. Хранение: меньшая, но растущая часть счёта

В облачных хранилищах хранение обычно составляет 5–20 % счёта, а сканирование — остальное. Это не повод его игнорировать: хранение растёт монотонно и незаметно.

  • Мелкие файлы. Тысячи файлов по 2 МБ вместо сотен по 256 МБ — это и лишние метаданные, и замедление планирования. Компакция обязательна для каждой активно пишущейся таблицы (см. главу 06).
  • Снапшоты и time travel. Каждая перезапись оставляет предыдущую версию. Полезно, но окно хранения должно быть выбрано осознанно, а истёкшие снапшоты — удаляться по расписанию.
  • Уровни хранения. Партиции старше года почти никогда не читаются: перенос в холодный класс хранения объектов даёт кратное удешевление. Детали классов — в «Объектных хранилищах».
  • Дубли. Копии витрин «на всякий случай», песочницы годичной давности, три параллельные версии одной таблицы — обычное дело. Отчёт по неиспользуемым таблицам стоит держать постоянным.

11. Гардрейлы: как не полагаться на аккуратность

Оптимизация одноразова, гардрейлы работают всегда.

Минимальный набор, который стоит включить в первый же месяц: таймаут по умолчанию для интерактивных пулов, лимит на сканирование для ролей аналитиков, обязательный тег, ежедневный отчёт «топ-20 по стоимости», алерт на запрос дороже порога. Всё это ограничивает не людей, а последствия опечаток.


12. Типичные ошибки

  1. SELECT * в колоночном хранилище. Прямой множитель к счёту, обычно 5–20×.
  2. Функция над колонкой партиционирования. WHERE date(ts) = '2026-07-01' отключает отсечение; правильно — полуинтервал по самой колонке.
  3. Оптимизация без плана. Меняют формулировку SQL наугад вместо чтения EXPLAIN.
  4. Больший кластер как первое лекарство. Ускоряет в 2 раза, дорожает в 2 раза, не устраняет причину.
  5. Full refresh по привычке. Модель на 400 ГБ полностью пересчитывается каждый час, потому что так было проще написать в первый день.
  6. Автообновление дашборда каждые 5 минут при данных, которые обновляются раз в час.
  7. Цепочки вложенных представлений. Фильтр не доходит до сканирования, читается вся история.
  8. Предагрегаты на все случаи жизни. Комбинаторный взрыв: 30 витрин, которые дороже, чем исходный факт.
  9. Материализация без анализа частоты чтения. Пересчитываем ежечасно то, что открывают дважды в неделю.
  10. Нет тегов и нет атрибуции. Счёт растёт, виноватых нет, действий не возникает.
  11. Общий пул для ELT и BI. Ночной пересчёт и утренние дашборды дерутся за ресурсы.
  12. Оптимизация без измерения «до». Невозможно доказать эффект и легко откатить улучшение чужой правкой.

13. Как это выглядит в проде

У зрелой платформы есть отдельная витрина о самой себе: история запросов, стоимость по тегам, топ-запросов, стоимость каждой модели за прогон и стоимость каждого дашборда за месяц. Владелец дашборда видит его цену в интерфейсе. PR-проверка сообщает, что новая модель будет сканировать 1,8 ТБ за прогон, и просит обоснование. Раз в квартал проходит уборка: модели без чтений за 90 дней переводятся в представления или удаляются. Нагрузки разведены по пулам, у аналитического пула стоит таймаут и лимит сканирования. Экономия здесь — не героический проект, а фоновая гигиена; проекты по «снижению costs на 40 %» появляются там, где этой гигиены не было три года.


14. Мини-итог

  • Сначала определите модель биллинга: она решает, что вообще имеет смысл оптимизировать.
  • Главная величина — объём прочитанных данных; всё остальное вторично.
  • План запроса читается снизу вверх; первые числа для проверки — доля прочитанных партиций и колонок.
  • Порядок рычагов: не читать → читать меньше колонок → не пересчитывать → не шаффлить → предагрегировать → кэшировать → и только потом добавлять ресурсы.
  • Материализация — арифметика: цена пересчёта против суммарной цены чтений; спуск по лестнице так же нужен, как подъём.
  • Ширина кластера лечит один тяжёлый запрос, число кластеров — очередь; путать дорого.
  • Кэши экономят до тех пор, пока о них знают; молчаливый промах — самый дорогой вид кэша.
  • Без тегов и атрибуции расход анонимен и не уменьшается.
  • Гардрейлы (таймауты, лимиты, обязательные теги) работают постоянно, разовая оптимизация — нет.

Источники

Что дальше

Мы научились считать быстро и недорого. Но витрина, которую никто не открывает и в которую ничего не упирается, не создаёт ценности: данные должны попасть туда, где принимаются решения и совершаются действия, — в CRM, в рассылку, в интерфейс продукта, в партнёрскую интеграцию. Следующая статья — про обратный путь: reverse ETL, сервинг с низкой задержкой, данные как продукт и петли, которые при этом легко замкнуть себе на ногу.

Дальше: Активация данных: reverse ETL, сервинг и data-продукты.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов