Условие
По таблице otp_log(client_id, sessionId, datetime, status, category) одним запросом сформировать:
- Количество успешно введённых OTP в разрезе категорий.
- Долю каждой категории по убыванию.
- Количество с накопительным итогом.
- Общее количество введённых OTP (как ещё одна колонка).
- Последний полный месяц от текущего.
Решение
Подход
Все метрики на одном уровне агрегации (category). Окно по всем строкам сразу даёт total и накопительный итог.
Реализация
WITH last_full_month AS (
-- Полный месяц, предшествующий текущему
SELECT
DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1 month') AS month_start
),
agg AS (
SELECT
category,
COUNT(*) AS success_cnt
FROM otp_log, last_full_month lfm
WHERE status = 'success'
AND datetime >= lfm.month_start
AND datetime < lfm.month_start + INTERVAL '1 month'
GROUP BY category
)
SELECT
category,
success_cnt,
ROUND(100.0 * success_cnt / SUM(success_cnt) OVER (), 2) AS share_pct,
SUM(success_cnt) OVER (ORDER BY success_cnt DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
AS cum_cnt,
SUM(success_cnt) OVER () AS total_cnt,
(SELECT month_start FROM last_full_month) AS report_month
FROM agg
ORDER BY success_cnt DESC;Что важно
SUM(...) OVER ()безPARTITION BY— считает по всему результату, идеально для итога и доли.- Накопительный итог требует
ORDER BYиROWS BETWEEN ...для корректности. - «Последний полный месяц» — на 5 мая это апрель. Формула
DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1 month')даёт начало апреля.
Ожидаемый вид
| category | success_cnt | share_pct | cum_cnt | total_cnt | report_month |
|---|---|---|---|---|---|
| payment | 5000 | 50.00 | 5000 | 10000 | 2026-04-01 |
| transfer | 3000 | 30.00 | 8000 | 10000 | 2026-04-01 |
| login | 2000 | 20.00 | 10000 | 10000 | 2026-04-01 |
Подводные камни
statusenum. Часто'success'пишут как1(boolean) или'OK'. Проверить заранее.- Точность доли. Без
100.0 *целочисленное деление в некоторых СУБД даст 0. - Накопительный итог при равных
success_cnt. Если две категории с одинаковым числом,ROWS UNBOUNDED PRECEDINGкорректно прибавляет одну, потом другую;RANGEслил бы их в одну точку. - «Последний полный месяц от текущего». Если сегодня 1-е число, то «последний полный» — это позапрошлый. Уточнить ТЗ.
- NULL в category. Попадает ли он в результат? Обычно нужно либо отфильтровать, либо вынести в
'unknown'.
Эталонный ответ
CTE с агрегацией COUNT(*) FILTER (WHERE status='success') по категории за последний полный месяц → итог через оконку SUM(...) OVER () для доли и total + накопительный SUM(...) OVER (ORDER BY ... DESC ROWS UNBOUNDED PRECEDING). «Последний полный месяц» — DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1 month').