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

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

Средний чек (AOV) = Общая выручка / Количество заказов

В SQL — проще некуда:

SELECT AVG(amount) AS aov FROM orders;

Или эквивалентно:

SELECT SUM(amount) / COUNT(*) AS aov FROM orders;

По периодам

SELECT DATE_TRUNC('month', created_at) AS month,
    AVG(amount) AS aov,
    COUNT(*) AS orders,
    SUM(amount) AS revenue
FROM orders
GROUP BY 1 ORDER BY 1;

Видим динамику AOV по месяцам.

По сегментам

По каналам

SELECT u.channel, AVG(o.amount) AS aov
FROM users u JOIN orders o USING (user_id)
GROUP BY u.channel
ORDER BY aov DESC;

По платформам

SELECT platform, AVG(amount) AS aov
FROM orders
GROUP BY platform;

По городам

SELECT u.city, AVG(o.amount) AS aov
FROM users u JOIN orders o USING (user_id)
GROUP BY u.city
HAVING COUNT(*) > 100  -- исключаем города с малым числом заказов
ORDER BY aov DESC;

Средний чек клиента (не заказа)

Если нужен средний чек на пользователя за период:

WITH user_totals AS (
    SELECT user_id, SUM(amount) AS total
    FROM orders
    WHERE created_at >= '2026-04-01'
    GROUP BY user_id
)
SELECT AVG(total) AS avg_per_user FROM user_totals;

Это revenue per paying user (ARPPU), не AOV.

Медиана 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;

Если mean > median — правый хвост. Подробнее про медиану.

Прокачать тему на реальных задачах удобно в боте @kariernik_bot — база вопросов собрана с собеседований в Яндексе, Авито, Ozon, Тинькофф.

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)
    GROUP BY p.category
)
SELECT category, aov, rev,
    ROUND(100.0 * rev / SUM(rev) OVER (), 2) AS share_pct
FROM cat_stats
ORDER BY rev DESC;

MoM изменение AOV

WITH monthly AS (
    SELECT DATE_TRUNC('month', created_at) AS month,
        AVG(amount) AS aov
    FROM orders 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)) /
        LAG(aov) OVER (ORDER BY month), 2) AS mom_pct
FROM monthly;

AOV с фильтрами

Только оплаченные заказы:

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

Только заказы от новых пользователей:

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

AOV: 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)
    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 упал — проверяем, что именно: количество товаров в корзине или средняя цена.

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

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

На собесе такие штуки часто спрашивают. Быстрый способ довести до автоматизма — тренажёр в Telegram с задачами из реальных интервью.

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

1. Считать AOV включая отменённые

-- ❌ Завышает AOV
SELECT AVG(amount) FROM orders;

-- ✅ Только paid
SELECT AVG(amount) FROM orders WHERE status = 'paid';

2. Integer division

-- ❌
SELECT SUM(amount) / COUNT(*) FROM orders;
-- Может быть int division если amount = int

-- ✅
SELECT SUM(amount)::NUMERIC / COUNT(*) FROM orders;
-- Или просто AVG(amount) — всегда корректен

3. Не учитывать возвраты

Если refund-ы вычитаются отдельной записью, AOV не отражает фактическую выручку.

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

  • AOV = revenue / orders.
  • ARPPU = revenue / paying users.

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

Визуализация AOV

import seaborn as sns
import matplotlib.pyplot as plt

sns.lineplot(data=monthly_aov, x='month', y='aov')
plt.axhline(monthly_aov['aov'].mean(), color='red', linestyle='--', label='Avg')
plt.legend()

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

  • AOV растёт — клиенты больше покупают, upsell работает.
  • AOV падает, orders растут — привлекли дешёвый сегмент.
  • AOV падает, orders падают — проблема с продуктом.

Подробнее в кейсе.

Читайте также

FAQ

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

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

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

Для метрик с тяжёлыми хвостами (чек в e-commerce премиума) — да, показывайте оба. Для LTV — обязательно.

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

Вычитать из выручки: SUM(amount - COALESCE(refund, 0)). Или считать «net AOV».

AOV в разных валютах?

Конвертируйте в одну (обычно USD/RUB) через таблицу курсов. Не смешивайте валюты в среднем.