$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогПрактический SQL для аналитики: как писать запросы, которые не “размножают” данные

Практический SQL для аналитики: как писать запросы, которые не “размножают” данные

$ sudo teach IT
·13 августа 2026 г.·13 мин·29
Практический SQL для аналитики: как писать запросы, которые не “размножают” данные

Покажем типовые причины ошибок в JOIN-агрегациях, как диагностировать кратности и как строить запросы с предсказуемыми результатами. Будут примеры с CTE, агрегатами и проверками корректности на контрольных наборах данных.

Содержание
Типовая проблема: агрегат после JOIN и эффект умножения строкМинимальный пример (логическая ошибка)Как диагностировать кратности JOIN: кто кого размножаетШаг 1. Определите «ключ группировки» для метрикиШаг 2. Проверьте кратность на ключахШаг 3. Проверьте, не умножаете ли вы агрегат дваждыТиповые причины ошибок в JOIN-агрегациях1) Агрегирование на неверной гранулярности2) JOIN-агрегация «сквозь» нерелевантную таблицу3) Несогласованность ключей агрегации4) Неправильный тип соединения (INNER vs LEFT)Контрольный набор данных: чтобы ошибки было видно быстроТаблицыПлохой запрос и правильная альтернативаОшибка: сумма позиций после JOIN с возвратамиРемонт: сначала агрегируйте на нужной гранулярности, потом джойнитеИсправленный запрос (через CTE)Контроль корректности: проверка на контрольных наборахПроверка инвариантовАвтоматическая проверка кратности (удобный диагностический CTE)Практический паттерн: «агрегируй до JOIN» и «джойни после контроля гранулярности»Сложнее: когда нужно несколько разных метрик из разных дочерних таблицПравильная схема: три независимых агрегатаТипичные ошибки при “ремонте”: как сделать правильно, но снова сломатьОшибка A: агрегировали до orderid, но не учли фильтрыОшибка B: дедупликация вместо агрегатаОшибка C: выбор “последнего статуса” без окнаДедупликация: когда нужно взять ровно одну запись на ключКогда “агрегируй до JOIN” не хватает: агрегаты по разным измерениямКак построить запрос с предсказуемыми результатами: практический чек-лист1) Явно зафиксируйте гранулярность каждого шага2) Агрегируйте дочерние таблицы раньше, чем начинаете «склеивать» их друг с другом3) Проверяйте ожидаемые суммы на контрольных наборах4) Смотрите на распределение ключей (кратности) перед расчетами5) Сначала делайте логически корректно — потом оптимизируйтеИтог: меньше магии, больше контроля над гранулярностью

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

Эта статья — практический разбор самых частых причин ошибок в JOIN-агрегациях, способов диагностики кратности (cardinality) и методов построения запросов с предсказуемыми результатами. Будут примеры с CTE, агрегатами и контрольными наборами данных, на которых легко проверить, что запрос считает корректно.


Типовая проблема: агрегат после JOIN и эффект умножения строк

Представьте схему данных для продуктовой аналитики:

  • orders — заказы
  • order_items — позиции в заказах (1 заказ → много позиций)
  • refunds — возвраты по заказам (возможны несколько возвратов на один заказ)

Если вы делаете запрос в стиле:

  1. джойните orders с order_items,
  2. джойните с refunds,
  3. суммируете items.price и/или суммы по заказам,

то любая «многозначность» на одной из сторон JOIN начинает влиять на итог. Особенно опасны случаи, когда в одной связке одна таблица «размножает» строки, а другая еще раз «размножает» — и агрегат увеличивается в несколько раз.

Минимальный пример (логическая ошибка)

Допустим:

  • заказ O1 имеет 2 позиции
  • и 3 возврата (или 2 возврата, неважно — главное, что больше 1)

Если запрос суммирует order_items.price после JOIN с refunds, то каждая позиция будет дублирована на количество возвратов, а итоговая сумма позиций вырастет.

Это не баг СУБД. Это следствие того, что агрегат посчитан на «смешанной» гранулярности.


Как диагностировать кратности JOIN: кто кого размножает

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

Шаг 1. Определите «ключ группировки» для метрики

Например, если метрика — «сумма выручки по дням», базовая единица времени — день. Если метрика — «количество заказов», единица — заказ (уникальный order_id). Если метрика — «выручка по пользователям», единица — пользователь.

Вопрос: после всех JOIN вы на какой гранулярности остаетесь? Часто ответ: «не знаем» или «на гранулярности самого “размножающего” JOIN».

Шаг 2. Проверьте кратность на ключах

Обычно достаточно двух диагностических запросов:

  1. сколько строк получается при JOIN на уровне ключей,
  2. какая кратность на стороне «много».

Универсальный паттерн для проверки кратности

Идея: считать распределение количества совпадений для каждого ключа.

code
-- Сколько строк "попадает" в result после JOIN с order_items
SELECT
  o.order_id,
  COUNT(*) AS joined_rows
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
GROUP BY o.order_id
ORDER BY joined_rows DESC;

Если joined_rows всегда равно 1 — значит, order_items не размножает (скорее всего, у вас ошибка в ожиданиях или связь на самом деле 1:1). Если же значения больше 1 — это сигнал: order_items переводит вас с гранулярности «заказ» на гранулярность «позиция».

Дальше аналогично — проверка для refunds:

code
-- Кратность возвратов на один заказ
SELECT
  o.order_id,
  COUNT(r.refund_id) AS refunds_count
FROM orders o
LEFT JOIN refunds r ON r.order_id = o.order_id
GROUP BY o.order_id
ORDER BY refunds_count DESC;

Шаг 3. Проверьте, не умножаете ли вы агрегат дважды

Если вы делаете два JOIN, и оба переводят гранулярность в «много», итоговая строка будет результатом композиции кратностей.

Например: 2 позиции × 3 возврата = 6 строк на один заказ. Сумма позиций окажется умноженной на 3 (или на 6 — в зависимости от того, что вы суммируете).


Типовые причины ошибок в JOIN-агрегациях

Разберем наиболее частые сценарии.

1) Агрегирование на неверной гранулярности

Вы делаете SUM(order_items.amount) после JOIN, но фактически суммируете на уровне строк, которые уже зависят от возвратов или других дочерних таблиц.

Симптомы:

  • сумма выручки «растет» при наличии возвратов,
  • агрегаты по заказам отличаются от контрольных значений,
  • при фильтрации по параметрам (канал, продукт, период) расхождения усиливаются.

2) JOIN-агрегация «сквозь» нерелевантную таблицу

Иногда запрос содержит JOIN, который нужен только для фильтра (например, убедиться, что заказ из конкретного региона), но затем поля из основной таблицы агрегируются уже после JOIN, и это создает кратность.

Например, вместо того чтобы:

  • использовать JOIN для фильтра,
  • затем агрегировать отдельно,

вы агрегируете все вместе.

3) Несогласованность ключей агрегации

Частая ловушка: один GROUP BY рассчитан на ключ A, а JOIN добавляет измерение B (которое может быть многозначным), и в итоге фактическая группа становится комбинацией A×B.

4) Неправильный тип соединения (INNER vs LEFT)

INNER JOIN может «съедать» записи, где нет дочерних сущностей. LEFT JOIN — сохраняет записи, но если потом вы делаете агрегации по дочерним таблицам, нужно корректно обработать NULL и предотвратить «раздувание» при последующих JOIN.

В обоих случаях итоговые метрики «едут», если не контролировать гранулярность.


Контрольный набор данных: чтобы ошибки было видно быстро

Давайте зафиксируем данные, на которых удобно проверить логику. В статье ниже будет несколько запросов — они должны давать ожидаемые результаты.

Таблицы

code
-- Заказы: 2 заказа
WITH orders AS (
  SELECT 1 AS order_id, DATE '2026-08-01' AS order_date, 10.00::numeric AS order_total
  UNION ALL
  SELECT 2 AS order_id, DATE '2026-08-01' AS order_date, 20.00::numeric AS order_total
),
-- Позиции: заказ 1 имеет 2 позиции, заказ 2 имеет 1 позицию
order_items AS (
  SELECT 1 AS order_id, 101 AS item_id, 4.00::numeric AS item_amount
  UNION ALL
  SELECT 1 AS order_id, 102 AS item_id, 6.00::numeric AS item_amount
  UNION ALL
  SELECT 2 AS order_id, 201 AS item_id, 20.00::numeric AS item_amount
),
-- Возвраты: заказ 1 имеет 2 возврата, заказ 2 — 0
refunds AS (
  SELECT 1001 AS refund_id, 1 AS order_id, 2.00::numeric AS refund_amount
  UNION ALL
  SELECT 1002 AS refund_id, 1 AS order_id, 1.00::numeric AS refund_amount
)
-- Ниже запросы будут использовать эти CTE
SELECT 1;

Ожидаемая логика:

  • Сумма позиций по заказам: 10 + 20 = 30
  • Сумма возвратов: 2 + 1 = 3
  • Если суммировать позиций после JOIN с возвратами, то заказ 1 будет иметь кратность 2 возврата, а значит суммы позиций заказ 1 (10) умножатся до 20.

Плохой запрос и правильная альтернатива

Ошибка: сумма позиций после JOIN с возвратами

Допустим, мы хотим посчитать по дню:

  • gross_items_amount — сумму позиций
  • refund_amount — сумму возвратов

Ниже — распространенный неправильный вариант: вы джойните и суммируете одновременно.

code
WITH orders AS (
  SELECT 1 AS order_id, DATE '2026-08-01' AS order_date, 10.00::numeric AS order_total
  UNION ALL
  SELECT 2 AS order_id, DATE '2026-08-01' AS order_date, 20.00::numeric AS order_total
),
order_items AS (
  SELECT 1 AS order_id, 101 AS item_id, 4.00::numeric AS item_amount
  UNION ALL
  SELECT 1 AS order_id, 102 AS item_id, 6.00::numeric AS item_amount
  UNION ALL
  SELECT 2 AS order_id, 201 AS item_id, 20.00::numeric AS item_amount
),
refunds AS (
  SELECT 1001 AS refund_id, 1 AS order_id, 2.00::numeric AS refund_amount
  UNION ALL
  SELECT 1002 AS refund_id, 1 AS order_id, 1.00::numeric AS refund_amount
)
SELECT
  o.order_date,
  SUM(oi.item_amount) AS gross_items_amount,
  COALESCE(SUM(r.refund_amount), 0) AS refund_amount
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
LEFT JOIN refunds r ON r.order_id = o.order_id
GROUP BY o.order_date;

Что пойдет не так:

  • Для order_id=1 у нас 2 позиции и 2 возврата. JOIN даст 4 строки.
  • SUM(oi.item_amount) по этим 4 строкам превратится в: (4+6) * 2 = 20.
  • order_id=2 — 1 позиция и 0 возвратов ⇒ останется одна строка (в LEFT JOIN), сумма позиций = 20.
  • Итого gross_items_amount станет 40, хотя корректно должно быть 30.

Это и есть «размножение» данных.


Ремонт: сначала агрегируйте на нужной гранулярности, потом джойните

Классический и наиболее надежный подход: свести дочерние таблицы к ключу родителя до того, как вы объедините их между собой.

В нашем примере:

  • сначала агрегируем order_items до уровня order_id,
  • затем агрегируем refunds до уровня order_id,
  • потом делаем join этих агрегатов с orders.

Исправленный запрос (через CTE)

code
WITH orders AS (
  SELECT 1 AS order_id, DATE '2026-08-01' AS order_date, 10.00::numeric AS order_total
  UNION ALL
  SELECT 2 AS order_id, DATE '2026-08-01' AS order_date, 20.00::numeric AS order_total
),
items_by_order AS (
  SELECT
    oi.order_id,
    SUM(oi.item_amount) AS gross_items_amount
  FROM order_items oi
  GROUP BY oi.order_id
),
refunds_by_order AS (
  SELECT
    r.order_id,
    SUM(r.refund_amount) AS refund_amount
  FROM refunds r
  GROUP BY r.order_id
)
SELECT
  o.order_date,
  SUM(COALESCE(i.gross_items_amount, 0)) AS gross_items_amount,
  SUM(COALESCE(r.refund_amount, 0)) AS refund_amount
FROM orders o
LEFT JOIN items_by_order i ON i.order_id = o.order_id
LEFT JOIN refunds_by_order r ON r.order_id = o.order_id
GROUP BY o.order_date;

Теперь гранулярность управляется:

  • items_by_order — ровно 1 строка на order_id,
  • refunds_by_order — ровно 1 строка на order_id,
  • итоговая сумма позиций не зависит от количества возвратов.

Контроль корректности: проверка на контрольных наборах

Чтобы уверенно править запросы, добавляйте проверяемость прямо в SQL (или хотя бы в промежуточных CTE).

Проверка инвариантов

Для нашего набора данных полезен инвариант:

  • сумма позиций по всем заказам должна равняться 30
  • сумма возвратов — 3

Можно добавить отдельный запрос:

code
WITH orders AS (
  SELECT 1 AS order_id, DATE '2026-08-01' AS order_date, 10.00::numeric AS order_total
  UNION ALL
  SELECT 2 AS order_id, DATE '2026-08-01' AS order_date, 20.00::numeric AS order_total
),
order_items AS (
  SELECT 1 AS order_id, 101 AS item_id, 4.00::numeric AS item_amount
  UNION ALL
  SELECT 1 AS order_id, 102 AS item_id, 6.00::numeric AS item_amount
  UNION ALL
  SELECT 2 AS order_id, 201 AS item_id, 20.00::numeric AS item_amount
),
refunds AS (
  SELECT 1001 AS refund_id, 1 AS order_id, 2.00::numeric AS refund_amount
  UNION ALL
  SELECT 1002 AS refund_id, 1 AS order_id, 1.00::numeric AS refund_amount
),
items_by_order AS (
  SELECT oi.order_id, SUM(oi.item_amount) AS gross_items_amount
  FROM order_items oi
  GROUP BY oi.order_id
),
refunds_by_order AS (
  SELECT r.order_id, SUM(r.refund_amount) AS refund_amount
  FROM refunds r
  GROUP BY r.order_id
),
final AS (
  SELECT
    o.order_id,
    o.order_date,
    COALESCE(i.gross_items_amount, 0) AS gross_items_amount,
    COALESCE(r.refund_amount, 0) AS refund_amount
  FROM orders o
  LEFT JOIN items_by_order i ON i.order_id = o.order_id
  LEFT JOIN refunds_by_order r ON r.order_id = o.order_id
)
SELECT
  SUM(gross_items_amount) AS gross_items_total,
  SUM(refund_amount) AS refund_total
FROM final;

Если вы видите, что gross_items_total стал 40 — значит вы где-то снова «размножили» данные.

Автоматическая проверка кратности (удобный диагностический CTE)

Иногда полезно проверить, что агрегаты действительно 1:1:

code
WITH items_by_order AS (
  SELECT
    oi.order_id,
    SUM(oi.item_amount) AS gross_items_amount
  FROM order_items oi
  GROUP BY oi.order_id
),
refunds_by_order AS (
  SELECT
    r.order_id,
    SUM(r.refund_amount) AS refund_amount
  FROM refunds r
  GROUP BY r.order_id
)
SELECT
  (SELECT COUNT(*) FROM items_by_order) AS items_rows,
  (SELECT COUNT(DISTINCT order_id) FROM items_by_order) AS items_distinct_order_id,
  (SELECT COUNT(*) FROM refunds_by_order) AS refunds_rows,
  (SELECT COUNT(DISTINCT order_id) FROM refunds_by_order) AS refunds_distinct_order_id;

Если COUNT(*) отличается от COUNT(DISTINCT order_id) — значит где-то ваша «агрегация к ключу» не сработала (например, вы группировали по лишним колонкам).


Практический паттерн: «агрегируй до JOIN» и «джойни после контроля гранулярности»

В реальных проектах правила проще запомнить как два принципа:

  1. Если нужно суммировать — суммируйте на “правильной” гранулярности до того, как появятся многозначные JOIN.
  2. Любой JOIN, который может сделать больше строк, должен быть “ограничен” агрегатами или дедупликацией.

На практике это выглядит так:

  • Дочерние таблицы (*_items, *_events, *_tags) сначала приводим к уровню родителя (entity_id или date+entity_id).
  • Затем джойним агрегаты с основной таблицей (orders, users, sessions) и делаем итоговую агрегацию по измерениям.

Сложнее: когда нужно несколько разных метрик из разных дочерних таблиц

Частая ситуация: вы хотите одновременно посчитать, например:

  • количество событий events на заказ
  • сумму payments по заказу
  • возвраты refunds по заказу
  • при этом все это свернуть по дням

Если сворачивать «все вместе» после JOIN — риск кратности максимален.

Правильная схема: три независимых агрегата

code
WITH orders AS (
  SELECT 1 AS order_id, DATE '2026-08-01' AS order_date
  UNION ALL
  SELECT 2 AS order_id, DATE '2026-08-01' AS order_date
),
events_by_order AS (
  SELECT order_id, COUNT(*) AS events_cnt
  FROM order_events
  GROUP BY order_id
),
payments_by_order AS (
  SELECT order_id, SUM(amount) AS payments_amount
  FROM payments
  GROUP BY order_id
),
refunds_by_order AS (
  SELECT order_id, SUM(refund_amount) AS refunds_amount
  FROM refunds
  GROUP BY order_id
)
SELECT
  o.order_date,
  SUM(COALESCE(e.events_cnt, 0)) AS events_cnt,
  SUM(COALESCE(p.payments_amount, 0)) AS payments_amount,
  SUM(COALESCE(r.refunds_amount, 0)) AS refunds_amount
FROM orders o
LEFT JOIN events_by_order e ON e.order_id = o.order_id
LEFT JOIN payments_by_order p ON p.order_id = o.order_id
LEFT JOIN refunds_by_order r ON r.order_id = o.order_id
GROUP BY o.order_date;

Заметьте: все агрегаты приходят одним числом на ключ. Поэтому итог по дням определяется только orders.


Типичные ошибки при “ремонте”: как сделать правильно, но снова сломать

Даже после перехода к CTE и агрегатам легко допустить другие логические промахи.

Ошибка A: агрегировали до order_id, но не учли фильтры

Например, вы фильтруете refunds по статусу «accepted» в основной части запроса, но если этот фильтр применен уже после JOIN, он может снова изменить набор строк.

Решение: фильтры, влияющие на набор дочерних данных, должны быть применены внутри соответствующего *_by_order CTE.

Ошибка B: дедупликация вместо агрегата

Иногда кажется, что достаточно SELECT DISTINCT перед JOIN. Но если в таблице есть разные суммы/события, DISTINCT может скрыть реальные значения.

Принцип: если вам нужна сумма/количество — делайте агрегат. Если вам нужна конкретная «последняя запись» — используйте оконные функции и выбирайте одну запись строго по правилу.

Ошибка C: выбор “последнего статуса” без окна

Например, статус заказа хранится как история. Если вы JOIN-ите историю статусов напрямую и потом считаете метрику, опять получите кратность.

Решение: привести историю к 1 строке на заказ:

  • через ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY status_date DESC).

Дедупликация: когда нужно взять ровно одну запись на ключ

Рассмотрим историю статусов заказа: order_status_history содержит несколько статусов. Допустим, вы хотите считать метрики только по текущим статусам.

Неправильно: джойнить всю историю статусов и фильтровать по status='delivered' — строк может быть много.

Правильно: выбрать текущий статус:

code
WITH orders AS (
  SELECT 1 AS order_id, DATE '2026-08-01' AS order_date
  UNION ALL
  SELECT 2 AS order_id, DATE '2026-08-01' AS order_date
),
current_status AS (
  SELECT
    osh.order_id,
    osh.status,
    ROW_NUMBER() OVER (
      PARTITION BY osh.order_id
      ORDER BY osh.status_date DESC
    ) AS rn
  FROM order_status_history osh
)
SELECT
  o.order_date,
  COUNT(*) AS orders_delivered
FROM orders o
JOIN current_status cs
  ON cs.order_id = o.order_id
 AND cs.rn = 1
WHERE cs.status = 'delivered'
GROUP BY o.order_date;

Теперь у вас 1 строка статуса на order_id, и дальше агрегация не раздувается.


Когда “агрегируй до JOIN” не хватает: агрегаты по разным измерениям

Иногда разные метрики должны считаться на разных временных горизонтах или с разной гранулярностью.

Например:

  • выручка по дате заказа
  • возвраты по дате возврата
  • конверсия по дате события

Если вы все сведете к order_date, часть метрик будет концептуально неверной.

Но и здесь можно сохранить управляемость: уводить агрегацию в отдельные CTE так, чтобы каждая метрика жила на своем измерении, а потом объединять их на уровне целевого отчета (обычно через FULL JOIN или через «календарь»/список дат).


Как построить запрос с предсказуемыми результатами: практический чек-лист

Ниже — чек-лист, который реально помогает при отладке.

1) Явно зафиксируйте гранулярность каждого шага

  • orders — 1 строка на order_id
  • items_by_order — 1 строка на order_id
  • refunds_by_order — 1 строка на order_id
  • финальный GROUP BY — по нужному измерению (дата, канал, сегмент)

Если в CTE не гарантирована кратность 1:1 на ключ — вы не закончили ремонт.

2) Агрегируйте дочерние таблицы раньше, чем начинаете «склеивать» их друг с другом

Это основной антидот от размножения.

3) Проверяйте ожидаемые суммы на контрольных наборах

Добавляйте временные запросы, которые сравнивают «идеальные» итоги (например, суммы по ключам) с тем, что получилось после всех JOIN.

4) Смотрите на распределение ключей (кратности) перед расчетами

Один COUNT(*) после JOIN на каждом ключе часто экономит часы.

5) Сначала делайте логически корректно — потом оптимизируйте

Переход на оптимизацию раньше исправления логики приводит к сложному и неверному запросу.


Итог: меньше магии, больше контроля над гранулярностью

Размножение данных в SQL почти всегда сводится к одному: агрегат считается на строках, гранулярность которых неожиданно поменялась из‑за JOIN. Диагностика кратности (cardinality) и подход «агрегируй до JOIN» дают предсказуемые результаты и делают запросы устойчивыми к расширению модели данных (добавили новую таблицу — и вы не получили внезапное умножение метрик).

Если вы только начинаете и хотите системно разобраться с тем, как думать о запросах, JOIN и агрегатах, можно начать с материала уровня «SQL — для начинающих!» — как минимум, он поможет выстроить правильные базовые представления о том, что именно и на каком этапе агрегируется (и почему это важно). В дальнейшем полезно возвращаться к практикам из этой статьи и применять их к вашим реальным схемам — например, отрабатывать паттерн с CTE и контрольными проверками.

Подобный путь обычно лучше всего работает: вы осваиваете не набор операторов, а мышление, которое предотвращает самые дорогие ошибки в аналитике. Если хочется продолжить, хороший следующий шаг — посмотреть практику по SQL для начинающих.

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

Автор

$ sudo teach IT

SQL – для начинающих!

Курс по теме

SQL – для начинающих!

С нуля до уверенных запросов на SQL. Никакой теории без дела — только живые запросы, настоящие данные и готовность к работе с любой базой. Хватит гуглить «как написать JOIN» — разберись один раз и забудь.

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу

Приложения для iPhone и Apple Watch на SwiftUI

Разработка приложений для iPhone и Apple Watch на SwiftUI: навигация, SwiftData, виджеты, часы, выпуск. Нужен Mac с Xcode 27, сами устройства не нужны.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу

Приложения для macOS на SwiftUI

Разработка приложений для Mac на SwiftUI: окна и меню, Liquid Glass, SwiftData, сеть, выпуск. Нужен Mac с macOS 27 и Xcode 27.

Перейти к курсу

Другие статьи

Как читать и писать REST API контракты: статусы, ошибки и схемы ответов
читать

Как читать и писать REST API контракты: статусы, ошибки и схемы ответов

Разберём типовые ошибки в дизайне API и научимся фиксировать контракт: коды HTTP, формат ошибок, версионирование и примеры запросов/ответов.

20 июля 2026 г.
600
SQL JOIN за 10 минут: наглядный гид с реальными примерами
join

SQL JOIN за 10 минут: наглядный гид с реальными примерами

Объясняем INNER, LEFT, RIGHT и FULL JOIN на живых таблицах — без воды и абстрактных схем, только запросы и результаты.

16 июля 2026 г.
530
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1151
Что такое переменная простыми словами: примеры из жизни и первый код
такое

Что такое переменная простыми словами: примеры из жизни и первый код

Разберём, что такое переменная без терминов: как “хранить” значение в памяти и как читать/менять его в программе. Дальше — мини-примеры на вводе/выводе и задания для новичка, чтобы закрепить понимание прямо в коде.

25 сентября 2026 г.
50
FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь
fastapi

FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь

Поймём различия между синхронной обработкой, BackgroundTasks и внешними очередями. Разберём idempotency, ретраи и мониторинг фоновых процессов.

22 июля 2026 г.
800
Какой первый проект выбрать новичку, чтобы не бросить обучение
первый

Какой первый проект выбрать новичку, чтобы не бросить обучение

Подберём 5–7 идей под уровень “с нуля”, объясним, что делать по шагам и как довести проект до результата без перегруза. В конце — как оформить мини-портфолио и что показать, даже если проект маленький.

23 сентября 2026 г.
250

Комментарии

Пока нет комментариев

Содержание

Типовая проблема: агрегат после JOIN и эффект умножения строкМинимальный пример (логическая ошибка)Как диагностировать кратности JOIN: кто кого размножаетШаг 1. Определите «ключ группировки» для метрикиШаг 2. Проверьте кратность на ключахШаг 3. Проверьте, не умножаете ли вы агрегат дваждыТиповые причины ошибок в JOIN-агрегациях1) Агрегирование на неверной гранулярности2) JOIN-агрегация «сквозь» нерелевантную таблицу3) Несогласованность ключей агрегации4) Неправильный тип соединения (INNER vs LEFT)Контрольный набор данных: чтобы ошибки было видно быстроТаблицыПлохой запрос и правильная альтернативаОшибка: сумма позиций после JOIN с возвратамиРемонт: сначала агрегируйте на нужной гранулярности, потом джойнитеИсправленный запрос (через CTE)Контроль корректности: проверка на контрольных наборахПроверка инвариантовАвтоматическая проверка кратности (удобный диагностический CTE)Практический паттерн: «агрегируй до JOIN» и «джойни после контроля гранулярности»Сложнее: когда нужно несколько разных метрик из разных дочерних таблицПравильная схема: три независимых агрегатаТипичные ошибки при “ремонте”: как сделать правильно, но снова сломатьОшибка A: агрегировали до orderid, но не учли фильтрыОшибка B: дедупликация вместо агрегатаОшибка C: выбор “последнего статуса” без окнаДедупликация: когда нужно взять ровно одну запись на ключКогда “агрегируй до JOIN” не хватает: агрегаты по разным измерениямКак построить запрос с предсказуемыми результатами: практический чек-лист1) Явно зафиксируйте гранулярность каждого шага2) Агрегируйте дочерние таблицы раньше, чем начинаете «склеивать» их друг с другом3) Проверяйте ожидаемые суммы на контрольных наборах4) Смотрите на распределение ключей (кратности) перед расчетами5) Сначала делайте логически корректно — потом оптимизируйтеИтог: меньше магии, больше контроля над гранулярностью