SQL-запросы для retail-аналитика
Специфика retail-аналитики
Retail — это не только e-commerce. В России огромный сегмент офлайн-ретейла: X5, Магнит, Лента, Ашан, Детский мир, Спортмастер и ещё сотни сетей. Плюс квик-коммерс и доставка еды — Самокат, Яндекс Еда, Delivery Club. Каждая из этих компаний имеет команду аналитиков, а задачи отличаются от классической продуктовой аналитики.
Главное отличие — в retail много физических ограничений. У вас есть магазины с конкретными площадями и stockom, склады с capacity, товары с shelf life, цепочки поставок с задержками. SQL для retail часто работает с более сложными схемами, чем в e-commerce.
Продажи по магазинам и категориям
Стандартный еженедельный отчёт в ретейле — продажи по магазинам в разрезе категорий.
SELECT
s.store_id,
s.store_name,
s.city,
c.category,
SUM(sl.revenue) AS revenue,
COUNT(DISTINCT sl.transaction_id) AS transactions,
SUM(sl.revenue) / NULLIF(COUNT(DISTINCT sl.transaction_id), 0) AS avg_ticket
FROM sales_lines sl
JOIN stores s USING (store_id)
JOIN products p USING (product_id)
JOIN categories c USING (category_id)
WHERE sl.sale_date >= '2026-04-01'
AND sl.sale_date < '2026-05-01'
GROUP BY s.store_id, s.store_name, s.city, c.category
ORDER BY revenue DESC;На этом же запросе можно построить тепловую карту: строки — магазины, столбцы — категории, значение — revenue. Это даёт командам понимание, где какая категория хорошо продаётся, а где — нет.
Like-for-like sales (LFL)
Важнейшая метрика для сетей — сравнение магазинов, открытых минимум год назад. Новые магазины тянут суммарную выручку вверх, но LFL показывает органическую динамику.
WITH store_lfl AS (
SELECT store_id FROM stores
WHERE opened_at <= '2025-04-01' -- минимум 12 месяцев работы
),
periods AS (
SELECT
store_id,
SUM(revenue) FILTER (
WHERE sale_date BETWEEN '2025-04-01' AND '2025-04-30'
) AS revenue_py,
SUM(revenue) FILTER (
WHERE sale_date BETWEEN '2026-04-01' AND '2026-04-30'
) AS revenue_cy
FROM sales
WHERE store_id IN (SELECT store_id FROM store_lfl)
GROUP BY store_id
)
SELECT
SUM(revenue_cy) AS total_cy,
SUM(revenue_py) AS total_py,
ROUND((SUM(revenue_cy) - SUM(revenue_py)) * 100.0 / SUM(revenue_py), 2) AS lfl_growth_pct
FROM periods;LFL рост в ретейле меньше 3% считается слабым, 10%+ — отличным.
Тренироваться на таких вопросах можно в Telegram-боте Карьерник — там 1500+ задач с реальных собесов с разборами.
Оборачиваемость запасов (inventory turnover)
Показывает, как быстро продаётся товар из магазинов. Низкая оборачиваемость означает замороженный капитал.
WITH sales_by_product AS (
SELECT
product_id,
SUM(units_sold) AS units_sold_per_month
FROM sales
WHERE sale_date >= CURRENT_DATE - INTERVAL '30 day'
GROUP BY product_id
),
current_stock AS (
SELECT
product_id,
SUM(units_on_hand) AS stock
FROM inventory
WHERE snapshot_date = CURRENT_DATE
GROUP BY product_id
)
SELECT
p.name,
s.stock,
sb.units_sold_per_month,
s.stock * 30.0 / NULLIF(sb.units_sold_per_month, 0) AS days_of_supply
FROM products p
JOIN current_stock s USING (product_id)
LEFT JOIN sales_by_product sb USING (product_id)
WHERE s.stock > 0
ORDER BY days_of_supply DESC;Товары с days_of_supply > 90 дней — кандидаты на скидку или снятие с продажи. Товары с days < 7 — на urgent-заказ у поставщика.
Out-of-stock analysis
Ситуация, когда товар заявлен, но физически отсутствует. Теряет продажи.
WITH daily_stock AS (
SELECT
store_id,
product_id,
snapshot_date,
units_on_hand
FROM inventory
WHERE snapshot_date >= '2026-04-01'
)
SELECT
s.store_name,
c.category,
COUNT(*) FILTER (WHERE ds.units_on_hand = 0) AS oos_days,
COUNT(*) AS total_days,
COUNT(*) FILTER (WHERE ds.units_on_hand = 0) * 100.0 / COUNT(*) AS oos_rate_pct
FROM daily_stock ds
JOIN stores s USING (store_id)
JOIN products p USING (product_id)
JOIN categories c USING (category_id)
GROUP BY s.store_name, c.category
HAVING COUNT(*) FILTER (WHERE ds.units_on_hand = 0) * 100.0 / COUNT(*) > 5
ORDER BY oos_rate_pct DESC;Out-of-stock rate выше 5% — уже проблема. Магазины и категории с высокой OOS требуют внимания operations.
Вклад категорий (category mix)
Какие категории вносят какую долю в общую выручку.
WITH cat_revenue AS (
SELECT
c.category,
SUM(sl.revenue) AS revenue
FROM sales_lines sl
JOIN products p USING (product_id)
JOIN categories c USING (category_id)
WHERE sl.sale_date >= CURRENT_DATE - INTERVAL '30 day'
GROUP BY c.category
)
SELECT
category,
revenue,
ROUND(revenue * 100.0 / SUM(revenue) OVER (), 2) AS share_pct,
ROUND(revenue * 100.0 /
LAG(SUM(revenue)) OVER (ORDER BY revenue DESC), 2) AS vs_prev_category_pct
FROM cat_revenue
ORDER BY revenue DESC;В продуктовых супермаркетах топ-5 категорий обычно дают 60–80% выручки. Если одна категория выше 40%, это концентрация риска.
ABC-анализ товаров
Классификация по вкладу в выручку.
WITH product_revenue AS (
SELECT
product_id,
SUM(revenue) AS revenue
FROM sales_lines
WHERE sale_date >= CURRENT_DATE - INTERVAL '90 day'
GROUP BY product_id
),
with_share AS (
SELECT
product_id,
revenue,
SUM(revenue) OVER (ORDER BY revenue DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumulative_revenue,
SUM(revenue) OVER () AS total_revenue
FROM product_revenue
)
SELECT
product_id,
revenue,
ROUND(cumulative_revenue * 100.0 / total_revenue, 2) AS cumulative_pct,
CASE
WHEN cumulative_revenue * 100.0 / total_revenue <= 80 THEN 'A'
WHEN cumulative_revenue * 100.0 / total_revenue <= 95 THEN 'B'
ELSE 'C'
END AS abc_class
FROM with_share
ORDER BY revenue DESC;Правило Парето работает: обычно 20% товаров дают 80% выручки. Это группа A — фокус внимания. C — кандидаты на снятие с полок.
К слову, набить руку на таких кейсах удобно через тренажёр в Telegram — разбирайте по 10 вопросов в день, через 2 недели тема становится рефлексом.
Средний чек по времени дня
В ресторанах и магазинах важно видеть пиковые часы.
SELECT
EXTRACT(DOW FROM transaction_at) AS day_of_week,
EXTRACT(HOUR FROM transaction_at) AS hour,
COUNT(*) AS transactions,
AVG(total_amount) AS avg_check,
SUM(total_amount) AS total_revenue
FROM transactions
WHERE transaction_at >= CURRENT_DATE - INTERVAL '30 day'
GROUP BY 1, 2
ORDER BY 1, 2;Этот отчёт помогает планировать смены персонала и маркетинговые кампании на непиковые часы.
Сравнение магазинов (benchmarking)
Позволяет видеть outliers — магазины, сильно отличающиеся от норм.
WITH store_metrics AS (
SELECT
s.store_id,
s.store_name,
s.format, -- 'small', 'medium', 'large'
s.sq_meters,
SUM(sl.revenue) AS revenue,
SUM(sl.revenue) / s.sq_meters AS revenue_per_sq_meter,
COUNT(DISTINCT sl.transaction_id) AS transactions
FROM sales_lines sl
JOIN stores s USING (store_id)
WHERE sale_date >= CURRENT_DATE - INTERVAL '30 day'
GROUP BY s.store_id, s.store_name, s.format, s.sq_meters
)
SELECT *,
AVG(revenue_per_sq_meter) OVER (PARTITION BY format) AS format_avg,
revenue_per_sq_meter - AVG(revenue_per_sq_meter) OVER (PARTITION BY format) AS diff_from_format_avg
FROM store_metrics
ORDER BY revenue_per_sq_meter DESC;Revenue per square meter — ключевая метрика эффективности магазина. Если магазин сильно ниже среднего по формату — investigating причины (плохое расположение, персонал, ассортимент).
Читайте также
FAQ
Офлайн-ретейл или e-commerce?
Разные области, хотя сливаются в omnichannel. Офлайн имеет больше operations-задач (склады, магазины, логистика).
Сложнее ли SQL в retail?
Часто да. Схемы более сложные, много source tables.
Какие компании нанимают?
X5, Магнит, Лента, Ашан, Детский мир, Леруа Мерлен, Спортмастер, Ozon, Wildberries, Самокат.
Работа с Excel в retail?
Да, очень много. Многие бизнес-процессы идут через Excel: планы, бюджеты, отчёты руководству.