SQL для аналитика. 50 задач с решениями на PostgreSQL - читать онлайн бесплатно, автор Максим Чалышев, ЛитПортал
SQL для аналитика. 50 задач с решениями на PostgreSQL
Добавить В библиотеку
Оценить:

Рейтинг: 3

Поделиться
Купить и скачать

SQL для аналитика. 50 задач с решениями на PostgreSQL

Год написания книги: 2026
На страницу:
3 из 4
Настройки чтения
Размер шрифта
Высота строк
Поля

– Присоединяем заказы → товары в заказе → продукты → категории

– Отбираем только завершённые заказы

– Группируем по категории

– Считаем среднюю сумму заказа в каждой группе

– Сортируем от самой дорогой категории к дешёвой


Мы используем INNER JOIN (просто JOIN), потому что категории без заказов нас не интересуют. Если нужны и они, замените JOIN на LEFT JOIN.


Что выводит запрос: две колонки – category_name (название категории) и avg_order_value (средняя сумма заказа в рублях).

Пример вывода (фрагмент):

text

«Ноутбуки» 121945.45«Смартфоны» 77651.72«Планшеты» 69900.00

Задача 2. Средний чек с количеством заказов.

Добавим информацию о том, сколько заказов попало в расчёт.

text

SELECTc.name AS category_name,COUNT (DISTINCT o. order_id) AS orders_count,AVG(oi.price_per_unit * oi. quantity)::decimal (10,2) AS avg_order_valueFROM orders oJOIN order_items oi ON o. order_id = oi. order_idJOIN products p ON oi.product_id = p.product_idJOIN categories c ON p.category_id = c.category_idWHERE o.status = ’completed’GROUP BY c.category_id, c.nameORDER BY avg_order_value DESC;

Как это работает

Добавили COUNT (DISTINCT o. order_id), чтобы не считать один заказ несколько раз, если в нём несколько товаров из одной категории.


Типичная ошибка

Добавить в GROUP BY не все колонки из SELECT. PostgreSQL требует, чтобы все колонки, не обёрнутые в агрегатную функцию, были перечислены в GROUP BY. Поэтому мы добавили c.category_id и c.name.

Вопросы и ответы

В: А если в категории нет заказов?

О: Такая категория не попадёт в результат, потому что INNER JOIN её отсекает. Чтобы показать категории с нулевыми продажами, замените JOIN на LEFT JOIN. Тогда в avg_order_value будет NULL.


В: Что будет, если в заказе несколько товаров из одной категории?

О: Сумма по каждому товару считается отдельно, потом все складываются в группе. Получается средняя сумма заказа по категории. Всё верно.


В: Почему мы используем DISTINCT в COUNT?

О: Если в заказе несколько товаров из одной категории, без DISTINCT один заказ посчитается несколько раз. Нам важно количество уникальных заказов.


В: Как понять, что средний чек получился достоверным?

О: Посмотрите на количество заказов в категории. Если заказов мало, средний чек может быть случайным.

Что в итоге

Мы научились считать средний чек:

• Группировать данные по категориям с GROUP BY

• Считать среднее арифметическое с AVG

• Добавлять в отчёт количество заказов для оценки достоверности

Эти навыки нужны для любой аналитики продаж.

* Задание со звёздочкой

Напишите запрос, который выводит средний чек по категориям для заказов, сделанных в феврале 2024 года. Используйте фильтр по дате и группировку.

Подсказка: добавьте в WHERE условие на order_date BETWEEN «2024-02-01» AND «2024-02-29» – .

Глава 9. GROUP BY, COUNT. Повторные покупки

Описание задачи

Составим отчёт о пользователях, которые совершили больше одной покупки. Иными словами – найти тех, кто возвращается в интернет магазин.

Узнать, сколько пользователей купили дважды, трижды и так далее.

Основные SQL-конструкции

COUNT – агрегатная функция, считает количество строк в группе.

text

COUNT (*) – считает все строки, включая NULLCOUNT (column) – считает только непустые значения в колонкеCOUNT (DISTINCT column) – считает уникальные значения

HAVING – фильтрует группы после группировки (как WHERE, но для GROUP BY).

text

SELECT category, COUNT (*) AS cntFROM productsGROUP BY categoryHAVING COUNT (*)> 5;

Типичная ошибка

Использовать WHERE вместо HAVING для фильтрации по агрегатным функциям. WHERE выполняется до группировки и не видит COUNT, SUM, AVG. Если есть группировка и нужно отфильтровать сгруппированные данные – всегда используем HAVING.

SQL-запросы с объяснением

Задача 1. Количество заказов по пользователям.

Посчитаем, сколько заказов сделал каждый пользователь.

text

SELECTu.user_id,u.user_name,COUNT (o. order_id) AS orders_countFROM users uLEFT JOIN orders o ON u.user_id = o.user_idGROUP BY u.user_id, u.user_nameORDER BY orders_count DESC;

Как это работает

– LEFT JOIN – оставляем всех пользователей, даже без заказов.

– COUNT (o. order_id) – считает только непустые заказы (у пользователей без заказов будет 0).

– GROUP BY – группируем по пользователю.

– ORDER BY orders_count DESC – сортируем от частых покупателей к редким.

Пример вывода (фрагмент):

text

user_id user_name orders_count1 alex_ivanov 914 lucas_silva 85 nurlan_kz 8

Задача 2. Пользователи с повторными покупками (больше одного заказа).

Используем HAVING, чтобы отфильтровать группы.

text

SELECTu.user_id,u.user_name,COUNT (o. order_id) AS orders_countFROM users uJOIN orders o ON u.user_id = o.user_idGROUP BY u.user_id, u.user_nameHAVING COUNT (o. order_id)> 1ORDER BY orders_count DESC;

Как это работает

– INNER JOIN – берём только пользователей с заказами (те, у кого 0 заказов, не нужны).– HAVING COUNT (o. order_id)> 1 – оставляем только тех, у кого заказов больше одного.– Сортировка от самых активных.

Пример вывода (фрагмент):

textuser_id user_name orders_count1 alex_ivanov 914 lucas_silva 85 nurlan_kz 810 armen_grigoryan 8

Совет

Если нужно посчитать пользователей с повторными покупками в процентах, можно использовать CTE:

text

WITH repeat_buyers AS (SELECT user_idFROM ordersGROUP BY user_idHAVING COUNT (*)> 1)SELECTCOUNT (*) AS repeat_buyers,(SELECT COUNT (*) FROM users) AS total_users,ROUND (100.0 * COUNT (*) / (SELECT COUNT (*) FROM users), 2) AS percentFROM repeat_buyers;

Задача 3. Распределение пользователей по количеству покупок.

Сколько пользователей купили 1 раз, 2 раза, 3 раза и так далее.

text

WITH order_counts AS(SELECTu.user_id,COUNT (o. order_id) AS orders_countFROM users uLEFT JOIN orders o ON u.user_id = o.user_idGROUP BY u.user_id)SELECTorders_count,COUNT (*) AS user_countFROM order_countsGROUP BY orders_countORDER BY orders_count;

Как это работает

– CTE order_counts – считает количество заказов для каждого пользователя.

– Основной запрос группирует пользователей по числу заказов.

Пример вывода:

text

orders_count user_count5 26 47 58 39 1

Вопросы и ответы

В: Чем HAVING отличается от WHERE?

О: WHERE фильтрует строки до группировки, HAVING – после. WHERE не может использовать агрегатные функции (COUNT, SUM, AVG).


В: Почему в первом запросе LEFT JOIN, а во втором INNER JOIN?

О: В первом запросе мы хотим видеть всех пользователей (даже с 0 заказов). LEFT JOIN это позволяет. COUNT (o. order_id) считает только непустые значения, так что 0 заказов = 0.


В: Как посчитать только уникальные заказы (без отменённых)?

О: Добавьте условие в JOIN или в WHERE: AND o.status = ’completed’.

Что в итоге

Мы научились:

– Считать количество заказов на пользователя с GROUP BY и COUNT

– Отбирать группы с помощью HAVING

– Строить распределение пользователей по частоте покупок

Эти навыки нужны для анализа лояльности и удержания пользователей.

* Задание со звёздочкой

Напишите запрос, который выводит пользователей, сделавших хотя бы 3 заказа со статусом ’completed’. Используйте HAVING.

Глава 10. GROUP BY, SUM. LTV пользователя

Описание задачи

LTV (Lifetime Value) – общая сумма денег, потраченная пользователем в магазине за всё время. Это ключевая метрика для оценки рейтинга клиента.

Всегда нужно знать: кто приносит больше всего денег, чтобы фокусироваться на удержании ценных клиентов, предложить скидки, подарить купон на день рождения.

Основные SQL-конструкции

SUM – агрегатная функция, суммирует значения в группе. Работает только для числовых колонок.

text

SUM (числовое_поле)

GROUP BY – группирует строки с одинаковыми значениями (разобрано в главе 4).


COALESCE – возвращает первое не-NULL значение. Полезно для замены NULL на 0.

text

COALESCE (column, 0) – если column NULL, вернёт 0

Совет

При суммировании лучше использовать SUM (COALESCE (amount, 0)), чтобы избежать NULL в результате. Но в нашей схеме суммы не NULL.

SQL-запросы с объяснением

Задача 1. LTV каждого пользователя (все заказы).

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

text

SELECTu.user_id,u.user_name,SUM (oi. quantity * oi.price_per_unit) AS ltvFROM users uJOIN orders o ON u.user_id = o.user_idJOIN order_items oi ON o. order_id = oi. order_idWHERE o.status = ’completed’GROUP BY u.user_id, u.user_nameORDER BY ltv DESC;

Как это работает

– Присоединяем заказы и товары в заказах.

– Фильтруем только завершённые заказы.

– Группируем по пользователю.

– Суммируем стоимость всех купленных товаров.

– Сортируем от самого ценного клиента к менее ценным.

Пример вывода (фрагмент):

text

user_id user_name ltv1 alex_ivanov 5430005 nurlan_kz 3240002 li_wei 1498003 aram_sargsyan 120000

Задача 2. LTV с учётом только успешных платежей (через payments).

Иногда удобнее считать LTV по таблице платежей, а не по order_items (например, если есть возвраты).

text

SELECTu.user_id,u.user_name,SUM(p.amount) AS ltvFROM users uJOIN payments p ON u.user_id = p.user_idGROUP BY u.user_id, u.user_nameORDER BY ltv DESC;

Как это работает

– Присоединяем платежи напрямую к пользователям.

– Суммируем суммы платежей.

– Группируем по пользователю.

Типичная ошибка

Забыть GROUP BY при использовании SUM. Без группировки SUM сложит все значения по всей таблице, а не по пользователям.


Задача 3. Средний LTV по странам.

Узнаем, в какой стране пользователи в среднем тратят больше.

text

SELECTu.country,COUNT (DISTINCT u.user_id) AS user_count,SUM (oi. quantity * oi.price_per_unit) AS total_revenue,ROUND (AVG (oi. quantity * oi.price_per_unit), 2) AS avg_ltvFROM users uJOIN orders o ON u.user_id = o.user_idJOIN order_items oi ON o. order_id = oi. order_idWHERE o.status = ’completed’GROUP BY u.countryORDER BY avg_ltv DESC;

Как это работает

– Группируем по стране.

– Считаем количество уникальных пользователей в стране.

– Общую выручку по стране.

– AVG считает средний LTV по стране.

Пример вывода (фрагмент):

text

country user_count total_revenue avg_ltvАрмения 2 240000 120000.00Россия 5 500000 100000.00Казахстан 2 150000 75000.00Египет 1 50000 50000.00

Вопросы и ответы

В: Чем LTV отличается от общей выручки?

О: LTV – сумма на одного пользователя. Общая выручка – сумма по всем пользователям.


В: Как считать LTV для пользователей без заказов?

О: Они не попадут в результат при INNER JOIN. Если нужно показать и их (с LTV = 0), используйте LEFT JOIN и COALESCE.


В: Можно ли считать LTV за определённый период?

О: Да, добавьте фильтр по дате в WHERE, например o. order_date> = «2024-01-01».


В: Почему в задаче 3 используется AVG, а не SUM?

О: AVG даёт средний LTV по стране, SUM – общую выручку. В задаче нужен именно средний.

Что в итоге

Мы научились считать LTV:

– Суммировать стоимость покупок с SUM

– Группировать данные по пользователям и странам

– Считать LTV – ключевую метрику для анализа ценности клиента

* Задание со звёздочкой

Напишите запрос, который выводит топ-3 пользователей по LTV за 2024 год (используйте фильтр по order_date). Выведите user_id, user_name и ltv.

Что в итоге

Мы научились считать LTV:

– Суммировать стоимость покупок с SUM

– Группировать данные по пользователям и странам

– Считать LTV – ключевую метрику для анализа ценности клиента

Глава 11. DATE_TRUNC, SUM, кросс-таблицы. Выручка по месяцам

Описание задачи

Новый отчёт: посчитаем общую выручку по месяцам на основе завершённых заказов. А затем представим эти данные в виде кросс-таблицы, где месяцы – строки, а категории товаров – колонки.

Таблицы: orders, order_items, products, categories.

Основные SQL-конструкции

DATE_TRUNC – обрезает дату до указанной точности (год, месяц, день, час).

text

DATE_TRUNC (’month’, order_date) /* 2024-01-15 → 2024-01-01 */

SUM – агрегатная функция, суммирует значения (см. главу 10).


crosstab – функция из расширения PostgreSQL tablefunc. Превращает строки в колонки. В функцию передаётся в кавычках запрос, который нам надо перевернуть.

Для использования crosstab нужно установить расширение:

text

CREATE EXTENSION IF NOT EXISTS tablefunc;

Совет

DATE_TRUNC удобен для группировки по периодам: неделям, месяцам, кварталам, годам.

SQL-запросы с объяснением

Задача 1. Выручка по месяцам.

text

SELECTDATE_TRUNC (’month’, order_date) AS month,SUM (oi. quantity * oi.price_per_unit) AS revenueFROM orders oJOIN order_items oi ON o. order_id = oi. order_idWHERE o.status = ’completed’GROUP BY DATE_TRUNC (’month’, order_date)ORDER BY month;Как это работает– DATE_TRUNC (’month’, order_date) – превращает дату в первый день месяца.– JOIN – соединяем заказы с их товарами.– SUM – считаем общую выручку.– GROUP BY – группируем по месяцам.– ORDER BY month – сортируем по возрастанию.

Пример вывода (фрагмент):

text

month revenue2024-01-01 00:00:00+03 1571090.002024-02-01 00:00:00+03 1458330.002024-03-01 00:00:00+03 1407930.002024-04-01 00:00:00+03 1193490.00

Задача 2. Выручка по месяцам и категориям (обычный GROUP BY).

Подготовим данные для кросс-таблицы: выручка по месяцам и категориям.

text

SELECTDATE_TRUNC (’month’, o. order_date) AS month,c.name AS category_name,SUM (oi. quantity * oi.price_per_unit) AS revenueFROM orders oJOIN order_items oi ON o. order_id = oi. order_idJOIN products p ON oi.product_id = p.product_idJOIN categories c ON p.category_id = c.category_idWHERE o.status = ’completed’GROUP BY month, c.category_id, c.nameORDER BY month, category_name;

Как это работает

– Группируем по месяцу и категории.

– Считаем выручку для каждой пары (месяц, категория).

Пример вывода (фрагмент):

text

month category_name revenue2024-01-01 00:00:00+03 Женская 11970.002024-01-01 00:00:00+03 Мужская 11970.002024-01-01 00:00:00+03 Наушники 79700.002024-01-01 00:00:00+03 Ноутбуки 689600.002024-01-01 00:00:00+03 Планшеты 69900.00

Задача 3. Кросс-таблица: месяцы в строках, категории в колонках.

Превращаем результат задачи 2 в таблицу, где категории становятся колонками, используем crosstab из расширения tablefunc.

text

CREATE EXTENSION IF NOT EXISTS tablefunc;SELECT * FROM crosstab («SELECTDATE_TRUNC (»’month’», o. order_date)::date AS month,c.name AS category_name,SUM (oi. quantity * oi.price_per_unit) AS revenueFROM orders oJOIN order_items oi ON o. order_id = oi. order_idJOIN products p ON oi.product_id = p.product_idJOIN categories c ON p.category_id = c.category_idWHERE o.status = '’completed’'/*выполнено*/GROUP BY month, c.category_id, c.nameORDER BY 1,2»,«SELECT DISTINCT name FROM categories WHERE name IN (»«Ноутбуки»», ««Смартфоны»», ««Планшеты»») ORDER BY 1»)AS ct (month date, «Ноутбуки» numeric, «Смартфоны» numeric, «Планшеты» numeric);

Как это работает

– Первый запрос – данные: месяц, категория, выручка.

– Второй запрос – список категорий, которые станут колонками.

– crosstab превращает строки в колонки AS ct (month date, «Ноутбуки» numeric, «Смартфоны» numeric, «Планшеты» numeric).

– ::date – преобразуем timestamp в дату.

– Кавычки в названиях колонок нужны из-за русских букв.

– Две одинарные кавычки подряд используются для экранирования кавычек внутри строки crosstab.

результат

«2024-01-01» 689600.00 69900.00 699000.00«2024-02-01» 489500.00 209700.00 594200.00«2024-03-01» 799400.00 209700.00 354500.00«2024-04-01» 614400.00 69900.00 449400.00«2024-05-01» 89900.00 null 154800.00

Вопросы и ответы

В: Зачем нужна кросс-таблица?

О: Она компактно показывает данные: строки – месяцы, колонки – категории. Удобно для отчётов и дашбордов.


В: Почему во втором запросе crosstab указаны не все категории?

О: Чтобы колонок было не слишком много. В реальных отчётах выбирают топ-5 или топ-10 категорий.


В: Что делать, если в какой-то месяц не было продаж по категории?

О: В примере выше будет 0.00 или NULL.


В: Можно ли использовать crosstab без второго запроса?

О: Да, но тогда категории нужно перечислить вручную в определении колонок, а запрос должен возвращать данные в строгом порядке.

Что в итоге

Мы научились:

– Группировать данные по месяцам с DATE_TRUNC

– Считать выручку по месяцам и категориям

– Строить кросс-таблицы с помощью crosstab

Кросс-таблицы удобны для отчётов, где нужно сравнить показатели по разным категориям в динамике.

* Задание со звёздочкой

Напишите запрос, который строит кросс-таблицу выручки по кварталам (используйте DATE_TRUNC (’quarter’, order_date)) и категориям «Ноутбуки», «Смартфоны», «Планшеты».

Подсказка: замените ’month’ на ’quarter’ в первом запросе crosstab.

Глава 12. Аналитические функции. Топ-5 товаров

Описание задачи

Представьте, что нам нужны 5 товаров, которые принесли больше всего выручки. Это классическая задача для отчётов и дашбордов.

Таблицы: order_items, products.

Основные SQL-конструкции

Аналитические (оконные) функции – выполняют вычисления по набору строк, связанных с текущей строкой. В отличие от GROUP BY, они не сворачивают строки в одну.

ROW_NUMBER () – присваивает уникальный номер каждой строке в рамках окна (партиции).

Синтаксис:

text

ROW_NUMBER () OVER (PARTITION BY колонка ORDER BY колонка)

– PARTITION BY – делит строки на группы (если не указано, всё окно)

– ORDER BY – задаёт порядок нумерации внутри группы

Совет

ROW_NUMBER () часто используют для нумерации строк в отчётах и для отбора топ-N записей в каждой категории.

SQL-запросы с объяснением

Задача 1. Топ-5 товаров по выручке (простой способ).

text

SELECTp.product_id,p.name,SUM (oi. quantity * oi.price_per_unit) AS revenueFROM order_items oiJOIN products p ON oi.product_id = p.product_idGROUP BY p.product_id, p.nameORDER BY revenue DESCLIMIT 5;

Как это работает

– Группируем по товару.

– Считаем выручку.

– Сортируем от большего к меньшему.

– LIMIT 5 оставляет только первые 5 строк.

Пример вывода:

text

product_id name revenue4 MacBook Pro 14 2398800.005 Lenovo ThinkPad 1078800.001 iPhone 15 1038700.002 Samsung Galaxy S24 973700.0011 iPad Air 699000.00

Задача 2. Топ-5 товаров с нумерацией (ROW_NUMBER).

Добавим нумерацию строк.

text

SELECTROW_NUMBER () OVER (ORDER BY SUM (oi. quantity * oi.price_per_unit) DESC) AS rank,p.product_id,p.name,SUM (oi. quantity * oi.price_per_unit) AS revenueFROM order_items oiJOIN products p ON oi.product_id = p.product_idGROUP BY p.product_id, p.nameORDER BY revenue DESCLIMIT 5;

Как это работает

– ROW_NUMBER () OVER (ORDER BY SUM (…) DESC) – нумерует строки от 1 до N в порядке убывания выручки.

– Остальное как в задаче 1.


Задача 3. Топ-3 товара в каждой категории (ROW_NUMBER с PARTITION BY).

Более сложный пример: для каждой категории найти 3 самых продаваемых товара.

text

WITH product_revenue AS (SELECTc.name AS category_name,p.product_id,p.name AS product_name,SUM (oi. quantity * oi.price_per_unit) AS revenueFROM order_items oiJOIN products p ON oi.product_id = p.product_idJOIN categories c ON p.category_id = c.category_idGROUP BY c.category_id, c.name, p.product_id, p.name),ranked AS (SELECTcategory_name,product_name,revenue,ROW_NUMBER () OVER (PARTITION BY category_name ORDER BY revenue DESC) AS rankFROM product_revenue)SELECTcategory_name,product_name,revenue,rankFROM rankedWHERE rank <= 3ORDER BY category_name, rank;

Как это работает

– product_revenue – считаем выручку по товарам с категориями.

– ranked – нумеруем товары внутри каждой категории (PARTITION BY category_name) по убыванию выручки.

– WHERE rank <= 3 – оставляем только топ-3 в каждой категории.

Пример вывода (фрагмент):

text

category_name product_name revenue rankЖенская Сумка женская 29940.00 1Женская Платье летнее 20940.00 2Мужская Джинсы мужские 27930.00 1Мужская Футболка хлопок 5970.00 2Наушники Наушники Sony WH-1000XM5 179400.00 1Наушники Наушники AirPods Pro 174300.00 2

Вопросы и ответы

В: Чем ROW_NUMBER () отличается от RANK () и DENSE_RANK ()?

О: При одинаковых значениях ROW_NUMBER () даёт разные номера (1,2,3,4…), RANK () пропускает номера при совпадении (1,1,3,4…), DENSE_RANK () не пропускает (1,1,2,3…).


В: Зачем использовать ROW_NUMBER (), если есть LIMIT?

О: LIMIT работает только на всём результате. ROW_NUMBER () с PARTITION BY позволяет отобрать топ-N в каждой группе (категории, стране и т.д.).


В: Можно ли использовать ROW_NUMBER () без PARTITION BY?

О: Да. Тогда нумерация будет по всему результату.


В: Что будет, если несколько товаров имеют одинаковую выручку?

О: ROW_NUMBER () назначит разные номера (порядок не определён). Если нужна одинаковая позиция, используйте RANK () или DENSE_RANK ().

Что в итоге

Мы научились:

– Находить топ-5 товаров с помощью GROUP BY и LIMIT

– Добавлять нумерацию с ROW_NUMBER ()

– Отбирать топ-N товаров в каждой категории с PARTITION BY

Оконные функции открывают возможности для более сложной аналитики.

* Задание со звёздочкой

Напишите запрос, который выводит топ-2 товара по выручке в каждой категории. Используйте ROW_NUMBER () OVER (PARTITION BY …).

Подсказка: возьмите за основу задачу 3 и измените условие фильтрации rank <= 2.

Глава 13. Аналитические функции. Retention 7 дней

Описание задачи

Retention – это способность продукта возвращать пользователей. Он показывает, какая доля пользователей вернулась на следующий день, через неделю, через месяц.

Мы посчитаем retention за 7 дней: сколько пользователей, зарегистрировавшихся в определённый день, совершили хотя бы одно действие (например, покупку или вход) в каждый из следующих дней в течение 7 дней после регистрации.

Таблицы: users, users_log.

Основные SQL-конструкции

FIRST_VALUE () – оконная функция, которая возвращает значение из первой строки в рамках окна.

text

FIRST_VALUE (users) OVER (PARTITION BY reg_date ORDER BY day_number)

В нашей задаче мы используем FIRST_VALUE (users) OVER (PARTITION BY reg_date ORDER BY day_number) – берём количество пользователей в день 0 (день регистрации) для каждой когорты.

На страницу:
3 из 4