Что на самом деле происходит
Cartesian explosion — это не «баг SQL», а математика связей. Ожидаемое число строк после соединения ≈
> сумма по всем ключам
Если у тебя «многие-ко-многим» (или несколько «один-ко-многим» поверх одного ключа), произведение взлетает. SQL честен: он возвращает все комбинации. Боль — на нашей стороне модели.
▪️Где это прячется системах?
• ТFact × Fact. Соединяешь две транзакционные таблицы «по пользователю/дате» — получаешь карнавал дублей.
• Несколько 1:N за раз. Заказы × платежи × доставки: один заказ внезапно «размножается» ×(платежи×доставки).
• SCD Type 2 / интервальные джойны. «Актуальная версия профиля на момент события» без point-in-time логики даёт пересечения интервалов и копии.
• Мосты (bridge) и M:N. Категории, теги, кампании — фан-аут по определению.
• Плавающие ключи. JOIN по региону/имени/дате без суррогатного id.
• Нулевые/пустые ключи. NULL в ключе + «случайный» фильтр → неожиданные комбинации.
• EXPLODE/UNNEST. Вытягивание массивов в строки — скрытый множитель.
• Data skew. Один «супер-ключ» встречается в 30% строк → локальные взрывы и перекос нагрузки.
▪️Диагностика
1) Назови зерно (grain) вслух.
2) Померь «мультипликаторы».
Посмотри распределение n_left(k) и n_right(k) по ключу: топ-значения, медианы, 95-й перцентиль. Ищи хвосты — именно они делают каскад.
3) Посчитай фан-аут.
fanout
4) Смотри план, а не верь на слово.
5) Канареечный прогон.
Запускай на узком окне (день/неделя), фиксируй метрики: строки до/после JOIN, дубликат-рейт по ключу, топ-«жирные» ключи.
Профилактика (модель → запрос → рантайм)
На уровне модели данных
• Определи и задокументируй grain для каждого факта и измерения. Любой JOIN начинается с вопроса: «Где у нас 1:1?»
• Суррогатные ключи > натуральных. Текстовые «регион/имя» не годятся для целостности.
• SCD и точка во времени. Для Type 2 готовь
• Уникальность — не пожелание, а контракт. Уникальные индексы/тесты в staging, а не «где-то потом».
На уровне запросов
• Предагрегируй «многие-ко-многим». Своди платежи/доставки до 1 строки на факт (сумма, MAX, counts) до JOIN.
• Выбирай одну запись явно. Оконные функции/ранжирование для «последней/текущей» вместо «потащу всё, а там разберёмся».
• Сначала фильтруй, потом соединяй. Любая лишняя строка до JOIN — лишние комбинации после.
• Семиджойн там, где тебе не нужны колонки.
• Не склеивай несколько 1:N на одном шаге. Делай этапами: факт×платежи → свёл до 1:1 → факт×доставки → снова свёл.
На уровне исполнения
• Учитывай skew. Соль key_salt для топ-ключей, перераспределение партиций, broadcast маленьких таблиц, включение bloom-фильтров/динамического прунинга, если движок поддерживает.
• Лимиты и сторожа. Бюджет на fanout, алерты на «rows_after_join», квоты на временное хранилище.
Что считать «хорошим результатом»
💸 Поддержать канал
👉 Senior Data Analyst | #SQL
Cartesian explosion — это не «баг SQL», а математика связей. Ожидаемое число строк после соединения ≈
> сумма по всем ключам
k от n_left(k) × n_right(k).
Если у тебя «многие-ко-многим» (или несколько «один-ко-многим» поверх одного ключа), произведение взлетает. SQL честен: он возвращает все комбинации. Боль — на нашей стороне модели.
▪️Где это прячется системах?
• ТFact × Fact. Соединяешь две транзакционные таблицы «по пользователю/дате» — получаешь карнавал дублей.
• Несколько 1:N за раз. Заказы × платежи × доставки: один заказ внезапно «размножается» ×(платежи×доставки).
• SCD Type 2 / интервальные джойны. «Актуальная версия профиля на момент события» без point-in-time логики даёт пересечения интервалов и копии.
• Мосты (bridge) и M:N. Категории, теги, кампании — фан-аут по определению.
• Плавающие ключи. JOIN по региону/имени/дате без суррогатного id.
• Нулевые/пустые ключи. NULL в ключе + «случайный» фильтр → неожиданные комбинации.
• EXPLODE/UNNEST. Вытягивание массивов в строки — скрытый множитель.
• Data skew. Один «супер-ключ» встречается в 30% строк → локальные взрывы и перекос нагрузки.
Правило большого пальца: если ты соединяешь две таблицы, каждая из которых способна иметь >1 строку на ключ, у тебя по умолчанию есть риск explosion.
▪️Диагностика
1) Назови зерно (grain) вслух.
«Факт заказов: 1 строка = 1 заказ. Платежи: 1 строка = 1 платёж».
Если зерно не 1:1 — ты уже в зоне риска.
2) Померь «мультипликаторы».
Посмотри распределение n_left(k) и n_right(k) по ключу: топ-значения, медианы, 95-й перцентиль. Ищи хвосты — именно они делают каскад.
3) Посчитай фан-аут.
fanout
= rows_after_join / max(rows_left, rows_right).
Нормально: ~1.0–1.2. Подозрительно: >1.5. Красная зона: >2–3.
4) Смотри план, а не верь на слово.
EXPLAIN/EXPLAIN ANALYZE:
несоответствие estimated vs actual rows, проливы на диск, перестановки JOIN-порядка — всё это индикаторы проблемы кардинальности.5) Канареечный прогон.
Запускай на узком окне (день/неделя), фиксируй метрики: строки до/после JOIN, дубликат-рейт по ключу, топ-«жирные» ключи.
Профилактика (модель → запрос → рантайм)
На уровне модели данных
• Определи и задокументируй grain для каждого факта и измерения. Любой JOIN начинается с вопроса: «Где у нас 1:1?»
• Суррогатные ключи > натуральных. Текстовые «регион/имя» не годятся для целостности.
• SCD и точка во времени. Для Type 2 готовь
point-in-time с effective_from/effective_to
и снимки «на момент события».• Уникальность — не пожелание, а контракт. Уникальные индексы/тесты в staging, а не «где-то потом».
На уровне запросов
• Предагрегируй «многие-ко-многим». Своди платежи/доставки до 1 строки на факт (сумма, MAX, counts) до JOIN.
• Выбирай одну запись явно. Оконные функции/ранжирование для «последней/текущей» вместо «потащу всё, а там разберёмся».
• Сначала фильтруй, потом соединяй. Любая лишняя строка до JOIN — лишние комбинации после.
• Семиджойн там, где тебе не нужны колонки.
EXISTS/IN
часто заменяет «плотный» JOIN и сохраняет зерно.• Не склеивай несколько 1:N на одном шаге. Делай этапами: факт×платежи → свёл до 1:1 → факт×доставки → снова свёл.
На уровне исполнения
• Учитывай skew. Соль key_salt для топ-ключей, перераспределение партиций, broadcast маленьких таблиц, включение bloom-фильтров/динамического прунинга, если движок поддерживает.
• Лимиты и сторожа. Бюджет на fanout, алерты на «rows_after_join», квоты на временное хранилище.
Мысленно держи «бюджет кардинальности». Если после каждого шага fanout растёт на +20–30%, ты уже не контролируешь процесс.
Что считать «хорошим результатом»
«Мы получили те же агрегаты на проде и на тесте, но путь — воспроизводимый:
документированное зерно, явные ключи, фан-аут ≤ 1.2 на каждом шаге, нули/дубли пойманы тестами.»
💸 Поддержать канал
👉 Senior Data Analyst | #SQL
🔥3👍1
Ты запускаешь запрос и внезапно видишь: строк стало в 10 раз больше, чем самая большая таблица в join?
Anonymous Quiz
2%
A) Пользователи реально стали в 10 раз активнее — срочно звонить маркетингу
16%
B) DISTINCT ещё не прикрутили, а кто-то верит, что это «нормально»
73%
C) У тебя классический cartesian explosion: join many-to-many без контроля
4%
D) PostgreSQL запустил режим fun mode и умножил данные ради развлечения
6%
E) Всё так и должно быть, просто Вселенная расширяется вместе с твоим запросом
👍2🔥1