Как посчитать AOV в SQL

Карьерник — квиз-тренажёр в Telegram с 1500+ вопросами для собесов аналитика. SQL, Python, A/B, метрики. Бесплатно.

Что такое AOV

AOV (Average Order Value) — средний чек заказа.

AOV = Total revenue / Total orders

Одна из ключевых метрик в e-commerce, маркетплейсах, сервисах доставки.

Схема данных

orders (order_id, user_id, total, status, created_at)

1. Общий AOV

SELECT
    COUNT(*) AS orders_cnt,
    SUM(total) AS revenue,
    AVG(total) AS aov
FROM orders
WHERE status = 'paid'
  AND created_at >= '2026-01-01';

Важно: фильтр по status = 'paid' — не включать отменённые.

2. AOV с медианой

AOV чувствительно к выбросам (один большой заказ искажает среднее). Медиана устойчива:

SELECT
    AVG(total) AS aov_mean,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total) AS aov_median,
    PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY total) AS p95
FROM orders
WHERE status = 'paid';

Если AVG >> median — есть тяжёлый хвост (киты).

3. AOV по месяцам

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

4. AOV по категориям

SELECT
    category,
    COUNT(*) AS orders,
    AVG(total) AS aov,
    PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY total) AS median_aov
FROM orders
WHERE status = 'paid'
GROUP BY category
ORDER BY aov DESC;

5. AOV по каналу привлечения

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

6. AOV новых vs повторных покупателей

WITH user_orders AS (
    SELECT
        user_id,
        total,
        created_at,
        ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_num
    FROM orders
    WHERE status = 'paid'
)
SELECT
    CASE WHEN order_num = 1 THEN 'new' ELSE 'returning' END AS buyer_type,
    COUNT(*) AS orders,
    AVG(total) AS aov
FROM user_orders
GROUP BY 1;

Обычно repeat AOV > new AOV (лояльные покупают больше).

7. AOV по сегменту пользователя (value tier)

WITH user_ltv AS (
    SELECT user_id, SUM(total) AS ltv
    FROM orders WHERE status = 'paid'
    GROUP BY user_id
),
user_tiers AS (
    SELECT
        user_id,
        ltv,
        NTILE(10) OVER (ORDER BY ltv DESC) AS decile
    FROM user_ltv
)
SELECT
    ut.decile,
    COUNT(o.order_id) AS orders,
    AVG(o.total) AS aov
FROM orders o
JOIN user_tiers ut ON ut.user_id = o.user_id
WHERE o.status = 'paid'
GROUP BY ut.decile
ORDER BY ut.decile;

Декиль 1 (top 10%) обычно имеет AOV в 2-3 раза выше среднего.

8. AOV по когортам

Как AOV меняется с «возрастом» пользователя:

WITH cohorts AS (
    SELECT user_id, DATE_TRUNC('month', MIN(created_at)) AS cohort_month
    FROM orders WHERE status = 'paid'
    GROUP BY user_id
)
SELECT
    c.cohort_month,
    DATE_TRUNC('month', o.created_at) AS order_month,
    COUNT(*) AS orders,
    AVG(o.total) AS aov
FROM cohorts c
JOIN orders o ON o.user_id = c.user_id
WHERE o.status = 'paid'
GROUP BY c.cohort_month, DATE_TRUNC('month', o.created_at)
ORDER BY c.cohort_month, order_month;

9. AOV с учётом скидок

Иногда важен AOV без скидок:

SELECT
    AVG(total) AS aov_net,        -- после скидки
    AVG(total + discount) AS aov_gross,  -- до скидки
    AVG(discount) AS avg_discount
FROM orders
WHERE status = 'paid';

10. AOV динамика (MoM)

WITH monthly AS (
    SELECT
        DATE_TRUNC('month', created_at) AS month,
        AVG(total) AS aov
    FROM orders
    WHERE status = 'paid'
    GROUP BY 1
)
SELECT
    month,
    aov,
    LAG(aov) OVER (ORDER BY month) AS prev_month_aov,
    (aov - LAG(aov) OVER (ORDER BY month)) / LAG(aov) OVER (ORDER BY month) * 100 AS mom_change_pct
FROM monthly
ORDER BY month;

11. AOV + orders per buyer = revenue per buyer

WITH buyer_stats AS (
    SELECT
        user_id,
        COUNT(*) AS orders_per_buyer,
        AVG(total) AS aov_per_buyer,
        SUM(total) AS rev_per_buyer
    FROM orders WHERE status = 'paid'
    GROUP BY user_id
)
SELECT
    AVG(orders_per_buyer) AS avg_orders_per_buyer,
    AVG(aov_per_buyer) AS avg_aov,
    AVG(rev_per_buyer) AS avg_revenue_per_buyer
FROM buyer_stats;

12. AOV с confidence interval

WITH stats AS (
    SELECT
        AVG(total) AS mean,
        STDDEV(total) AS std,
        COUNT(*) AS n
    FROM orders WHERE status = 'paid'
)
SELECT
    mean AS aov,
    mean - 1.96 * std / SQRT(n) AS ci_lower,
    mean + 1.96 * std / SQRT(n) AS ci_upper
FROM stats;

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

Ошибка 1. Включать отменённые заказы

-- завышает AOV, если есть cancelled с высоким total
AVG(total) FROM orders

-- правильно
AVG(total) FROM orders WHERE status = 'paid'

Ошибка 2. AOV vs Revenue per User

AOV = revenue / orders. Revenue per user = revenue / users. Разные вещи, часто путают.

Ошибка 3. Ср. AOV и median — только среднее

Медиана важна. Если AVG 2000 ₽, median 800 ₽ — распределение сильно скошено.

Ошибка 4. Не нормализовать валюту

Если заказы в разных валютах — обязательно приведите к одной.

Ошибка 5. Смешивать разные категории

Одежда 5000 ₽ vs продукты 300 ₽. Общий AOV ничего не скажет. Сегментация критична.

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

FAQ

AOV или ARPU — что важнее?

Разные метрики. AOV — на заказ. ARPU — на пользователя. Обе важны.

Какой нормальный AOV?

Зависит от категории. Продукты: 500-1500 ₽. Одежда: 2000-5000 ₽. Электроника: 10 000+ ₽.

AOV или medium — что смотреть?

Обе. Среднее — для бизнес-метрик. Медиана — для понимания типичного клиента.

Как понять, AOV — средневзвешенный?

Да, он среднее по всем заказам. Если хотите «по пользователю» — сначала AVG(total) per user, потом среднее по users.


Тренируйте SQL — откройте тренажёр с 1500+ вопросами для собесов.