Собесов

Росмэн SQL: доля возвратов, средний интервал между заказами, топ-клиент

SQLОконные функцииСредняяJunior

Условие

Таблицы:

  • Orders(ID, Date, ClientID) — заказ.
  • OrdersState(ID, Date, StateID) — история статусов (0=принят, 2=отмена, 3=в доставке, 4=доставлен, 5=возврат). Текущий статус — последний.
  • Clients(ClientID, FIO).

Задачи:

  1. Доля заказов со статусом «возврат» и «отмена» от общего числа принятых.
  2. Среднее количество дней между первым и вторым заказом пользователей. Без JOIN-ов.
  3. ФИО и количество заказов клиента, у которого их больше всего.

Решение

1. Доля возвратов и отмен

«Текущий статус» = последний по Date. Используем оконку:

WITH last_state AS (
  SELECT
    ID,
    StateID,
    ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Date DESC) AS rn
  FROM OrdersState
),
final AS (
  SELECT ID, StateID FROM last_state WHERE rn = 1
)
SELECT
  100.0 * SUM(CASE WHEN StateID = 5 THEN 1 ELSE 0 END) / COUNT(*) AS pct_return,
  100.0 * SUM(CASE WHEN StateID = 2 THEN 1 ELSE 0 END) / COUNT(*) AS pct_cancel
FROM final;

2. Средний интервал «первый → второй заказ» без JOIN

SELECT AVG(EXTRACT(EPOCH FROM (second_date - first_date))/86400) AS avg_days
FROM (
  SELECT
    ClientID,
    MIN(CASE WHEN rn = 1 THEN Date END) AS first_date,
    MIN(CASE WHEN rn = 2 THEN Date END) AS second_date
  FROM (
    SELECT
      ClientID, Date,
      ROW_NUMBER() OVER (PARTITION BY ClientID ORDER BY Date) AS rn
    FROM Orders
  ) t
  WHERE rn IN (1, 2)
  GROUP BY ClientID
) x
WHERE second_date IS NOT NULL;

Альтернатива через LAG:

SELECT AVG(diff) AS avg_days FROM (
  SELECT EXTRACT(DAY FROM (Date - LAG(Date) OVER (PARTITION BY ClientID ORDER BY Date))) AS diff,
         ROW_NUMBER() OVER (PARTITION BY ClientID ORDER BY Date) AS rn
  FROM Orders
) t WHERE rn = 2;

3. Самый активный клиент

SELECT c.FIO, cnt
FROM (
  SELECT ClientID, COUNT(*) AS cnt
  FROM Orders
  GROUP BY ClientID
  ORDER BY cnt DESC
  LIMIT 1
) t JOIN Clients c ON c.ClientID = t.ClientID;

С учётом ничьей:

WITH r AS (
  SELECT ClientID, COUNT(*) AS cnt,
         RANK() OVER (ORDER BY COUNT(*) DESC) AS rk
  FROM Orders
  GROUP BY ClientID
)
SELECT c.FIO, r.cnt FROM r JOIN Clients c USING(ClientID) WHERE rk = 1;

Подводные камни

  1. «Принятые» в задаче 1 — может быть всего заказов (любой статус), а не только state=0. Уточнить.
  2. У клиента только один заказ → second_date IS NULL, не учитываем; иначе AVG будет сдвинут.
  3. Запрос 2 «без JOIN» — выполнен через подзапрос с ROW_NUMBER/LAG.
  4. Возвраты и отмены могут пересекаться в истории; финальный статус — единственный валидный.

Эталонный ответ

1) Финальный статус через ROW_NUMBER() ... ORDER BY Date DESCSUM(CASE)/COUNT. 2) Pivot 1-го и 2-го заказа через ROW_NUMBER. 3) GROUP BY ClientID + LIMIT 1 (или RANK=1).

Хочешь увидеть разбор?

Зарегистрируйся бесплатно — откроется развёрнутое решение этой задачи и ещё 4 на выбор.

Зарегистрироваться и увидеть разбор
Уже есть аккаунт? Войти