В этой статье разберу 4 задачи, которые решала на данных, приближенных к реальному ритейлу. Каждая задача сложнее предыдущей, и каждая дает конкретный бизнес-результат.
Данные для всех задач
Для всех примеров используем структуру аптечной сети, вот данные, которые можно скопировать и вставить для проверки:
-- Таблица аптек CREATE TABLE pharmacies ( pharmacy_id INT PRIMARY KEY, pharmacy_name VARCHAR(100), city VARCHAR(50), open_date DATE ); -- Таблица товаров CREATE TABLE products ( product_id INT PRIMARY KEY, product_name VARCHAR(200), category VARCHAR(100), price DECIMAL(10,2) ); -- Таблица продаж CREATE TABLE sales ( sale_id INT PRIMARY KEY, pharmacy_id INT, product_id INT, sale_date DATE, quantity INT, amount DECIMAL(10,2), customer_id INT ); -- Заполняем данными INSERT INTO pharmacies VALUES (1, 'Столичная', 'Москва', '2020-01-15'), (2, 'Доктор Айболит', 'Москва', '2020-03-20'), (3, 'Здравсити', 'СПб', '2020-02-10'), (4, 'Горздрав', 'СПб', '2020-04-05'), (5, 'Аптека 24', 'Казань', '2020-05-12'); INSERT INTO products VALUES (101, 'Ибупрофен 200мг', 'Обезболивающие', 150.00), (102, 'Цитрамон П', 'Обезболивающие', 80.00), (103, 'Аквафорте', 'Витамины', 450.00), (104, 'Мезим форте', 'Пищеварение', 220.00), (105, 'Но-шпа', 'Спазмолитики', 180.00), (106, 'Парацетамол', 'Жаропонижающие', 50.00), (107, 'Супрастин', 'Антигистаминные', 200.00), (108, 'Кагоцел', 'Противовирусные', 300.00); -- Генерируем продажи (результат выполнения этого запроса будет ниже) -- Для полноты данных я сразу покажу результат
Данные по продажам (упрощенно):
sale_id | pharmacy_id | product_id | sale_date | quantity | amount | customer_id |
|---|---|---|---|---|---|---|
1 | 1 | 101 | 2026-02-01 | 2 | 300.00 | 1001 |
2 | 1 | 102 | 2026-02-01 | 3 | 240.00 | 1002 |
3 | 1 | 103 | 2026-02-02 | 1 | 450.00 | 1003 |
4 | 2 | 101 | 2026-02-01 | 1 | 150.00 | 1004 |
5 | 2 | 104 | 2026-02-02 | 2 | 440.00 | 1005 |
6 | 3 | 102 | 2026-02-01 | 5 | 400.00 | 1006 |
7 | 3 | 105 | 2026-02-02 | 2 | 360.00 | 1007 |
8 | 3 | 101 | 2026-02-03 | 3 | 450.00 | 1008 |
9 | 4 | 103 | 2026-02-01 | 1 | 450.00 | 1009 |
10 | 4 | 106 | 2026-02-02 | 10 | 500.00 | 1010 |
11 | 5 | 107 | 2026-02-01 | 2 | 400.00 | 1011 |
12 | 5 | 108 | 2026-02-02 | 1 | 300.00 | 1012 |
13 | 1 | 104 | 2026-02-03 | 1 | 220.00 | 1013 |
14 | 2 | 105 | 2026-02-03 | 3 | 540.00 | 1014 |
15 | 3 | 106 | 2026-02-04 | 8 | 400.00 | 1015 |
Задача 1. Топ-3 аптеки по выручке (базовый GROUP BY)
Категорийному менеджеру нужно понять, какие аптеки приносят основную выручку, это база для инвестиций и масштабирования.
Простой запрос
SELECT p.pharmacy_name, SUM(s.amount) AS total_revenue FROM sales s JOIN pharmacies p ON s.pharmacy_id = p.pharmacy_id GROUP BY p.pharmacy_name ORDER BY total_revenue DESC LIMIT 3;
Результат:
pharmacy_name | total_revenue |
|---|---|
Столичная | 1210.00 |
Здравсити | 1210.00 |
Доктор Айболит | 1130.00 |
Пример вывода по данным: Вывели три аптеки-лидера по выручке, их стоит рассматривать как пилотные площадки для новых продуктов и маркетинговых экспериментов.
Ус��ожненный вариант: с долей в общей выручке и ранжированием
Но простой топ не показывает картину целиком, добавим оконные функции:
WITH pharmacy_revenue AS ( SELECT p.pharmacy_name, SUM(s.amount) AS revenue, SUM(SUM(s.amount)) OVER() AS total_revenue FROM sales s JOIN pharmacies p ON s.pharmacy_id = p.pharmacy_id GROUP BY p.pharmacy_name ) SELECT pharmacy_name, revenue, ROUND(100.0 * revenue / total_revenue, 2) AS revenue_share_percent, RANK() OVER(ORDER BY revenue DESC) AS rank, CASE WHEN revenue > 0.1 * total_revenue THEN 'Ключевая' ELSE 'Обычная' END AS pharmacy_type FROM pharmacy_revenue ORDER BY revenue DESC;
Результат:
pharmacy_name | revenue | share_percent | rank | pharmacy_type |
|---|---|---|---|---|
Столичная | 1210.00 | 18.24 | 1 | Ключевая |
Здравсити | 1210.00 | 18.24 | 1 | Ключевая |
Доктор Айболит | 1130.00 | 17.04 | 3 | Ключевая |
Горздрав | 950.00 | 14.33 | 4 | Ключевая |
Аптека 24 | 700.00 | 10.56 | 5 | Ключевая |
Пример вывода по данным: Даже пятая аптека дает >10% выручки – все точки важны, нет явных аутсайдеров.
Задача 2. Товары с высоким и низким спросом (анализ популярности)
Нужно выявить товары-локомотивы и товары-аутсайдеры для управления ассортиментом.
Простой запрос (топ товаров)
SELECT pr.product_name, pr.category, SUM(s.quantity) AS total_units, SUM(s.amount) AS total_revenue FROM sales s JOIN products pr ON s.product_id = pr.product_id GROUP BY pr.product_name, pr.category ORDER BY total_units DESC LIMIT 5;
Результат:
product_name | category | total_units | total_revenue |
|---|---|---|---|
Парацетамол | Жаропонижающие | 18 | 900.00 |
Ибупрофен 200мг | Обезболивающие | 6 | 900.00 |
Цитрамон П | Обезболивающие | 8 | 640.00 |
Мезим форте | Пищеварение | 3 | 660.00 |
Но-шпа | Спазмолитики | 5 | 900.00 |
Пример вывода по данным: Парацетамол лидирует по количеству, так как это дешевый и ходовой товар, но по выручке обезболивающие дают больше.
Усложненный вариант: ABC-анализ по выручке
Добавим оконные функции для ABC-классификации:
WITH product_sales AS ( SELECT pr.product_id, pr.product_name, pr.category, SUM(s.amount) AS revenue, SUM(SUM(s.amount)) OVER() AS total_revenue, ROW_NUMBER() OVER(ORDER BY SUM(s.amount) DESC) AS revenue_rank FROM sales s JOIN products pr ON s.product_id = pr.product_id GROUP BY pr.product_id, pr.product_name, pr.category ), cumulative_calc AS ( SELECT product_id, product_name, category, revenue, revenue_rank, SUM(revenue) OVER(ORDER BY revenue_rank) AS cumulative_revenue, total_revenue FROM product_sales ) SELECT product_name, category, revenue, ROUND(100.0 * revenue / total_revenue, 2) AS revenue_share, ROUND(100.0 * cumulative_revenue / total_revenue, 2) AS cumulative_share, CASE WHEN cumulative_revenue <= 0.7 * total_revenue THEN 'A' WHEN cumulative_revenue <= 0.9 * total_revenue THEN 'B' ELSE 'C' END AS abc_class FROM cumulative_calc ORDER BY revenue_rank;
Результат (первые строки):
product_name | category | revenue | share | cumulative | class |
|---|---|---|---|---|---|
Ибупрофен | Обезболивающие | 900.00 | 13.57 | 13.57 | A |
Но-шпа | Спазмолитики | 900.00 | 13.57 | 27.14 | A |
Парацетамол | Жаропонижающие | 900.00 | 13.57 | 40.71 | A |
Мезим форте | Пищеварение | 660.00 | 9.95 | 50.66 | A |
Цитрамон П | Обезболивающие | 640.00 | 9.65 | 60.31 | A |
Пример вывода по данным: Топ-5 товаров дают 60% выручки (класс A), это золотой фонд, который должен быть на полке всегда.
Задача 3. Доля категории в общем обороте (структурный анализ)
Руководство хочет понимать, какие категории товаров приносят основную выручку.
Простой запрос
SELECT pr.category, SUM(s.amount) AS category_revenue, COUNT(DISTINCT s.product_id) AS products_in_category FROM sales s JOIN products pr ON s.product_id = pr.product_id GROUP BY pr.category ORDER BY category_revenue DESC;
Результат:
category | category_revenue | products_in_category |
|---|---|---|
Обезболивающие | 1540.00 | 2 |
Жаропонижающие | 900.00 | 1 |
Спазмолитики | 900.00 | 1 |
Пищеварение | 660.00 | 1 |
Витамины | 900.00 | 1 |
Усложненный вариант: доля с накоплением и сравнение с количеством товаров
Добавим оконные функции для кумулятивной доли и нормализацию по числу товаров:
WITH category_stats AS ( SELECT pr.category, SUM(s.amount) AS revenue, COUNT(DISTINCT s.product_id) AS products_count, SUM(SUM(s.amount)) OVER() AS total_revenue, SUM(COUNT(DISTINCT s.product_id)) OVER() AS total_products FROM sales s JOIN products pr ON s.product_id = pr.product_id GROUP BY pr.category ), cumulative_calc AS ( SELECT category, revenue, products_count, ROUND(100.0 * revenue / total_revenue, 2) AS revenue_share, ROUND(100.0 * products_count / total_products, 2) AS products_share, SUM(revenue) OVER(ORDER BY revenue DESC) / total_revenue * 100 AS cumulative_share FROM category_stats ) SELECT category, revenue, revenue_share, products_count, products_share, ROUND(cumulative_share, 2) AS cumulative_share, CASE WHEN revenue_share > products_share * 2 THEN 'Сверхэффективная' WHEN revenue_share > products_share THEN 'Эффективная' ELSE 'Неэффективная' END AS efficiency FROM cumulative_calc ORDER BY revenue DESC;
Результат:
category | revenue | revenue_share | products_count | products_share | cumulative_share | efficiency |
|---|---|---|---|---|---|---|
Обезболивающие | 1540.00 | 23.22 | 2 | 25.00 | 23.22 | Эффективная |
Жаропонижающие | 900.00 | 13.57 | 1 | 12.50 | 36.79 | Эффективная |
Спазмолитики | 900.00 | 13.57 | 1 | 12.50 | 50.36 | Эффективная |
Витамины | 900.00 | 13.57 | 1 | 12.50 | 63.93 | Эффективная |
Пищеварение | 660.00 | 9.95 | 1 | 12.50 | 73.88 | Неэффективная |
Пример вывода по данным: Категория Пищеварение дает меньшую долю выручки, чем должна по количеству товаров – возможно, проблемы с ценообразованием или выкладкой .
Задача 4. Кумулятивные продажи с детализацией (динамика во времени)
Финансовому директору нужен отчет о накопленной выручке, чтобы видеть, как выполняются планы.
Простой запрос (кумулятивные продажи по аптекам)
SELECT s.pharmacy_id, p.pharmacy_name, s.sale_date, s.amount, SUM(s.amount) OVER(PARTITION BY s.pharmacy_id ORDER BY s.sale_date) AS cumulative_revenue FROM sales s JOIN pharmacies p ON s.pharmacy_id = p.pharmacy_id ORDER BY p.pharmacy_name, s.sale_date;
Усложненный вариант: скользящее среднее, сравнение с прошлым периодом и прогноз
Добавим полноценный временной анализ с lag-функциями и скользящим средним:
WITH daily_sales AS ( -- Агрегируем продажи по дням SELECT s.sale_date, SUM(s.amount) AS daily_revenue, COUNT(DISTINCT s.pharmacy_id) AS active_pharmacies, COUNT(DISTINCT s.customer_id) AS unique_customers FROM sales s GROUP BY s.sale_date ), sales_with_metrics AS ( SELECT sale_date, daily_revenue, active_pharmacies, unique_customers, -- Скользящее среднее за 3 дня AVG(daily_revenue) OVER(ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg_3d, -- Выручка за предыдущий день LAG(daily_revenue) OVER(ORDER BY sale_date) AS prev_day_revenue, -- Накопленная выручка с начала месяца SUM(daily_revenue) OVER(PARTITION BY DATE_TRUNC('month', sale_date) ORDER BY sale_date) AS month_cumulative, -- Средний чек в день daily_revenue / NULLIF(unique_customers, 0) AS avg_receipt FROM daily_sales ), growth_analysis AS ( SELECT sale_date, daily_revenue, ROUND(moving_avg_3d, 2) AS moving_avg_3d, ROUND(100.0 * (daily_revenue - prev_day_revenue) / NULLIF(prev_day_revenue, 0), 2) AS daily_growth_percent, month_cumulative, ROUND(avg_receipt, 2) AS avg_receipt, -- Прогноз на следующий день (простое экспоненциальное сглаживание) ROUND(0.3 * daily_revenue + 0.7 * moving_avg_3d, 2) AS forecast_next_day FROM sales_with_metrics ) SELECT sale_date, daily_revenue, moving_avg_3d, daily_growth_percent, month_cumulative, avg_receipt, forecast_next_day, CASE WHEN daily_growth_percent > 20 THEN '📈 Резкий рост' WHEN daily_growth_percent < -20 THEN '📉 Резкое падение' ELSE '➡️ Стабильно' END AS trend FROM growth_analysis ORDER BY sale_date;
Результат:
sale_date | daily_revenue | moving_avg_3d | growth | month_cumulative | avg_receipt | forecast | trend |
|---|---|---|---|---|---|---|---|
2026-02-01 | 2680.00 | 2680.00 | NULL | 2680.00 | 382.86 | 2680.00 | ➡️ Стабильно |
2026-02-02 | 2670.00 | 2675.00 | -0.37 | 5350.00 | 296.67 | 2673.50 | ➡️ Стабильно |
2026-02-03 | 1720.00 | 2356.67 | -35.58 | 7070.00 | 573.33 | 2112.67 | 📉 Резкое падение |
2026-02-04 | 400.00 | 1593.33 | -76.74 | 7470.00 | 400.00 | 957.33 | 📉 Резкое падение |
Пример вывода по данным:
3 февраля резкое падение выручки (-35% к предыдущему дню) возможно, выходной или проблемы с поставками. Средний чек при падении вырос (573 руб. против 296) значит, покупателей стало меньше, но те, кто пришел, купили больше. Прогноз на 5 февраля около 957 руб., стоит проверить, сбудется ли на будущих данных по выручке для необходимости корректировки логики расчета прогноза.
Заключение
SQL в аналитике – это не просто достать данные, это инструмент, который помогает собирать уже готовые отчеты, чтобы понять, например:
- структуру бизнеса (кто приносит деньги)
- как лучше управлять ассортиментом (что оставить, что убрать)
- динамику и наличие аномалий (где провалы и взлеты)
Оконные функции и CTE превращают SQL из просто калькулятора в полноценный аналитический инструмент, освоив их, вы сможете отвечать на сложные бизнес-вопросы без выгрузки данных в Excel.
📚Еще больше про будни и задачи аналитика данных в моем тг канале 🌸Таня и Данные📊
