Как просуммировать покупки пользователя, исключая покупки после бана, и сравнить сумму с порогом в SQL?
Что проверяют
Умеете ли вы перевести бизнес-условие в корректный SQL: соединение с банами, фильтр до агрегации и сравнение после неё. Следом почти всегда спрашивают про пользователей без бана и про повторные баны.
Как отвечать
Сначала уточните условие вслух: учитываем ли пользователей без банов, какой бан брать при нескольких, входит ли покупка в ту же секунду, что и бан. Каркас для PostgreSQL:
WITH first_ban AS (
SELECT user_id, MIN(banned_at) AS banned_at
FROM user_bans GROUP BY user_id
)
SELECT p.user_id, SUM(p.amount) AS total
FROM purchases p
LEFT JOIN first_ban b ON b.user_id = p.user_id
WHERE b.banned_at IS NULL OR p.created_at < b.banned_at
GROUP BY p.user_id
HAVING SUM(p.amount) > %(threshold)s;LEFT JOIN: пользователи без бана тоже попадают в выборку, у нихb.banned_atравенNULL, и условиеIS NULLих пропускает;WHEREиHAVING: покупки после бана отсекаются до группировки, порог сравнивается уже с суммой;- условная агрегация:
SUM(p.amount) FILTER (WHERE ...)в PostgreSQL илиSUM(CASE WHEN ... THEN p.amount ELSE 0 END), если в одном запросе нужны и полная сумма, и сумма до бана; - флаг вместо фильтра: если нужны все пользователи, а не только превысившие, верните
SUM(p.amount) > 1000 AS over_limit.
Из Python порог передаётся параметром драйвера (%(threshold)s в psycopg, :threshold в text() у SQLAlchemy), а не f-строкой. Деньги храните в NUMERIC, не во float.
Где спотыкаются
Пишут подзапрос p.created_at < (SELECT ban_date ...). Для пользователя без бана он вернёт NULL, сравнение тоже даст NULL, и все его покупки пропадут. При двух банах подзапрос вернёт две строки, и запрос упадёт.
Ставят псевдоним в HAVING: HAVING total > 1000 проходит в MySQL и SQLite, а в PostgreSQL это ошибка.
Исключают забаненных целиком через status != 'banned'. Задача была другая: их покупки до бана тоже входят в сумму.