Производительность и стоимость аналитической нагрузки
Один и тот же ответ — «выручка по регионам за 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. Жизнь запроса: где именно уходит время
Из схемы видны четыре места, где сгорает бюджет, в порядке убывания частоты:
- Не сработало отсечение — движок читает всю таблицу вместо одной партиции.
- Читаются лишние колонки —
SELECT *в колоночном хранилище означает «прочитать все 60 колонок вместо трёх». - Данные едут по сети между стадиями — shuffle на join и агрегации, разобранный в главе про пакетную обработку.
- Не хватило памяти — сортировка или 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. Гардрейлы: как не полагаться на аккуратность
Оптимизация одноразова, гардрейлы работают всегда.
потребителя?} B -->|нет| B1[Отклонить или пометить
как анонимный расход] B -->|да| C{Оценка сканирования
выше порога роли?} C -->|да| C1[Отклонить с подсказкой:
добавьте фильтр по дате] C -->|нет| D{Пул соответствует
типу нагрузки?} D -->|нет| D1[Перенаправить в ad-hoc-пул] D -->|да| E[Выполнить] E --> F{Превышен таймаут
или лимит байт?} F -->|да| F1[Прервать, записать в отчёт] F -->|нет| G[Результат + запись стоимости] G --> H{Аномалия:
дороже медианы в 10 раз?} H -->|да| H1[Уведомить владельца
с планом запроса] H -->|нет| I[Обычный день]
Минимальный набор, который стоит включить в первый же месяц: таймаут по умолчанию для интерактивных пулов, лимит на сканирование для ролей аналитиков, обязательный тег, ежедневный отчёт «топ-20 по стоимости», алерт на запрос дороже порога. Всё это ограничивает не людей, а последствия опечаток.
12. Типичные ошибки
SELECT *в колоночном хранилище. Прямой множитель к счёту, обычно 5–20×.- Функция над колонкой партиционирования.
WHERE date(ts) = '2026-07-01'отключает отсечение; правильно — полуинтервал по самой колонке. - Оптимизация без плана. Меняют формулировку SQL наугад вместо чтения
EXPLAIN. - Больший кластер как первое лекарство. Ускоряет в 2 раза, дорожает в 2 раза, не устраняет причину.
- Full refresh по привычке. Модель на 400 ГБ полностью пересчитывается каждый час, потому что так было проще написать в первый день.
- Автообновление дашборда каждые 5 минут при данных, которые обновляются раз в час.
- Цепочки вложенных представлений. Фильтр не доходит до сканирования, читается вся история.
- Предагрегаты на все случаи жизни. Комбинаторный взрыв: 30 витрин, которые дороже, чем исходный факт.
- Материализация без анализа частоты чтения. Пересчитываем ежечасно то, что открывают дважды в неделю.
- Нет тегов и нет атрибуции. Счёт растёт, виноватых нет, действий не возникает.
- Общий пул для ELT и BI. Ночной пересчёт и утренние дашборды дерутся за ресурсы.
- Оптимизация без измерения «до». Невозможно доказать эффект и легко откатить улучшение чужой правкой.
13. Как это выглядит в проде
У зрелой платформы есть отдельная витрина о самой себе: история запросов, стоимость по тегам, топ-запросов, стоимость каждой модели за прогон и стоимость каждого дашборда за месяц. Владелец дашборда видит его цену в интерфейсе. PR-проверка сообщает, что новая модель будет сканировать 1,8 ТБ за прогон, и просит обоснование. Раз в квартал проходит уборка: модели без чтений за 90 дней переводятся в представления или удаляются. Нагрузки разведены по пулам, у аналитического пула стоит таймаут и лимит сканирования. Экономия здесь — не героический проект, а фоновая гигиена; проекты по «снижению costs на 40 %» появляются там, где этой гигиены не было три года.
14. Мини-итог
- Сначала определите модель биллинга: она решает, что вообще имеет смысл оптимизировать.
- Главная величина — объём прочитанных данных; всё остальное вторично.
- План запроса читается снизу вверх; первые числа для проверки — доля прочитанных партиций и колонок.
- Порядок рычагов: не читать → читать меньше колонок → не пересчитывать → не шаффлить → предагрегировать → кэшировать → и только потом добавлять ресурсы.
- Материализация — арифметика: цена пересчёта против суммарной цены чтений; спуск по лестнице так же нужен, как подъём.
- Ширина кластера лечит один тяжёлый запрос, число кластеров — очередь; путать дорого.
- Кэши экономят до тех пор, пока о них знают; молчаливый промах — самый дорогой вид кэша.
- Без тегов и атрибуции расход анонимен и не уменьшается.
- Гардрейлы (таймауты, лимиты, обязательные теги) работают постоянно, разовая оптимизация — нет.
Источники
- Google Cloud. Optimize query computation — практики снижения сканирования в BigQuery: https://cloud.google.com/bigquery/docs/best-practices-performance-compute
- Google Cloud. Estimate and control costs — как считается стоимость запроса: https://cloud.google.com/bigquery/docs/best-practices-costs
- Snowflake. Performance optimization и устройство кэшей и варехаусов: https://docs.snowflake.com/en/guides-overview-performance
- Snowflake. Query history и account usage — источник данных для атрибуции: https://docs.snowflake.com/en/sql-reference/account-usage/query_history
- Trino. EXPLAIN и оптимизация запросов — чтение планов распределённых запросов: https://trino.io/docs/current/optimizer/cost-based-optimizations.html
- ClickHouse. Primary indexes and query performance — как порядок сортировки решает скорость: https://clickhouse.com/docs/en/optimize/sparse-primary-indexes
- Apache Iceberg. Maintenance — компакция, истечение снапшотов, уборка файлов: https://iceberg.apache.org/docs/latest/maintenance/
- Daniel Abadi et al. The Design and Implementation of Modern Column-Oriented Database Systems — почему колоночность даёт порядки: https://stratos.seas.harvard.edu/files/stratos/files/columnstoresfntdbs.pdf
- FinOps Foundation. Framework — язык и практики управления облачным расходом: https://www.finops.org/framework/
Что дальше
Мы научились считать быстро и недорого. Но витрина, которую никто не открывает и в которую ничего не упирается, не создаёт ценности: данные должны попасть туда, где принимаются решения и совершаются действия, — в CRM, в рассылку, в интерфейс продукта, в партнёрскую интеграцию. Следующая статья — про обратный путь: reverse ETL, сервинг с низкой задержкой, данные как продукт и петли, которые при этом легко замкнуть себе на ногу.
Дальше: Активация данных: reverse ETL, сервинг и data-продукты.