SQL для e-commerce аналитика

Проверь себя · 1/3разбор после ответа
В таблице метрик есть dt, platform, dau. Нужно вывести значение dau за предыдущий день для той же платформы, чтобы посчитать дневное изменение. Какое выражение верное?

Зачем это знать

E-commerce — один из крупнейших нанимателей аналитиков в РФ. Wildberries, Ozon, Lamoda, Avito, Яндекс Маркет — все держат большие команды аналитиков и заваливают кандидатов SQL-задачами. Специфика в том, что вопросы не абстрактные, а завязаны на доменные метрики: GMV, средний чек, брошенные корзины, повторные покупки, возвраты.

Поэтому на собесе в e-commerce мало просто знать оконные функции и джойны — надо понимать, что этими джойнами считаешь и зачем бизнесу этот показатель. Ниже — типичные таблицы и готовые запросы на самые частые метрики, которые просят написать вживую.

Ключевые таблицы

Почти в любом e-commerce схема сводится к нескольким таблицам: заказы, позиции заказов, товары, пользователи и сессии. На собесе вам обычно дают примерно такую модель и просят по ней написать запрос.

-- orders: шапка заказа
(id, user_id, created_at, status, shipping_cost, discount)

-- order_items: позиции внутри заказа
(id, order_id, product_id, quantity, price)

-- products: каталог
(id, name, category, brand, cost)

-- users: покупатели
(id, signup_at, email)

-- sessions: визиты (для конверсии и атрибуции)
(id, user_id, started_at, device, utm_source)

GMV (Gross Merchandise Value)

GMV — суммарная стоимость проданного товара за период. Это верхнеуровневая метрика оборота: сколько денег «прошло» через площадку до вычета возвратов и комиссий. Считается как сумма количество × цена по всем позициям завершённых заказов.

SELECT SUM(quantity * price) AS gmv
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
WHERE o.status = 'completed'
    AND o.created_at BETWEEN '2026-04-01' AND '2026-04-30';

По дням — чтобы построить тренд и заметить провалы:

SELECT
    DATE(o.created_at) AS day,
    SUM(oi.quantity * oi.price) AS gmv
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status = 'completed'
GROUP BY 1
ORDER BY 1;

AOV (Average Order Value)

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

SELECT
    AVG(order_total) AS aov
FROM (
    SELECT order_id, SUM(quantity * price) AS order_total
    FROM order_items
    GROUP BY order_id
) sub;

По сегментам (например, по устройству) — чтобы увидеть, где чек выше:

SELECT
    device,
    AVG(order_total) AS aov,
    COUNT(DISTINCT order_id) AS orders
FROM (
    SELECT o.id AS order_id, o.device,
           SUM(oi.quantity * oi.price) AS order_total
    FROM orders o
    JOIN order_items oi ON oi.order_id = o.id
    GROUP BY o.id, o.device
) s
GROUP BY device;

Брошенные корзины (cart abandonment)

Доля корзин, которые создали, но не довели до покупки. Метрика ловит потери в самом конце воронки: человек добавил товар, но не оплатил. Считаем через флаги событий на уровне сессии.

WITH cart_events AS (
    SELECT user_id, session_id,
           MAX(CASE WHEN event = 'cart_add' THEN 1 ELSE 0 END) AS added_to_cart,
           MAX(CASE WHEN event = 'checkout_start' THEN 1 ELSE 0 END) AS started_checkout,
           MAX(CASE WHEN event = 'purchase' THEN 1 ELSE 0 END) AS purchased
    FROM events
    WHERE created_at BETWEEN '2026-04-01' AND '2026-04-30'
    GROUP BY 1, 2
)
SELECT
    SUM(added_to_cart) AS carts_created,
    SUM(CASE WHEN added_to_cart = 1 AND purchased = 0 THEN 1 ELSE 0 END) AS abandoned,
    SUM(CASE WHEN added_to_cart = 1 AND purchased = 0 THEN 1 ELSE 0 END) * 100.0 /
        NULLIF(SUM(added_to_cart), 0) AS abandonment_rate
FROM cart_events;

Доля повторных покупок (repeat purchase rate)

Показывает, какая доля покупателей вернулась за вторым заказом. Прокси для удержания: чем выше, тем меньше вы зависите от постоянной закупки нового трафика.

WITH user_orders AS (
    SELECT user_id, COUNT(*) AS order_count
    FROM orders
    WHERE status = 'completed'
    GROUP BY user_id
)
SELECT
    COUNT(*) AS total_buyers,
    COUNT(CASE WHEN order_count >= 2 THEN 1 END) AS repeat_buyers,
    COUNT(CASE WHEN order_count >= 2 THEN 1 END) * 100.0 / COUNT(*) AS repeat_rate
FROM user_orders;

Интервал между покупками

Среднее число дней между соседними заказами одного пользователя. Помогает понять частоту покупок и когда клиент «выпадает» из нормального цикла. Берём предыдущую дату заказа через LAG и считаем разницу.

WITH ordered AS (
    SELECT
        user_id,
        created_at,
        LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_order
    FROM orders
    WHERE status = 'completed'
)
SELECT
    AVG(EXTRACT(DAY FROM (created_at - prev_order))) AS avg_days_between
FROM ordered
WHERE prev_order IS NOT NULL;

Конверсия (conversion rate)

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

WITH session_outcomes AS (
    SELECT
        s.id AS session_id,
        s.user_id,
        MAX(CASE WHEN o.id IS NOT NULL THEN 1 ELSE 0 END) AS converted
    FROM sessions s
    LEFT JOIN orders o ON o.user_id = s.user_id
        AND o.created_at BETWEEN s.started_at AND s.started_at + INTERVAL '1 hour'
    WHERE s.started_at >= CURRENT_DATE - 30
    GROUP BY 1, 2
)
SELECT
    AVG(converted) * 100 AS session_cr
FROM session_outcomes;
Прокачай SQL для собеса
500+ задач по SQL: оконные функции, JOIN, CTE — с разбором каждой
Тренировать SQL в Telegram

Метрики по товарам

Топ продаж

SELECT
    p.name,
    p.category,
    SUM(oi.quantity) AS units_sold,
    SUM(oi.quantity * oi.price) AS revenue
FROM products p
JOIN order_items oi ON oi.product_id = p.id
JOIN orders o ON o.id = oi.order_id
WHERE o.status = 'completed'
    AND o.created_at BETWEEN '2026-04-01' AND '2026-04-30'
GROUP BY p.id, p.name, p.category
ORDER BY revenue DESC
LIMIT 20;

Доля возвратов по товару

Возвраты — больная тема e-commerce: высокий возврат съедает маржу и логистику. Считаем долю возвращённых заказов по каждому товару, отсекая товары с малой выборкой через HAVING.

SELECT
    p.id, p.name,
    COUNT(DISTINCT CASE WHEN o.status = 'completed' THEN o.id END) AS orders,
    COUNT(DISTINCT CASE WHEN o.status = 'returned' THEN o.id END) AS returns,
    COUNT(DISTINCT CASE WHEN o.status = 'returned' THEN o.id END) * 100.0 /
        NULLIF(COUNT(DISTINCT o.id), 0) AS return_rate
FROM products p
JOIN order_items oi ON oi.product_id = p.id
JOIN orders o ON o.id = oi.order_id
GROUP BY p.id, p.name
HAVING COUNT(DISTINCT o.id) > 50
ORDER BY return_rate DESC;

Маржа по товару

SELECT
    p.name,
    AVG(oi.price) AS avg_selling_price,
    p.cost,
    AVG(oi.price) - p.cost AS margin_per_unit,
    (AVG(oi.price) - p.cost) / AVG(oi.price) * 100 AS margin_pct
FROM products p
JOIN order_items oi ON oi.product_id = p.id
GROUP BY p.id, p.name, p.cost;

LTV клиента

LTV (lifetime value) — сколько денег в среднем приносит один покупатель за всё время. Базовая версия — суммарная выручка на пользователя и среднее число заказов. В реальности LTV ещё учитывает маржу и горизонт, но на собесе часто хватает такой оценки.

WITH user_ltv AS (
    SELECT
        user_id,
        MIN(created_at) AS first_order,
        COUNT(*) AS total_orders,
        SUM(order_total) AS total_spent
    FROM orders_with_totals
    GROUP BY user_id
)
SELECT
    AVG(total_spent) AS ltv,
    AVG(total_orders) AS avg_orders_per_user
FROM user_ltv;

Первая покупка против повторной

Сравнение экономики первого и последующих заказов. Часто первый чек ниже (скидка новичку), а повторные — прибыльнее. Нумеруем заказы пользователя через ROW_NUMBER и делим на две группы.

WITH order_rank AS (
    SELECT
        id, user_id, order_total,
        ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_num
    FROM orders
    WHERE status = 'completed'
)
SELECT
    CASE WHEN order_num = 1 THEN 'First-time' ELSE 'Repeat' END AS customer_type,
    COUNT(*) AS orders,
    AVG(order_total) AS aov,
    SUM(order_total) AS revenue
FROM order_rank
GROUP BY 1;

Анализ по категориям

SELECT
    p.category,
    SUM(oi.quantity * oi.price) AS revenue,
    COUNT(DISTINCT o.id) AS orders,
    COUNT(DISTINCT o.user_id) AS buyers,
    AVG(oi.quantity * oi.price) AS avg_line_value
FROM products p
JOIN order_items oi ON oi.product_id = p.id
JOIN orders o ON o.id = oi.order_id
WHERE o.status = 'completed'
GROUP BY p.category
ORDER BY revenue DESC;

Типичные дашборды

Аналитик e-commerce почти всегда отвечает за набор регулярных дашбордов. Понимать, что на них выводят, полезно и для работы, и для собеса:

  • GMV, выручка и число заказов — верхнеуровневая динамика.
  • Воронка конверсии: визит → корзина → чекаут → покупка.
  • Тренд среднего чека (AOV).
  • Топ товаров и категорий по выручке.
  • Сегменты покупателей: новые против вернувшихся.
  • Атрибуция по каналам привлечения.
  • Доля возвратов.
  • Оборачиваемость запасов (продвинутый уровень).

Как это спрашивают на собесе

Помимо «напиши запрос на метрику X», в e-commerce любят продуктовые вопросы, где SQL — только часть ответа.

«Как бы вы измеряли успех e-commerce?» North Star обычно GMV или контрибуционная маржа. Дальше раскладываете на драйверы: трафик, конверсия, средний чек, доля повторных покупок. Сильный ответ — не назвать одну метрику, а показать дерево: как верхняя метрика собирается из нижних.

«Продажи упали — как будете разбираться?» Это задача на root cause analysis. Идёте по декомпозиции: где именно упало — в новых или вернувшихся клиентах? В конкретных категориях? На отдельных каналах или устройствах? Сломалась ли оплата? SQL здесь — инструмент, чтобы последовательно сузить гипотезу до конкретного сегмента.

Интервьюер смотрит не только на синтаксис, но и на то, уточняете ли вы определения (какой статус считать продажей, включать ли возвраты) и думаете ли о бизнес-смысле метрики.

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

FAQ

Чем Gross GMV отличается от Net GMV?

Gross GMV — оборот до вычета возвратов и отмен, Net GMV — после. Разница бывает большой в категориях с высоким возвратом (одежда, обувь). На собесе всегда уточняйте, какое определение имеет в виду интервьюер, прежде чем писать запрос.

AOV или ARPU — что использовать?

AOV считается на один заказ, ARPU — на одного пользователя за период. Они связаны через частоту покупок: ARPU ≈ AOV × число заказов на пользователя. Для оценки чека берут AOV, для оценки ценности клиента — ARPU или LTV.

Почему AOV нельзя считать простым AVG по order_items?

Потому что одна строка order_items — это одна позиция, а не заказ. Прямой AVG(quantity * price) усреднит по позициям и занизит чек. Сначала надо свернуть позиции в сумму заказа через GROUP BY order_id, и только потом усреднять.

Чем юнит-экономика e-commerce сложнее, чем в SaaS?

В физическом e-commerce на маржу влияют доставка, себестоимость товара, возвраты и хранение — всё это надо вычитать из выручки. В SaaS предельная себестоимость почти нулевая, поэтому юнит-экономика проще. На собесе в маркетплейсе ждут, что вы упомянете возвраты и логистику как часть contribution margin.

Как считать конверсию — по сессиям или по пользователям?

Зависит от вопроса. Session CR (доля сессий с покупкой) показывает эффективность конкретных визитов и полезна для оценки UX и трафика. User CR (доля пользователей, купивших за период) ближе к бизнес-результату. На собесе стоит проговорить, какую именно конверсию считаете и почему.


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