ABC-анализ закупок по сумме
Вопрос, с которого начинается почти любой spend-анализ: на что уходят деньги. Отчёт по всем девяти категориям сразу бесполезен — в нём нет приоритета. ABC расставляет категории по вкладу в затраты и показывает, где заканчиваются деньги, которые стоит контролировать индивидуально.
Считать будем по сумме заказов за 24 месяца. Метрика — деньги, а не число заказов и не количество позиций: категория с сотней мелких заказов и категория с десятком крупных — разные истории, и путать их нельзя.
Нужна одна таблица — purchases. Одна строка — один заказ, поле amount —
его сумма. Позиции заказов здесь не нужны: сумма заказа уже собрана из них,
и соединение с purchase_lines умножит каждую строку на число позиций.
Все 400 заказов идут в расчёт. Статус не фильтруем: заказ, который ещё не закрыт, всё равно связывает деньги. Чем это грозит при фильтрации — в разделе про ошибки.
Сумма по категории, доля от итога и накопленная доля. Накопленную считает оконная функция: она складывает суммы от самой крупной категории вниз.
WITH cat AS ( SELECT category, count(*) AS orders, sum(amount) AS spend FROM purchases GROUP BY category),tot AS (SELECT sum(spend) AS total FROM cat)SELECT category AS "категория", orders AS "заказов", round(spend / 1000000, 1) AS "млн ₽", round(100 * spend / (SELECT total FROM tot), 1) AS "доля, %", round(100 * sum(spend) OVER (ORDER BY spend DESC, category) / (SELECT total FROM tot), 1) AS "накопл., %"FROM catORDER BY spend DESC, category;┌─────────────────┬─────────┬───────┬─────────┬────────────┐│ категория │ заказов │ млн ₽ │ доля, % │ накопл., % │├─────────────────┼─────────┼───────┼─────────┼────────────┤│ Металлопрокат │ 72 │ 130.0 │ 46.8 │ 46.8 ││ ИТ-оборудование │ 48 │ 52.5 │ 18.9 │ 65.7 ││ Электрика │ 48 │ 30.9 │ 11.1 │ 76.8 ││ Логистика │ 48 │ 20.2 │ 7.3 │ 84.0 ││ Спецодежда │ 40 │ 12.8 │ 4.6 │ 88.6 ││ Крепёж │ 48 │ 11.5 │ 4.2 │ 92.8 ││ Химия и ГСМ │ 24 │ 9.1 │ 3.3 │ 96.1 ││ Упаковка │ 48 │ 8.7 │ 3.1 │ 99.2 ││ Канцелярия │ 24 │ 2.3 │ 0.8 │ 100.0 │└─────────────────┴─────────┴───────┴─────────┴────────────┘- Одна категория — почти половина бюджета. Металлопрокат даёт 130,0 млн ₽ из 277,9 млн ₽, или 46,8%. Один процент цены на прокат — это 1,3 млн ₽, и он важнее любой экономии в хвосте.
- Первые три категории — 76,8%. Металлопрокат, ИТ-оборудование и Электрика забирают три четверти затрат. Это и есть граница, за которой начинается индивидуальная работа с поставщиком и ценой.
- Число заказов не показывает деньги. У Упаковки 48 заказов — столько же, сколько у ИТ-оборудования, но 8,7 млн ₽ против 52,5 млн ₽. Если сортировать по количеству заказов, приоритеты получатся другими и неверными.
- Хвост короткий, но не пустой. Три последние категории — 7,2% затрат, накопленная доля идёт от 96,1 к 100,0. Управлять ими индивидуально дороже, чем они приносят: это работа правилами и каталогом.
- Накопленный процент не складывается из округлённых долей. 76,8 + 7,3 даёт 84,1, а в таблице 84,0: накопленная сумма считается по неокруглённым значениям, а доли показаны с одним знаком. Расхождение в 0,1 — это округление, а не ошибка расчёта, но именно из-за него отчёты не сходятся с калькулятором.
Классы A, B, C
Заголовок раздела «Классы A, B, C»Границы групп задают заранее. Распространённая схема: A — до 80% накопленной доли, B — до 95%, C — остаток. Проверим, сколько категорий попадает в каждый класс:
WITH cat AS ( SELECT category, sum(amount) AS spend FROM purchases GROUP BY category),calc AS ( SELECT category, sum(spend) OVER (ORDER BY spend DESC, category) AS running, sum(spend) OVER () AS total FROM cat),cls AS ( SELECT category, CASE WHEN 100 * running / total <= 80 THEN 'A' WHEN 100 * running / total <= 95 THEN 'B' ELSE 'C' END AS abc FROM calc)SELECT abc AS "класс", count(*) AS "категорий", string_agg(category, ', ' ORDER BY category) AS "категории"FROM cls GROUP BY abc ORDER BY abc;┌───────┬───────────┬───────────────────────────────────────────┐│ класс │ категорий │ категории │├───────┼───────────┼───────────────────────────────────────────┤│ A │ 3 │ ИТ-оборудование, Металлопрокат, Электрика ││ B │ 3 │ Крепёж, Логистика, Спецодежда ││ C │ 3 │ Канцелярия, Упаковка, Химия и ГСМ │└───────┴───────────┴───────────────────────────────────────────┘Треть категорий даёт 76,8% денег, треть — 16,0%, треть — 7,2%. Ровное деление 3/3/3 здесь случайность конкретного датасета, а не правило: в живых данных в группу C обычно попадает большинство строк.
Типичные ошибки
Заголовок раздела «Типичные ошибки»Первая и самая дорогая — фильтр по статусу. Заказы за ноябрь и декабрь 2025
года ещё не закрыты, и условие status = 'исполнен' вырезает их целиком:
SELECT round(sum(amount)/1000000, 1) AS "все заказы, млн ₽", round(sum(amount) FILTER (WHERE status = 'исполнен')/1000000, 1) AS "только исполненные, млн ₽", count(*) FILTER (WHERE status = 'в работе') AS "заказов в работе"FROM purchases;┌───────────────────┬───────────────────────────┬──────────────────┐│ все заказы, млн ₽ │ только исполненные, млн ₽ │ заказов в работе │├───────────────────┼───────────────────────────┼──────────────────┤│ 277.9 │ 252.5 │ 33 │└───────────────────┴───────────────────────────┴──────────────────┘9,1% бюджета исчезает, и вместе с ним — два последних месяца: на графике динамики линия просто обрывается. Если статус нужен, задавайте условие через исключение, а не через единственное значение: иначе новые статусы будут молча выпадать из отчёта.
| Ошибка | Что происходит | Как правильно |
|---|---|---|
| Сортировать по числу заказов, а называть отчёт ABC по деньгам | У Металлопроката 72 заказа и 130,0 млн ₽, у Упаковки 48 заказов и 8,7 млн ₽: заказов в 1,5 раза меньше, денег — в 15 раз | Метрика одна и названа в заголовке отчёта |
| Считать накопленную долю от максимальной суммы | Последняя строка даёт не 100%, а другое число | Знаменатель — общая сумма всех категорий |
Соединять purchases с purchase_lines и суммировать amount |
Каждый заказ считается столько раз, сколько у него позиций: 595,0 млн ₽ вместо 277,9 млн ₽ | Суммировать заказы до соединения |
| Брать границы 80/15/5 как норму | Группы получаются несопоставимыми между отчётами: два аналитика с разными границами дадут разные A | Границы фиксируют в методике до расчёта |
| Сравнивать ABC за разные периоды без пересчёта | Сезонная категория переезжает из C в A и обратно | Сравнивать на одном окне и одной метрике |
Что читать дальше
Заголовок раздела «Что читать дальше»- XYZ по вариативности — вторая координата матрицы
- Концентрация поставщиков — та же сумма, но со стороны поставщика
- ABC и XYZ анализ закупок — методика без SQL