← Назад к списку
ПрограммированиеBackendSenior

С помощью оконных функций выведите по каждому клиенту его последний заказ и долю этого заказа в общей сумме заказов клиента.

Короткий ответ

  • 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, теряя строки при дублях дат

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru