Датасет практикума: таблицы и зерно
Что означает строка
Заголовок раздела «Что означает строка»У каждой таблицы своё зерно — то, что описывает одна строка. Пока зерно
не зафиксировано, любой SUM даёт число, у которого нет смысла: непонятно,
что именно сложили.
В практикуме четыре зерна:
| Таблица | Одна строка — это | Ключ | Строк |
|---|---|---|---|
suppliers |
запись в справочнике поставщиков | id |
60 |
purchases |
заказ на закупку | id |
400 |
purchase_lines |
позиция заказа | order_id + line_no |
923 |
bids |
предложение поставщика по заказу | order_id + supplier_id |
1600 |
Заказ и позиция — разные вещи. Сумма заказа равна сумме его позиций, поэтому складывать обе величины в одном отчёте нельзя: получите двойной счёт. Это первая ловушка датасета, и она разобрана ниже на числах.
purchases: заказ
Заголовок раздела «purchases: заказ»SELECT id, order_date, category, amount, statusFROM 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 года: они ещё не закрыты.
purchase_lines: позиция заказа
Заголовок раздела «purchase_lines: позиция заказа»SELECT order_id, line_no, item, qty, unit_price, baseline_unit_priceFROM 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— базовая цена: с ней сравнивают факт, когда считают экономию. Это не «цена прошлого года» и не «средняя по рынку» — в датасете это расчётная референсная цена, от которой отсчитывается эффект.
suppliers: поставщик
Заголовок раздела «suppliers: поставщик»SELECT id, name, country, category, risk_scoreFROM 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, чем выше, тем рискованнее.
bids: предложение поставщика
Заголовок раздела «bids: предложение поставщика»SELECT order_id, supplier_id, price, bid_dateFROM 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— дата предложения. Она всегда раньше даты заказа: на этом различии строится урок про время цикла.- Победитель всегда есть среди подавших — кроме десяти заказов, у которых предложений нет вовсе.
Ловушка: join размножает сумму
Заголовок раздела «Ловушка: join размножает сумму»Соедините заказы с позициями и сложите 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 пар дублей.
ООО «УралМеталлСнаб»иУралМеталлСнаб— одна компания с двумя номерами. Находится нормализацией имён, разбор — в уроке про концентрацию.
Что читать дальше
Заголовок раздела «Что читать дальше»- Сборка учебной базы в DuckDB
- ABC-анализ по сумме закупок
- Концентрация поставщиков
- Мастер-данные закупок — почему справочник важнее отчёта