Собесов

Karpov ДЗ: Сравнение выручки с предыдущим днём (LAG)

SQLWindow functionsЛёгкаяJunior

Условие

Дана таблица payments(payment_id, user_id, payment_date, amount, status). Для каждой даты:

  1. Сумма успешных платежей за день.
  2. Сумма за вчерашний день (на той же строке).
  3. Изменение в абсолюте и %.
  4. Флаг «провал»: если выручка просела > 20% к предыдущему дню.

Решение

Запрос

WITH daily AS (
    SELECT payment_date::date AS dt,
           SUM(amount) AS revenue
    FROM payments
    WHERE status = 'success'
    GROUP BY 1
)
SELECT
    dt,
    revenue,
    LAG(revenue) OVER (ORDER BY dt)                                   AS revenue_prev,
    revenue - LAG(revenue) OVER (ORDER BY dt)                         AS abs_diff,
    ROUND((revenue - LAG(revenue) OVER (ORDER BY dt))
          * 100.0 / NULLIF(LAG(revenue) OVER (ORDER BY dt), 0), 2)    AS pct_diff,
    CASE
        WHEN (revenue - LAG(revenue) OVER (ORDER BY dt))
             / NULLIF(LAG(revenue) OVER (ORDER BY dt), 0) < -0.20
        THEN 1 ELSE 0
    END AS drop_flag
FROM daily
ORDER BY dt;

Если пропуски дат

LAG берёт предыдущую строку, а не предыдущий день. Если пропуски возможны:

-- сначала добить календарём
WITH cal AS (
    SELECT generate_series(MIN(payment_date)::date,
                           MAX(payment_date)::date,
                           INTERVAL '1 day')::date AS dt
    FROM payments
),
filled AS (
    SELECT c.dt, COALESCE(d.revenue, 0) AS revenue
    FROM cal c LEFT JOIN daily d ON d.dt = c.dt
)
SELECT *, LAG(revenue) OVER (ORDER BY dt) AS revenue_prev FROM filled;

— теперь LAG = «вчера».

LAG с offset и default

LAG(revenue, 7, 0) OVER (ORDER BY dt) AS revenue_week_ago

7 — на сколько строк назад, 0 — значение для первых строк, где «вчера» не существует.

Альтернатива через self-join (читаемее, но медленнее)

SELECT d.dt, d.revenue, p.revenue AS revenue_prev
FROM daily d
LEFT JOIN daily p ON p.dt = d.dt - INTERVAL '1 day';

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

  1. Пропуски датLAG без календаря даёт «предыдущий имеющийся день», что часто бизнес считает «днём ранее». Согласовать.
  2. status = 'success' в WHERE — отфильтрует и пустые дни. Если хочется «0 в днях без успешных платежей» — фильтр в SUM(amount) FILTER (WHERE status='success').
  3. LAG() без ORDER BY — undefined behavior, не работает.
  4. PARTITION BY user_id если хотим «вчера на пользователя»LAG тогда внутри пользователя.
  5. Division by zero при revenue_prev = 0NULLIF обязателен.
  6. Часовые пояса: payment_date::date в сессионной TZ; UTC vs МСК сдвинет границы дня.
  7. Округление: ROUND(... * 100.0) — без .0 целочисленное деление в Postgres даст 0.

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

WITH daily AS (
    SELECT payment_date::date AS dt, SUM(amount) AS rev
    FROM payments WHERE status='success' GROUP BY 1
)
SELECT dt, rev,
       LAG(rev) OVER (ORDER BY dt) AS rev_prev,
       (rev - LAG(rev) OVER (ORDER BY dt)) * 100.0
            / NULLIF(LAG(rev) OVER (ORDER BY dt), 0) AS pct_diff
FROM daily ORDER BY dt;

Ключи: LAG(col) OVER (ORDER BY dt) для «вчера»; NULLIF от деления на 0; если возможны пропуски — добить календарём, иначе LAG сравнит с «предыдущим имеющимся днём», не с «вчера».

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

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

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