С помощью оконных функций выведите по каждому клиенту его последний заказ и долю этого заказа в общей сумме заказов клиента.
Короткий ответ
- ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC)
- Оконные функции не схлопывают строки в отличие от GROUP BY
- SUM(amount) OVER (PARTITION BY customer_id) — сумма без группировки
- Фильтр rn = 1 только через подзапрос или CTE
- В WHERE оконную функцию использовать нельзя
- Альтернатива в Postgres — DISTINCT ON
Оконные функции дают агрегаты и нумерацию без схлопывания строк; фильтр по ним возможен только во внешнем запросе.
Как сказать вслух
пример ответаБеру ROW_NUMBER по партиции клиента с сортировкой по дате по убыванию — у последнего заказа номер один. Той же оконной конструкцией считаю сумму всех заказов клиента: SUM с PARTITION BY не схлопывает строки, как GROUP BY, а приписывает итог каждой строке. Важно, что отфильтровать по номеру строки прямо в WHERE нельзя — оконные функции вычисляются после него, поэтому оборачиваю запрос в CTE и снаружи беру строки с номером один.
Подробный ответ
Основной ответ
Оконные функции вычисляются над «окном» строк, заданным PARTITION BY и ORDER BY, и не уменьшают число строк результата — этим они отличаются от GROUP BY. ROW_NUMBER даёт уникальную нумерацию внутри партиции; RANK и DENSE_RANK допускают одинаковые номера при равенстве значений сортировки — выбор зависит от того, что делать с заказами на одну дату. Агрегат с OVER (SUM, AVG, COUNT) считает значение по партиции и дублирует его в каждую строку, что идеально для долей и процентов. Логический порядок выполнения SQL ставит оконные функции после WHERE/GROUP BY/HAVING, но до ORDER BY — поэтому фильтрация по результату окна требует подзапроса, CTE или (в новых СУБД) QUALIFY. Postgres-альтернатива для «последней записи на группу» — DISTINCT ON (customer_id) ... ORDER BY customer_id, created_at DESC — лаконичнее, но нестандартна.
Ключевые моменты
- ROW_NUMBER vs RANK. При равных датах ROW_NUMBER выберет одну строку произвольно, RANK вернёт обе — надо явно решить, что считать «последним».
- Окно против GROUP BY. GROUP BY схлопывает группу в строку, окно оставляет все строки и дописывает агрегат — можно смешивать детальные и итоговые значения.
- Фильтр по окну. WHERE rn = 1 внутри того же SELECT — синтаксическая ошибка; нужен внешний запрос или QUALIFY там, где он есть.
Практический контекст
Паттерн «последняя запись на группу» — один из самых частых в бэкенде: последний платёж, актуальный статус, свежая версия документа. На собеседованиях уровня middle+ оконные функции — водораздел: кто пишет аналитические запросы руками, решает за пару минут. Дополнительные вопросы: чем заменить (LATERAL, DISTINCT ON), как это индексировать, что с производительностью на больших партициях.
Пример кода
WITH ranked AS (
SELECT o.*,
ROW_NUMBER() OVER (PARTITION BY o.customer_id
ORDER BY o.created_at DESC) AS rn,
SUM(o.amount) OVER (PARTITION BY o.customer_id) AS customer_total
FROM orders o
)
SELECT customer_id,
id AS last_order_id,
amount AS last_order_amount,
ROUND(100.0 * amount / customer_total, 2) AS pct_of_total
FROM ranked
WHERE rn = 1;Частые ошибки
- Пытаются написать WHERE rn = 1 в том же SELECT, где объявлено окно
- Не видят разницы ROW_NUMBER и RANK при одинаковых значениях сортировки
- Решают задачу через GROUP BY с MAX(created_at) и JOIN, теряя строки при дублях дат