Перейти к содержимому

Датасет практикума: таблицы и зерно

У каждой таблицы своё зерно — то, что описывает одна строка. Пока зерно не зафиксировано, любой SUM даёт число, у которого нет смысла: непонятно, что именно сложили.

В практикуме четыре зерна:

Таблица Одна строка — это Ключ Строк
suppliers запись в справочнике поставщиков id 60
purchases заказ на закупку id 400
purchase_lines позиция заказа order_id + line_no 923
bids предложение поставщика по заказу order_id + supplier_id 1600

Заказ и позиция — разные вещи. Сумма заказа равна сумме его позиций, поэтому складывать обе величины в одном отчёте нельзя: получите двойной счёт. Это первая ловушка датасета, и она разобрана ниже на числах.

SELECT id, order_date, category, amount, status
FROM purchases ORDER BY id LIMIT 3;
┌────┬────────────┬───────────┬───────────┬──────────┐
│ id │ order_date │ category │ amount │ status │
├────┼────────────┼───────────┼───────────┼──────────┤
│ 1 │ 2025-05-03 │ Упаковка │ 163100.28 │ исполнен │
│ 2 │ 2024-06-15 │ Упаковка │ 165485.92 │ исполнен │
│ 3 │ 2024-04-16 │ Электрика │ 478022.68 │ исполнен │
└────┴────────────┴───────────┴───────────┴──────────┘
  • id — номер заказа, он же ключ.
  • order_date — дата заказа. Период: 2024-01-06 … 2025-12-25.
  • supplier_id — ссылка на suppliers.id, поставщик, которому ушёл заказ.
  • category — категория закупки, девять значений.
  • amount — сумма заказа в рублях, равна сумме позиций.
  • currency — во всех строках RUB. Одна валюта — сознательное упрощение: суммировать рубли с долларами без курса на дату нельзя, а курс в курс не входит.
  • status — исполнен или в работе. Второй статус у заказов за ноябрь и декабрь 2025 года: они ещё не закрыты.
SELECT order_id, line_no, item, qty, unit_price, baseline_unit_price
FROM purchase_lines WHERE order_id = 1 ORDER BY line_no;
┌──────────┬─────────┬──────────────────────┬─────┬────────────┬─────────────────────┐
│ order_id │ line_no │ item │ qty │ unit_price │ baseline_unit_price │
├──────────┼─────────┼──────────────────────┼─────┼────────────┼─────────────────────┤
│ 1 │ 1 │ Стрейч-плёнка 500 мм │ 52 │ 1338.49 │ 1498.03 │
│ 1 │ 2 │ Гофрокороб 400×300 │ 60 │ 900.33 │ 907.64 │
│ 1 │ 3 │ Скотч упаковочный │ 110 │ 358.90 │ 374.05 │
└──────────┴─────────┴──────────────────────┴─────┴────────────┴─────────────────────┘
  • order_id + line_no — составной ключ: позиция уникальна внутри заказа.
  • item — наименование позиции, написанное так, как его завёл заказчик. Одинаковые по смыслу позиции могут называться по-разному.
  • qty и unit_price — количество и цена за единицу. Сумма позиции — qty * unit_price.
  • baseline_unit_price — базовая цена: с ней сравнивают факт, когда считают экономию. Это не «цена прошлого года» и не «средняя по рынку» — в датасете это расчётная референсная цена, от которой отсчитывается эффект.
SELECT id, name, country, category, risk_score
FROM suppliers ORDER BY id LIMIT 3;
┌────┬──────────────────────┬─────────┬───────────────┬────────────┐
│ id │ name │ country │ category │ risk_score │
├────┼──────────────────────┼─────────┼───────────────┼────────────┤
│ 1 │ ООО «УралМеталлСнаб» │ RU │ Металлопрокат │ 22 │
│ 2 │ АО «СтальТрейд» │ RU │ Металлопрокат │ 35 │
│ 3 │ ООО «МеталлСнаб» │ RU │ Металлопрокат │ 28 │
└────┴──────────────────────┴─────────┴───────────────┴────────────┘
  • name — имя в том виде, в каком оно попало в справочник. Именно здесь живут дубли: одна компания может быть заведена дважды.
  • country — страна регистрации: RU, CN, BY, KZ, TR.
  • category — основная категория поставщика.
  • risk_score — условный балл риска от 0 до 100, чем выше, тем рискованнее.
SELECT order_id, supplier_id, price, bid_date
FROM bids WHERE order_id = 1 ORDER BY bid_date;
┌──────────┬─────────────┬───────────┬────────────┐
│ order_id │ supplier_id │ price │ bid_date │
├──────────┼─────────────┼───────────┼────────────┤
│ 1 │ 60 │ 146365.07 │ 2025-04-14 │
│ 1 │ 40 │ 194446.36 │ 2025-04-14 │
│ 1 │ 42 │ 201429.10 │ 2025-04-16 │
│ 1 │ 39 │ 185847.83 │ 2025-04-17 │
└──────────┴─────────────┴───────────┴────────────┘
  • price — цена, которую поставщик предложил по этому заказу.
  • bid_date — дата предложения. Она всегда раньше даты заказа: на этом различии строится урок про время цикла.
  • Победитель всегда есть среди подавших — кроме десяти заказов, у которых предложений нет вовсе.

Соедините заказы с позициями и сложите amount — получите не бюджет, а бюджет, умноженный на число позиций:

SELECT round(sum(amount)) AS "по заказам" FROM purchases;
┌────────────┐
│ по заказам │
├────────────┤
│ 277915910 │
└────────────┘
SELECT round(sum(p.amount)) AS "после join с позициями", count(*) AS "строк"
FROM purchases p JOIN purchase_lines l ON l.order_id = p.id;
┌────────────────────────┬───────┐
│ после join с позициями │ строк │
├────────────────────────┼───────┤
│ 595010676 │ 923 │
└────────────────────────┴───────┘

Один и тот же заказ попал в выборку столько раз, сколько у него позиций, и его сумма посчиталась столько же раз. Правило простое: заказы агрегируют по purchases, позиции — по purchase_lines, а если нужны обе гранулярности сразу, сумму заказа берут до соединения — в подзапросе или в CTE.

В датасете намеренно оставлены вещи, которые встречаются в живых выгрузках:

SELECT
(SELECT count(*) FROM purchases p
WHERE NOT EXISTS (SELECT 1 FROM bids b WHERE b.order_id = p.id)) AS "заказов без предложений",
(SELECT count(*) FROM purchase_lines
WHERE unit_price > baseline_unit_price) AS "позиций дороже базы",
(SELECT count(*) FROM suppliers s
WHERE NOT EXISTS (SELECT 1 FROM purchases p WHERE p.supplier_id = s.id)) AS "поставщиков без заказов";
┌─────────────────────────┬─────────────────────┬─────────────────────────┐
│ заказов без предложений │ позиций дороже базы │ поставщиков без заказов │
├─────────────────────────┼─────────────────────┼─────────────────────────┤
│ 10 │ 109 │ 3 │
└─────────────────────────┴─────────────────────┴─────────────────────────┘

Что это значит на практике:

  • 10 заказов без предложений. В bids для них нет ни строки. Такие заказы выпадают из любого расчёта, где нужны даты предложений, и остаются в spend-анализе — это несоответствие надо объяснять, а не замалчивать.
  • 109 позиций дороже базовой цены. Экономия по портфелю положительная, но внутри неё есть переплата: 2,39 млн ₽ против 13,78 млн ₽ выигрыша. Средняя экономия эти два потока складывает и прячет.
  • 3 поставщика без заказов. Записи в справочнике есть, движения денег нет. В расчёте доли рынка они не участвуют, но в счётчике «поставщиков в базе» участвуют, и цифры расходятся.
  • 6 пар дублей. ООО «УралМеталлСнаб» и УралМеталлСнаб — одна компания с двумя номерами. Находится нормализацией имён, разбор — в уроке про концентрацию.