Как посчитать средний чек в SQL
Содержание:
Формула среднего чека
«Средний чек» в русскоязычной аналитике — это почти всегда 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;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 в разных валютах — что делать?
Приведите всё к одной валюте через таблицу курсов. Среднее по «миксу валют» — не метрика, а шум.