Как посчитать Items Per Order в SQL

Проверь себя · 1/3разбор после ответа
Как поведёт себя фильтр WHERE country IN ('RU', 'KZ') для строки, где country IS NULL?

Зачем Items Per Order

Items Per Order (IPO) — частный случай basket size, но более узкий: считает уникальные позиции (SKU), а не количество единиц товара. Это индикатор того, насколько широко клиенты осваивают ассортимент.

Формула

IPO (lines) = total_distinct_lines / total_orders
IPO (units) = total_quantities / total_orders

Базовый расчёт

WITH order_stats AS (
    SELECT
        order_id,
        COUNT(*) AS lines,
        SUM(quantity) AS units
    FROM order_items
    WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
    GROUP BY order_id
)
SELECT
    COUNT(*) AS orders,
    AVG(lines) AS avg_lines_per_order,
    AVG(units) AS avg_units_per_order,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY lines) AS median_lines
FROM order_stats;

Распределение

WITH order_lines AS (
    SELECT order_id, COUNT(*) AS lines
    FROM order_items
    WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
    GROUP BY order_id
)
SELECT
    CASE
        WHEN lines = 1 THEN '1 SKU'
        WHEN lines = 2 THEN '2 SKUs'
        WHEN lines = 3 THEN '3 SKUs'
        WHEN lines <= 5 THEN '4-5 SKUs'
        ELSE '6+ SKUs'
    END AS bucket,
    COUNT(*) AS orders,
    COUNT(*)::NUMERIC * 100 / SUM(COUNT(*)) OVER () AS pct
FROM order_lines
GROUP BY bucket
ORDER BY MIN(lines);

Если 80%+ заказов состоят из одного SKU — кросс-селл не работает.

По каналам

SELECT
    o.acquisition_channel,
    COUNT(DISTINCT oi.order_id) AS orders,
    COUNT(*)::NUMERIC / NULLIF(COUNT(DISTINCT oi.order_id), 0) AS avg_lines_per_order,
    SUM(oi.quantity)::NUMERIC / NULLIF(COUNT(DISTINCT oi.order_id), 0) AS avg_units_per_order,
    AVG(oi.total) AS aov
FROM order_items oi
JOIN orders o USING (order_id)
WHERE o.created_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY o.acquisition_channel
ORDER BY avg_lines_per_order DESC;
Закрепи формулу items per order в Карьернике
Запомнить надолго — 5 коротких сессий с задачами на эту тему. Бесплатно
Тренировать items per order в Telegram

Динамика IPO по месяцам

SELECT
    DATE_TRUNC('month', order_date) AS month,
    COUNT(DISTINCT order_id) AS orders,
    COUNT(*)::NUMERIC / NULLIF(COUNT(DISTINCT order_id), 0) AS ipo_lines,
    SUM(quantity)::NUMERIC / NULLIF(COUNT(DISTINCT order_id), 0) AS ipo_units,
    -- AOV breakdown
    AVG(unit_price * quantity) AS avg_line_value,
    SUM(total) / NULLIF(COUNT(DISTINCT order_id), 0) AS aov
FROM order_items
WHERE order_date >= CURRENT_DATE - INTERVAL '12 months'
GROUP BY 1
ORDER BY 1;

Связь IPO и AOV

WITH order_summary AS (
    SELECT
        order_id,
        COUNT(*) AS lines,
        SUM(total) AS order_value
    FROM order_items
    WHERE order_date >= CURRENT_DATE - INTERVAL '90 days'
    GROUP BY order_id
)
SELECT
    lines AS items_in_order,
    COUNT(*) AS orders,
    AVG(order_value) AS avg_order_value
FROM order_summary
WHERE lines <= 10
GROUP BY lines
ORDER BY lines;

Связь обычно линейная: больше позиций — выше AOV. Но предельная ценность каждой следующей позиции в заказе часто снижается.

Частые ошибки

Ошибка 1. Позиции против единиц. Два одинаковых SKU — это одна позиция, но две единицы товара. Показывайте обе метрики.

Ошибка 2. Наборы (бандлы). Набор — это один SKU, но внутри него 5 отдельных товаров. Как считать?

Ошибка 3. Отменённые позиции. Часть товаров в заказе отменили. Считайте по факту оставшихся позиций (net).

Ошибка 4. Составные продукты. Подписочный набор — это «одна позиция»? Зависит от того, как вы его учитываете.

Ошибка 5. Компромисс между IPO и AOV. Высокий AOV можно получить и за счёт меньшего числа дорогих товаров, и за счёт большего числа позиций. Это стратегический выбор.

Связанные темы

FAQ

Чем IPO отличается от Basket Size?

Часто это синонимы, но нюанс есть: IPO обычно считают по позициям (lines), а basket size — по единицам товара (units). Главное — заранее договориться, что именно вы измеряете.

Как увеличить IPO?

Работают три рычага: товарные рекомендации, блок «с этим товаром покупают» и порог бесплатной доставки, который мотивирует добрать корзину до нужной суммы.

В чём сложность с наборами?

Решите заранее, считать набор как один атомарный SKU или разбивать на составляющие товары. Если вы продаёте бандлы, эту методику нужно зафиксировать до расчётов, иначе цифры не сойдутся.

Как считать IPO в подписке?

В подписке из месяца в месяц идут одни и те же товары, поэтому IPO почти не меняется. Из-за этого эффект кросс-селла в подписке по IPO не виден — его нужно смотреть отдельно.

IPO падает — что значит?

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