Собесов

Магнит SQL — актуальные цены, фильтры по сумме и квартальная сводка

SQLОкна и агрегацияСредняяJunior

Условие

Дано две таблицы:

items(item_id, name, price, update_date) — история цен на товары. По одному item_id бывает несколько строк.

orders(order_id, user_id, item_id, order_date).

Написать запросы:

  1. Актуальное состояние товаров на 2020-06-01.
  2. Товары, купленные по цене ≥ 3.
  3. Сумма покупок клиента 1.
  4. Сумма всех покупок до 2020-05-01 включительно.
  5. Сумма всех заказов и средняя цена заказа по кварталам.

Решение

1. Актуальное состояние на 2020-06-01

«Актуальная» цена — последняя update_date ≤ 2020-06-01.

SELECT item_id, name, price, update_date
FROM (
  SELECT *,
    ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY update_date DESC) AS rn
  FROM items
  WHERE update_date <= DATE '2020-06-01'
) t
WHERE rn = 1;

2. Товары, купленные по цене ≥ 3

«По какой цене купили» = цена, актуальная на дату заказа.

WITH order_with_price AS (
  SELECT
    o.order_id, o.item_id, o.order_date,
    (SELECT i.price FROM items i
     WHERE i.item_id = o.item_id AND i.update_date <= o.order_date
     ORDER BY i.update_date DESC LIMIT 1) AS price_at_order
  FROM orders o
)
SELECT DISTINCT item_id
FROM order_with_price
WHERE price_at_order >= 3;

Или через JOIN с LATERAL (PostgreSQL):

SELECT DISTINCT o.item_id
FROM orders o
JOIN LATERAL (
  SELECT price
  FROM items i
  WHERE i.item_id = o.item_id AND i.update_date <= o.order_date
  ORDER BY i.update_date DESC LIMIT 1
) i ON TRUE
WHERE i.price >= 3;

3. Сумма покупок клиента 1

Сумма за заказ = цена на момент заказа.

WITH priced AS (
  SELECT o.user_id, o.order_id,
    (SELECT i.price FROM items i
     WHERE i.item_id = o.item_id AND i.update_date <= o.order_date
     ORDER BY i.update_date DESC LIMIT 1) AS price
  FROM orders o
  WHERE o.user_id = 1
)
SELECT user_id, SUM(price) AS total_spent
FROM priced
GROUP BY user_id;

4. Сумма всех покупок до 2020-05-01 включительно

WITH priced AS (
  SELECT o.order_id, o.order_date,
    (SELECT i.price FROM items i
     WHERE i.item_id = o.item_id AND i.update_date <= o.order_date
     ORDER BY i.update_date DESC LIMIT 1) AS price
  FROM orders o
  WHERE o.order_date <= DATE '2020-05-01'
)
SELECT SUM(price) AS total
FROM priced;

5. Сумма всех заказов и средняя цена заказа по кварталам

WITH priced AS (
  SELECT o.order_id, o.order_date,
    (SELECT i.price FROM items i
     WHERE i.item_id = o.item_id AND i.update_date <= o.order_date
     ORDER BY i.update_date DESC LIMIT 1) AS price
  FROM orders o
)
SELECT
  EXTRACT(YEAR    FROM order_date)        AS year,
  EXTRACT(QUARTER FROM order_date)        AS qtr,
  SUM(price)                              AS total_revenue,
  AVG(price)                              AS avg_order_price
FROM priced
GROUP BY 1, 2
ORDER BY 1, 2;

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

  1. «Актуальная цена» — это <= дата, не =. Если на запрашиваемую дату нет апдейта, нужна последняя предыдущая.
  2. Цена по order_date — на момент покупки, а не текущая. Иначе при последующих изменениях цен ретроспектива испортится.
  3. update_date в будущем. Если в данных есть будущая цена, фильтр <= обязательно с учётом.
  4. Производительность LATERAL. На больших объёмах подзапрос на каждую строку дорог. В прод-системах хранят prepared prices_history(item_id, valid_from, valid_to, price) (SCD2) и делают range-join.
  5. AVG(price) — это средняя цена позиции. Если в orders хранятся «итоги заказа» (несколько позиций → одна строка), формула не сходится. В этой задаче считаем 1 позицию = 1 строка orders.
  6. Кварталы. В разных СУБД синтаксис разный: EXTRACT(QUARTER ...), DATEPART(qq, ...), QUARTER(...).

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

Везде, где нужна «цена на момент»: подзапрос/LATERAL берёт MAX update_date <= нужной даты. Для квартальной сводки — EXTRACT(QUARTER ...) с GROUP BY year, qtr. Не путать «текущая цена» и «цена на дату заказа».

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

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

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