Условие
Дана таблица payments(payment_id, user_id, payment_date, amount, status). Для каждой даты:
- Сумма успешных платежей за день.
- Сумма за вчерашний день (на той же строке).
- Изменение в абсолюте и %.
- Флаг «провал»: если выручка просела > 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_ago7 — на сколько строк назад, 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';Подводные камни
- Пропуски дат —
LAGбез календаря даёт «предыдущий имеющийся день», что часто бизнес считает «днём ранее». Согласовать. status = 'success'в WHERE — отфильтрует и пустые дни. Если хочется «0 в днях без успешных платежей» — фильтр вSUM(amount) FILTER (WHERE status='success').LAG()безORDER BY— undefined behavior, не работает.PARTITION BY user_idесли хотим «вчера на пользователя» —LAGтогда внутри пользователя.- Division by zero при
revenue_prev = 0—NULLIFобязателен. - Часовые пояса:
payment_date::dateв сессионной TZ; UTC vs МСК сдвинет границы дня. - Округление:
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 сравнит с «предыдущим имеющимся днём», не с «вчера».