Собесов

Ренессанс Кредит SQL — клиенты с ростом MoM > 10% и их коммуникации

SQLОкна и MoMСредняяMiddle

Условие

Найти всех клиентов, у которых торговый оборот в текущем месяце вырос более чем на 10% относительно предыдущего месяца, и вывести коммуникации, которые они получили за прошлые 30 дней.

Таблицы — те же, что в задаче по партнёрской рассылке (purchases(ow_id, partner_id, pos, cash_dt), comms(ow_id, sent_date, channel_nm, partner_id, ...)).

Решение

Подход

  1. Считаем оборот по клиенту по месяцам.
  2. Текущий и предыдущий месяц через LAG (или self-join).
  3. Фильтр current / prev > 1.1.
  4. Подтягиваем коммуникации за прошлые 30 дней.

Реализация

WITH monthly AS (
  SELECT
    ow_id,
    DATE_TRUNC('month', cash_dt) AS m,
    SUM(pos)                     AS total_pos
  FROM purchases
  GROUP BY ow_id, DATE_TRUNC('month', cash_dt)
),
with_prev AS (
  SELECT
    ow_id, m, total_pos,
    LAG(total_pos) OVER (PARTITION BY ow_id ORDER BY m) AS prev_pos
  FROM monthly
),
growers AS (
  SELECT ow_id
  FROM with_prev
  WHERE m = DATE_TRUNC('month', CURRENT_DATE)
    AND prev_pos > 0
    AND total_pos > prev_pos * 1.10
)
SELECT
  g.ow_id,
  c.sent_date,
  c.channel_nm,
  c.partner_id,
  c.sent, c.delivered, c.opened, c.clicked
FROM growers g
JOIN comms c ON c.ow_id = g.ow_id
WHERE c.sent_date >= CURRENT_DATE - INTERVAL '30 day'
ORDER BY g.ow_id, c.sent_date;

Граничные случаи

  • prev_pos IS NULL — клиент первый раз появился в текущем месяце. Формально «рост на бесконечность». Решение бизнеса: либо включаем (prev_pos IS NULL OR ...), либо исключаем как «новые без истории». В коде явно prev_pos > 0 исключает их.
  • prev_pos = 0 не возникнет, если в monthly нет нулевых строк (нет агрегации по пустым месяцам). Если делать через cross join со справочником месяцев — учтите деление на 0.
  • Текущий месяц неполный — рост 10% к полному предыдущему месяцу некорректен. Логичнее сравнивать первые N дней с первыми N днями.

Подводные камни

  1. Сравнение неполного и полного месяца — частая ошибка. Либо ждём конца месяца, либо нормализуем по дням / используем «скользящие 30 дней vs предыдущие 30».
  2. Rounding в условии 10%. > prev * 1.1 или >= prev * 1.1? Явно проговорить, чтобы не было «потерянных» клиентов на границе.
  3. Дубли коммуникаций. Один пуш может отправиться несколько раз — если в задаче «вывести коммуникации», обычно нужно DISTINCT или агрегировать.
  4. Партиции в LAG. Без PARTITION BY ow_id сместится с предыдущим клиентом — катастрофа.
  5. Месяцы с дырами. Если у клиента не было покупок в марте, LAG даст февраль. В большинстве кейсов это ОК (мы сравниваем с «прошлой непустой активностью»), но если требуется именно «прошлый календарный месяц», нужно другое решение через cross join.

Альтернатива через self-join

SELECT cur.ow_id
FROM monthly cur
JOIN monthly prev
  ON cur.ow_id = prev.ow_id
 AND prev.m = cur.m - INTERVAL '1 month'
WHERE cur.m = DATE_TRUNC('month', CURRENT_DATE)
  AND cur.total_pos > prev.total_pos * 1.10;

Это вариант со строгой проверкой именно прошлого календарного месяца.

Эталонный ответ

LAG на месячных агрегатах оборота → фильтр current > prev * 1.1 → JOIN с comms за прошлые 30 дней. Внимательно с prev IS NULL и неполным текущим месяцем.

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти