Тестирование и CI/CD дата-пайплайнов: среды, unit-тесты SQL, data diff и релиз моделей
В обычном сервисе цена ошибки ограничена: выкатили баг, увидели рост 5xx, откатили за три минуты,
пользователи потеряли пару запросов. В дата-платформе так не работает.
Ошибка в SQL-модели проявляется не сразу, а на следующем расчёте — через час или через сутки. К моменту обнаружения по ней уже посчитаны двенадцать витрин, обучена модель, отправлены отчёты инвесторам и приняты решения. Откат кода не возвращает данные: строки уже перезаписаны, а «вчерашняя выручка» уже жила три дня в неверном значении. И самое неприятное — тест на проде здесь не бесплатный: один неудачный прогон полного пересчёта стоит реальных денег за сканирование.
Отсюда практический вывод, который отличает зрелую команду данных: инвестиции идут не в «побольше тестов», а в сокращение расстояния между ошибкой и её обнаружением. Эта статья — про то, как поймать ошибку до того, как её увидят данные в проде.
Важно сразу разграничить с главой о качестве данных: там речь о проверках данных в проде (свежесть, объём, аномалии, WAP). Здесь — о проверках кода до релиза. Это две разные сети с разной ячейкой, и подменять одну другой не получается.
1. Две оси, четыре квадранта
Всё, что можно проверить в платформе данных, раскладывается по двум осям: что проверяем (код или данные) и когда (до релиза или в проде).
| До релиза (CI) | В проде (runtime) | |
|---|---|---|
| Код | компиляция SQL, линтер, unit-тесты трансформаций, тесты DAG’ов | ошибки исполнения, таймауты, падения задач |
| Данные | прогон на подвыборке, data diff с продом, схемные контракты | тесты свежести/уникальности, аномалии, WAP, сверка |
Ошибка большинства команд — жить только в правом нижнем квадранте: «у нас есть dbt-тесты в проде, значит, качество под контролем». Правый нижний ловит дефект после того, как он материализовался, и стоит пересчёта. Левый верхний ловит его за 90 секунд в пул-реквесте и стоит ноль.
секунды"] A2["Unit-тесты на фикстурах
секунды"] A3["Тесты целостности DAG
секунды"] end subgraph M["Средняя цена"] B1["Сборка изменённых моделей
в CI-схеме, минуты"] B2["Схемные и контрактные
проверки"] B3["Data diff с продом
минуты"] end subgraph R["Дорого"] C1["Полный прогон на копии
данных, часы"] C2["Runtime-проверки и WAP
каждый запуск"] C3["Инцидент на проде
дни и доверие"] end A1 --> B1 --> C1 A2 --> B2 --> C2 A3 --> B3 --> C3 C3 -. "каждый пропущенный дефект
уезжает вправо" .-> L
Правило распределения усилий то же, что и в обычной разработке: чем дешевле проверка, тем больше её должно быть. Разница лишь в том, что «дорого» в данных измеряется не только минутами CI, но и счётом за сканирование.
2. Что вообще тестировать в SQL-модели
Типичный ответ «проверим, что строк больше нуля» бесполезен. Полезные тесты рождаются из вопроса: какие допущения делает эта модель и что случится, если они нарушатся?
Список допущений, которые ломаются чаще всего:
- Уникальность ключа. Модель предполагает одну строку на заказ; источник прислал две версии.
Результат — удвоенная выручка после
JOIN. - Обработка
NULL.amount > 100отбрасываетNULL, аNOT (amount <= 100)— нет. В агрегатахSUMигнорируетNULL,COUNT(*)— нет. Половина расхождений в отчётах живёт здесь. - Границы интервалов.
BETWEENвключает обе границы; полуинтервал[начало, конец)— нет. Заказ в 00:00:00 попадёт в два дня сразу или ни в один. - Часовые пояса. Данные в UTC, бизнес-день в
Europe/Moscow, отчёт «за 1 июля» отличается на три часа данных. Переход на летнее время в источниках с локальным временем даёт дни на 23 и 25 часов. - Поздние строки. Факт приехал через двое суток; инкрементальная модель, фильтрующая по
event_date = current_date - 1, не увидит его никогда. - Границы SCD. Джойн факта с версионным измерением по
valid_from <= t < valid_to— и вопрос, что происходит ровно на границе, и что с фактами до появления первой версии. - Деньги. Дробные типы, округление, валюты, знак у возвратов.
Каждый пункт — кандидат в unit-тест с двумя-тремя строками фикстуры. Именно поэтому unit-тесты трансформаций эффективнее, чем кажется: они проверяют не «SQL работает», а «SQL делает то, о чём мы договорились в спорных местах».
3. Unit-тесты трансформаций: фикстуры вместо продовых данных
Ключевая мысль: тест на реальных данных — не unit-тест. Реальные данные меняются, поэтому такой тест то падает, то нет; в них почти никогда нет нужного граничного случая; и они не отвечают на вопрос «что должно получиться», потому что эталон приходится считать тем же самым SQL.
Unit-тест трансформации выглядит так: фиксированный вход → ожидаемый выход. В dbt (начиная с версии 1.8) это встроенный механизм:
# models/marts/fct_orders.yml
unit_tests:
- name: возврат_уменьшает_выручку_и_не_ломает_дневную_границу
model: fct_orders
description: >
Проверяем три допущения сразу: возврат учитывается со знаком минус,
бизнес-день считается в московском времени, NULL в сумме не обнуляет заказ.
given:
- input: ref('stg_orders')
rows:
# заказ ровно на границе суток по UTC: 30 июня 21:30 UTC = 1 июля 00:30 MSK
- {order_id: 1, customer_id: 10, amount: 1000, kind: 'sale',
created_at: '2026-06-30 21:30:00'}
# возврат по тому же заказу на следующий день
- {order_id: 2, customer_id: 10, amount: 300, kind: 'refund',
created_at: '2026-07-01 10:00:00'}
# сумма не заполнена: заказ должен остаться в счётчике, но не в деньгах
- {order_id: 3, customer_id: 11, amount: null, kind: 'sale',
created_at: '2026-07-01 12:00:00'}
expect:
rows:
- {business_date: '2026-07-01', orders_cnt: 3, revenue: 700}
Стоимость такого теста — секунды: dbt подставляет фикстуры вместо ref() и выполняет модель на трёх
строках. Стоимость эквивалентной проверки «глазами на проде» — пересчёт витрины и вечер аналитика.
Для трансформаций на Python логика та же, только инструменты обычные:
# tests/test_sessionize.py — тест на склейку событий в сессии.
# Проверяем ровно то, о чём договорились: разрыв 30 минут начинает новую сессию,
# граница включительна, события одного пользователя не смешиваются с чужими.
import polars as pl
from pipelines.sessionize import sessionize
def test_разрыв_ровно_30_минут_начинает_новую_сессию():
events = pl.DataFrame({
"user_id": ["u1", "u1", "u1", "u2"],
"ts": ["2026-07-01T10:00:00", "2026-07-01T10:29:59",
"2026-07-01T10:59:59", "2026-07-01T10:10:00"],
}).with_columns(pl.col("ts").str.to_datetime())
result = sessionize(events, gap_minutes=30).sort(["user_id", "ts"])
# u1: первые два события в одной сессии (разрыв 29:59),
# третье — в новой (разрыв ровно 30:00, граница НЕ включается)
assert result["session_id"].to_list()[:3] == [1, 1, 2]
# события другого пользователя нумеруются независимо
assert result.filter(pl.col("user_id") == "u2")["session_id"].to_list() == [1]
Что тестировать не надо: тривиальные переименования колонок, SELECT * из источника, и «что
хранилище умеет складывать числа». Тест, который не может упасть от осмысленной ошибки, — это
стоимость поддержки без выгоды. Общая философия — в треке про тестирование:
«Модульное тестирование».
4. Тесты пайплайна, а не только трансформаций
Кроме SQL в репозитории живёт код оркестрации, и он ломается по-своему: опечатка в имени задачи,
цикл в графе, забытый retries, зависимость от задачи из другого DAG’а, которая переименована.
Базовый тест целостности графа разобран в главе про оркестрацию —
его достаточно поставить в CI один раз, и он окупается на первой же опечатке.
Стоит добавить к нему три вещи:
- Проверку конвенций. Все задачи имеют владельца и алерт-канал; у DAG’ов Tier-1 задан SLA; у инкрементальных моделей объявлен уникальный ключ. Это линтер, а не тест, но ловит он больше.
- Тесты на внешние вызовы с записанными ответами. Коннекторы из предыдущей главы тестируются на сохранённых ответах API: реальный вызов в CI — это флаки, лимиты и утечка ключей.
- Интеграционный прогон на подвыборке. Один DAG, маленький набор данных, реальные операторы, но контейнерные Postgres/MinIO вместо облака. Разбор подхода — в «Интеграционном тестировании».
5. Среды: где выполняется PR-версия модели
Главная особенность CI в данных: вычисление невозможно изолировать от хранилища. Модель нельзя собрать «в памяти» — её надо где-то материализовать. Отсюда три уровня сред.
| Среда | Что это физически | Данные | Кто платит за прогон |
|---|---|---|---|
| Личная среда разработчика | схема dev_alice в том же хранилище |
клон или подвыборка прода | разработчик, много мелких прогонов |
| CI-среда пул-реквеста | эфемерная схема ci_pr_482, удаляется после мержа |
подвыборка или клон, только изменённые модели | CI, десятки прогонов в день |
| Прод | analytics |
всё | расписание |
Три приёма делают такие среды дешёвыми:
Zero-copy clone. Snowflake и Databricks умеют клонировать схему без копирования файлов: создаётся новый набор метаданных, ссылающийся на те же данные, а расходятся они только по мере записи. Полноценная копия витрин на 40 ТБ создаётся за секунды и стоит копейки, пока в ней ничего не пишут.
-- Snowflake: личная среда разработчика поверх прод-данных за секунды.
CREATE SCHEMA IF NOT EXISTS dev_alice CLONE analytics;
-- Databricks / Delta: неглубокий клон таблицы (копируются только метаданные).
CREATE TABLE dev_alice.fct_orders SHALLOW CLONE analytics.fct_orders;
Подвыборка с сохранением ссылочной целостности. Если клона нет (BigQuery, ClickHouse, Postgres), берут срез: N дней фактов и только связанные измерения. Случайные 1 % строк — типичная ошибка: join’ы перестают находить пары, и тесты проходят на пустых результатах. Правильный срез строится по ключу: сначала выбираются заказы за 3 дня, затем клиенты и товары именно этих заказов.
Синтетика вместо персональных данных. Копия прода в dev-схеме — самый частый канал утечки персональных данных, о чём подробно в «Приватности и соответствии требованиям». Практика: клонируются витрины без PII-колонок, а поля вроде почты и телефона заменяются детерминированным псевдонимом — детерминированным, чтобы join’ы продолжали работать. Общая теория изолированных сред — в «Средах».
6. Сборка только изменённого: почему CI не пересчитывает всё
Наивный CI собирает весь проект: полторы тысячи моделей, сорок минут и заметный счёт за каждый пул-реквест, где поправили комментарий. Решение — дифференциальная сборка: пересобрать только изменённые модели и то, что от них зависит.
Механика простая. Прод-сборка сохраняет артефакт-манифест (граф моделей и хэши их кода). CI сравнивает свой манифест с прод-манифестом и получает список изменённых узлов. Дальше берётся изменённое плюс потомки — то, на что изменение может повлиять.
# Забираем манифест последней успешной прод-сборки
aws s3 cp s3://dbt-artifacts/prod/manifest.json ./prod-state/manifest.json
# Собираем только изменённые модели и всё, что ниже по графу.
# --defer: недостающие ЗАВИСИМОСТИ берём из прода, а не пересобираем.
dbt build \
--select 'state:modified+' \
--state ./prod-state \
--defer \
--target ci \
--vars '{"days_of_data": 3}' # в CI работаем на коротком окне
--defer — ключевая часть: если ваша модель читает stg_orders, который вы не меняли, CI не будет
пересобирать его в PR-схеме, а подставит прод-таблицу. Без этого дифференциальная сборка
бессмысленна: любая модель тянет за собой половину графа вверх.
Экономический эффект измеряется прямо: типичный переход с полной сборки на дифференциальную сокращает время PR-проверки с 30–50 минут до 3–6 и снижает счёт за CI на порядок. Побочный эффект важнее денег: проверка, которая идёт четыре минуты, действительно запускается на каждом изменении, а сорокаминутная — «потом, перед релизом».
7. Data diff: главный инструмент, которого обычно нет
Тесты отвечают на вопрос «модель корректна?». Они не отвечают на вопрос, который на самом деле волнует ревьюера: что именно изменится в данных, если этот PR влить?
Data diff сравнивает результат PR-версии модели с текущей прод-версией. Уровней три, и полезны все:
- Схема. Добавленные, удалённые, переименованные колонки, смена типов. Удаление колонки — ломающее изменение для BI, даже если все тесты зелёные.
- Агрегаты. Число строк, сумма ключевых мер, число уникальных ключей, доля
NULLпо колонкам. Быстро и почти бесплатно. - Построчно по первичному ключу. Сколько строк появилось, исчезло, изменилось; по каким колонкам; примеры расхождений. Дороже, но именно здесь видно «поменялось 0,4 % строк, все — заказы с возвратами».
-- Сравнение PR-версии с прод-версией по первичному ключу.
-- Идея та же, что в dbt-audit-helper compare_relations, но развёрнуто.
WITH prod AS (SELECT * FROM analytics.fct_orders WHERE business_date >= CURRENT_DATE - 7),
pr AS (SELECT * FROM ci_pr_482.fct_orders WHERE business_date >= CURRENT_DATE - 7),
joined AS (
SELECT COALESCE(p.order_id, c.order_id) AS order_id,
p.order_id IS NULL AS only_in_pr,
c.order_id IS NULL AS only_in_prod,
p.revenue AS pr_revenue,
c.revenue AS prod_revenue
FROM pr AS p
FULL OUTER JOIN prod AS c ON p.order_id = c.order_id
)
SELECT
COUNT(*) AS rows_total,
COUNT_IF(only_in_pr) AS added_rows,
COUNT_IF(only_in_prod) AS removed_rows,
COUNT_IF(NOT only_in_pr AND NOT only_in_prod
AND pr_revenue IS DISTINCT FROM prod_revenue) AS changed_revenue,
ROUND(100.0 * COUNT_IF(pr_revenue IS DISTINCT FROM prod_revenue)
/ NULLIF(COUNT(*), 0), 3) AS changed_pct,
SUM(pr_revenue) - SUM(prod_revenue) AS revenue_delta
FROM joined;
IS DISTINCT FROM вместо <> здесь принципиально: сравнение с NULL через <> даёт NULL, и
изменения «было NULL — стало 0» просто не попадут в отчёт.
Как читать результат — короткая таблица решений:
| Что показал diff | Вопрос ревьюера | Что должно быть в PR |
|---|---|---|
| 0 строк изменилось | рефакторинг без эффекта? | «это чистый рефакторинг» — и это ценно |
| Изменились все строки | точно ли так задумано? | объяснение и, скорее всего, backfill |
| Изменилась 0,1 % строк | что общего у этих строк? | пример строк и причина |
| Строки исчезли | фильтр стал строже намеренно? | обоснование или исправление |
| Появилась колонка | потребители знают? | обновлённый контракт и документация |
| Изменился тип колонки | ломает ли BI? | план миграции потребителей |
Именно этот отчёт превращает ревью SQL-модели из «вроде читается» в осмысленный разговор. Инструменты
готовые есть: dbt-audit-helper, data-diff от Datafold, встроенные планы sqlmesh plan, который
дополнительно классифицирует изменение как ломающее или нет и предлагает пересчитать только
затронутые интервалы.
8. Релиз модели: как выкатывать изменение витрины
Код влит — дальше начинается то, чего нет в обычном CD: изменение кода почти всегда требует изменения данных. Три сценария по возрастанию риска.
Аддитивное изменение (добавили колонку, добавили модель). Безопасно: старые потребители не
замечают. Правило — новые колонки добавляются в конец и допускают NULL для истории, пока не
выполнен backfill.
Изменение логики без изменения схемы (поправили определение метрики). Схема та же, числа другие. Это самое опасное: BI-дашборды продолжат работать и покажут другие цифры без предупреждения. Обязательны: backfill затронутого периода одним прогоном (см. оркестрацию), запись в журнал изменений метрики и уведомление потребителей. Смена определения без объявления — главный источник фразы «этим числам нельзя верить».
Ломающее изменение схемы (удалили или переименовали колонку, изменили зерно). Здесь нужен двухфазный релиз, как в миграциях баз: сначала добавить новое, потом перевести потребителей, потом удалить старое.
Механика переключения — представление как указатель: потребители читают analytics.fct_orders,
которое является вью над физической таблицей fct_orders_v1 или fct_orders_v2. Релиз =
пересоздание вью; откат = пересоздание обратно, мгновенно и без пересчёта.
-- Публичное имя не меняется никогда; меняется только то, куда оно указывает.
CREATE OR REPLACE VIEW analytics.fct_orders AS SELECT * FROM analytics_int.fct_orders_v2;
-- Откат: та же команда с v1. Секунда, а не пересчёт витрины.
Отдельно про удаление: deprecation — процесс, а не решение. Колонка помечается в каталоге,
потребители находятся по линиджу, назначается
дата, за неделю до неё колонка заполняется NULL в тестовой среде, чтобы «сломалось предсказуемо».
Общие практики выката и отката — в «CD и стратегиях релиза», а стратегия ветвления — в
«Стратегиях ветвления».
9. Тестирование потоковых джоб
Стриминг из четвёртой главы добавляет измерение, которого нет в батче: время. Дефекты живут не в логике, а в порядке и задержках, поэтому тест обязан управлять временем явно.
- Детерминированный вход. Набор событий с заданными временными метками, включая заведомо поздние и заведомо не по порядку. Ручной контроль водяного знака: в тестовом харнессе Flink его двигают явно, проверяя, что окно закрылось именно тогда, когда ожидалось.
- Тест на позднее событие. Пришло после закрытия окна — попало в боковой выход, а не потерялось молча. Это один из самых частых продовых дефектов и один из самых простых тестов.
- Тест на восстановление. Джоба останавливается, состояние восстанавливается из чекпоинта, результат совпадает с непрерывным прогоном. Ловит некорректную работу с состоянием.
- Тест на эволюцию состояния. Новая версия джобы читает savepoint старой. Если сериализация состояния изменилась несовместимо, вы узнаете об этом в CI, а не при выкате в 3 часа ночи.
- Реплей как приёмочный тест. Прогон исторического отрезка топика через новую версию и сравнение с эталоном — тот же data diff, только для потока.
Инфраструктурно это MiniCluster во Flink, встроенные тестовые харнессы Kafka Streams или
Testcontainers с реальным брокером. Замена брокера моком экономит минуту прогона и теряет ровно то,
ради чего тест писался, — семантику доставки.
10. Стоимость и время CI
CI в данных стоит денег буквально, поэтому его бюджет — инженерная задача, а не бухгалтерская.
| Приём | Эффект | Риск |
|---|---|---|
Дифференциальная сборка (state:modified+) |
минус порядок по времени и деньгам | пропуск эффекта, если зависимость неявная (ref не проставлен) |
| Ограничение окна данных в CI | линейное сокращение сканирования | не воспроизводятся дефекты на длинной истории |
| Deferred-зависимости | не пересобираем чужие модели | CI зависит от доступности прода |
| Кэш артефактов и пакетов | минус минуты | устаревший манифест даёт неверный diff |
| Nightly полная сборка | ловит то, что пропустил PR-контур | стоит денег каждую ночь; ставить на выборочные проекты |
| Ограничение размера warehouse для CI | предсказуемый счёт | долгие прогоны при больших PR |
Практический ориентир: PR-проверка должна укладываться в 10 минут — за этой границей разработчики начинают её обходить, и никакие правила процесса это не лечат. Общий разбор построения CI-конвейеров — в «Основах CI» и «Тестах в CI».
11. Метрики процесса
Понять, работает ли вся конструкция, можно по четырём числам. Их полезно считать так же аккуратно, как продуктовые метрики — платформа данных вполне способна измерить сама себя.
- Время от коммита до данных в проде. Если сутки — команда будет копить изменения в один большой релиз, а большой релиз ломается сильнее.
- Доля инцидентов качества, вызванных релизом. Растёт — значит, PR-контур пропускает; падает при неизменном числе релизов — контур работает.
- Время до отката. Есть ли вообще механизм отката, или единственный путь — «поправим и пересчитаем за ночь».
- Покрытие критичных моделей. Не «процент моделей с тестами» (эту метрику легко накрутить
not_nullна всё подряд), а «доля Tier-1-моделей, у которых есть unit-тест на бизнес-логику».
12. Типичные ошибки
- Тесты только в проде. Дефект ловится после материализации, каждый пропуск стоит пересчёта.
- Unit-тесты на живых данных. Флаки, отсутствие граничных случаев, эталон считается тем же кодом, что и проверяемое.
- Полная сборка в каждом PR. Сорок минут и счёт — проверку начинают обходить.
- Нет data diff. Ревью SQL превращается в чтение текста; изменение чисел обнаруживает аналитик.
<>вместоIS DISTINCT FROMв сравнениях. Изменения с участиемNULLне видны.- Случайная подвыборка для среды разработки. Join’ы не находят пар, тесты зелёные на пустоте.
- Прод-данные с PII в личных схемах. Работает до первого аудита или утечки.
- Изменение определения метрики без backfill и объявления. Числа «сами поменялись» — доверие потеряно надолго.
- Удаление колонки сразу после того, как «вроде никто не использует». Линидж проверяют после инцидента, а не до.
- CI, зависящий от расписания прода. Прогон PR ждёт ночного обновления — обратная связь приходит на следующий день и обесценивается.
13. Как это выглядит в проде
В зрелой команде изменение витрины выглядит так. Разработчик работает в личной схеме-клоне, собирает
две модели за 40 секунд. Пул-реквест запускает линтер, компиляцию, unit-тесты на фикстурах,
дифференциальную сборку в эфемерной схеме и data diff — суммарно пять минут. В PR автоматически
появляется комментарий: изменилось 0,4 % строк, все с непустым refund_id, сумма выручки за 7 дней
уменьшилась на 1,2 %. Ревьюер спрашивает, ожидаемо ли это; автор отвечает, что да, и прикладывает
план backfill за 90 дней. После мержа модель собирается по расписанию, а WAP-проверки из
главы 07 не пускают результат наружу, если
объём отклонился от нормы. Эфемерная схема удаляется автоматически.
Если что-то пошло не так, откат — это переключение вью на предыдущую версию таблицы, а не ночной пересчёт.
14. Мини-итог
- Проверки кода до релиза и проверки данных в проде — разные сети; нужны обе.
- Полезные тесты растут из явных допущений модели: уникальность ключа,
NULL, границы интервалов, часовые пояса, поздние строки, деньги. - Unit-тест трансформации — фикстура на вход, ожидаемый результат на выход; реальные данные для этого не годятся.
- Среда для PR строится клоном или ключевой подвыборкой; персональные данные в неё не попадают.
- Дифференциальная сборка с deferred-зависимостями превращает CI из получаса в минуты.
- Data diff отвечает на главный вопрос ревью: что изменится в данных.
- Релиз ломающего изменения — двухфазный, с публичным именем-указателем и мгновенным откатом.
- Стриминг тестируется управляемым временем: поздние события, восстановление из чекпоинта, совместимость состояния.
- PR-проверка дольше десяти минут перестаёт запускаться — это инженерное ограничение, а не пожелание.
Источники
- dbt. Unit tests — фикстуры и ожидаемые результаты для моделей: https://docs.getdbt.com/docs/build/unit-tests
- dbt. About state и
defer— дифференциальная сборка по манифесту: https://docs.getdbt.com/reference/node-selection/syntax#about-node-selection - dbt-audit-helper — макросы сравнения отношений и колонок: https://github.com/dbt-labs/dbt-audit-helper
- Datafold. data-diff — построчное сравнение таблиц между средами: https://github.com/datafold/data-diff
- SQLMesh. Plans and environments — классификация изменений на ломающие и нет: https://sqlmesh.readthedocs.io/en/stable/concepts/plans/
- Snowflake. Zero-copy cloning — как устроены клоны и во что они обходятся: https://docs.snowflake.com/en/user-guide/object-clone
- Apache Flink. Testing — харнессы, управление временем и состоянием в тестах: https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/testing/
- Great Expectations — декларативные ожидания к данным как часть конвейера: https://docs.greatexpectations.io/
- Nicole Forsgren, Jez Humble, Gene Kim. Accelerate — почему короткий цикл обратной связи связан с надёжностью: https://itrevolution.com/product/accelerate/
Что дальше
Изменения теперь безопасны, но у платформы есть второе постоянное давление — счёт. Один и тот же результат можно получить за 40 секунд и 2 цента или за 20 минут и 12 долларов, и разница почти никогда не в «мощности кластера»: она в том, сколько байт прочитано, сколько раз пересчитано и что материализовано. Следующая статья — про то, как читать план запроса, выбирать уровень материализации и удерживать стоимость аналитической нагрузки под контролем.
Дальше: Производительность и стоимость аналитической нагрузки.