Как посчитать средний чек в SQL

Проверь себя · 1/3разбор после ответа
Нужно сверить остатки товаров между учётной системой и складской. Некоторые товары есть только в одной из систем. Как получить полный список расхождений?

Формула среднего чека

«Средний чек» в русскоязычной аналитике — это почти всегда AOV (Average Order Value):

AOV = выручка / количество заказов

В SQL — одна строка:

SELECT AVG(amount) AS aov
FROM orders
WHERE status = 'paid';

Фильтр по оплаченным заказам обязателен — иначе в AOV попадут отменённые и завышат метрику.

1. AOV по периодам

SELECT
    DATE_TRUNC('month', created_at)::DATE AS month,
    AVG(amount)                            AS aov,
    COUNT(*)                               AS orders,
    SUM(amount)                            AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY 1
ORDER BY 1;

Видно, как AOV ведёт себя вместе с количеством заказов и выручкой — это три числа, которые всегда полезно держать в одной таблице.

2. AOV по каналам

SELECT
    u.channel,
    AVG(o.amount)          AS aov,
    COUNT(*)               AS orders
FROM users u
JOIN orders o USING (user_id)
WHERE o.status = 'paid'
GROUP BY u.channel
ORDER BY aov DESC;

3. AOV по платформам

SELECT
    platform,
    AVG(amount) AS aov,
    COUNT(*)    AS orders
FROM orders
WHERE status = 'paid'
GROUP BY platform;

4. AOV по городам

SELECT
    u.city,
    AVG(o.amount) AS aov,
    COUNT(*)      AS orders
FROM users u
JOIN orders o USING (user_id)
WHERE o.status = 'paid'
GROUP BY u.city
HAVING COUNT(*) >= 100     -- фильтруем города с малой выборкой
ORDER BY aov DESC;

На маленьких городах среднее слишком волатильное — HAVING помогает не принимать решения по двум-трём заказам.

5. Средний чек клиента (ARPPU)

Если нужен не «средний на заказ», а «средний на платящего пользователя»:

WITH user_totals AS (
    SELECT user_id, SUM(amount) AS total
    FROM orders
    WHERE status = 'paid'
      AND created_at >= '2026-04-01'
      AND created_at <  '2026-05-01'
    GROUP BY user_id
)
SELECT AVG(total) AS arppu
FROM user_totals;

Это уже ARPPU, а не AOV. Разница существенная: один платящий с пятью заказами будет вкладываться в ARPPU один раз, а в AOV — пятью заказами.

6. Среднее vs медиана

У чека часто тяжёлый правый хвост (редкие большие заказы), и среднее смещается наверх:

SELECT
    AVG(amount)                                                       AS mean_aov,
    PERCENTILE_CONT(0.5)  WITHIN GROUP (ORDER BY amount)              AS median_aov,
    PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY amount)              AS p95_aov
FROM orders
WHERE status = 'paid';

Если mean сильно выше median — распределение скошено, и отчёт про «средний чек 15 000 ₽» может не отражать поведение типичного клиента. В таких случаях разумно показывать пару «mean + median».

7. AOV по категориям с долей в выручке

WITH cat_stats AS (
    SELECT
        p.category,
        AVG(o.amount)  AS aov,
        SUM(o.amount)  AS rev,
        COUNT(*)       AS orders
    FROM products p
    JOIN orders o USING (product_id)
    WHERE o.status = 'paid'
    GROUP BY p.category
)
SELECT
    category,
    aov,
    rev,
    orders,
    ROUND(100.0 * rev / SUM(rev) OVER (), 2) AS share_pct
FROM cat_stats
ORDER BY rev DESC;
Закрепи формулу srednij chek в Карьернике
Запомнить надолго — 5 коротких сессий с задачами на эту тему. Бесплатно
Тренировать srednij chek в Telegram

8. MoM-динамика AOV

WITH monthly AS (
    SELECT
        DATE_TRUNC('month', created_at)::DATE AS month,
        AVG(amount) AS aov
    FROM orders
    WHERE status = 'paid'
    GROUP BY 1
)
SELECT
    month,
    aov,
    LAG(aov) OVER (ORDER BY month) AS prev_aov,
    ROUND(100.0 * (aov - LAG(aov) OVER (ORDER BY month))
                / NULLIF(LAG(aov) OVER (ORDER BY month), 0), 2) AS mom_pct
FROM monthly
ORDER BY month;

9. AOV только по новым пользователям

«Новый пользователь» — тот, кто зарегистрировался в последние 30 дней:

SELECT AVG(o.amount) AS new_user_aov
FROM orders o
JOIN users u USING (user_id)
WHERE o.status = 'paid'
  AND u.registered_at >= o.created_at - INTERVAL '30 day';

Это способ проверить, отличается ли поведение свежей когорты от остальных — иногда маркетинг приводит аудиторию с сильно другим чеком, и это важно замечать.

10. Декомпозиция: items per order × avg item price

WITH order_details AS (
    SELECT
        o.order_id,
        COUNT(*)                                AS items_count,
        AVG(oi.price)                           AS avg_item_price,
        SUM(oi.price * oi.quantity)             AS order_total
    FROM orders o
    JOIN order_items oi USING (order_id)
    WHERE o.status = 'paid'
    GROUP BY o.order_id
)
SELECT
    AVG(items_count)    AS avg_items_per_order,
    AVG(avg_item_price) AS avg_item_price,
    AVG(order_total)    AS aov
FROM order_details;

Эта разбивка помогает отвечать на вопрос «почему упал AOV» — из-за меньшего числа товаров в корзине или из-за снижения средней цены единицы.

11. AOV по A/B-группам

SELECT
    ab_group,
    AVG(o.amount)  AS aov,
    COUNT(*)       AS orders
FROM orders o
JOIN ab_test_users a USING (user_id)
WHERE o.status = 'paid'
  AND o.created_at BETWEEN a.test_start AND a.test_end
GROUP BY ab_group;

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

1. Учитывать отменённые заказы

-- AOV завышен, потому что попадают все статусы
SELECT AVG(amount) FROM orders;

-- Правильно: только оплаченные
SELECT AVG(amount) FROM orders WHERE status = 'paid';

2. Целочисленное деление

-- Если amount — integer, результат округляется
SELECT SUM(amount) / COUNT(*) FROM orders;

-- AVG(amount) сам приведёт к дробному типу
SELECT AVG(amount) FROM orders;

-- Или явно приводим тип
SELECT SUM(amount)::NUMERIC / NULLIF(COUNT(*), 0) FROM orders;

3. Забыть про возвраты

Если возвраты оформляются отдельной записью, SUM(amount) их не вычтет, и «net AOV» будет завышен. Используйте SUM(amount - COALESCE(refund_amount, 0)).

4. Смешивать AOV и ARPPU

  • AOV = выручка / заказы.
  • ARPPU = выручка / платящие пользователи.

У одного покупателя может быть несколько заказов, поэтому метрики почти всегда разные.

5. Смешивать валюты

Если в orders идут разные валюты, приводите всё к одной (обычно USD или RUB) через таблицу курсов. Среднее по смешанным валютам — бессмысленное число.

Интерпретация

  • AOV растёт, количество заказов стабильно — клиенты стали покупать больше за раз (работает upsell, кросс-селл, акции на дорогие позиции).
  • AOV падает, заказов больше — пришла новая, более «чувствительная к цене» аудитория.
  • AOV падает, заказы падают — скорее всего, проблема с продуктом или предложением.

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

FAQ

AOV считать на заказ или на пользователя?

AOV — на заказ (Average Order Value). «На пользователя» — это уже ARPPU.

Нужна ли медиана?

Для метрик с тяжёлым хвостом — да. Особенно в e-commerce с премиум-категориями и в SaaS с enterprise-клиентами.

Как обрабатывать возвраты?

Вычитать из выручки: SUM(amount - COALESCE(refund_amount, 0)). Полезно держать отдельно «gross AOV» и «net AOV», чтобы видеть масштаб возвратов.

AOV в разных валютах — что делать?

Приведите всё к одной валюте через таблицу курсов. Среднее по «миксу валют» — не метрика, а шум.