Глубина SQL, или вместо горы кода на бэкенде…

Вы уже наверняка сталкивались с задачами, когда приходилось писать много кода на бэкенде, чтобы обработать и агрегировать данные. Но что, если я скажу вам, что правильный запрос на SQL может сделать это за нас? Сегодня я хочу показать вам задачу, которая на первый взгляд кажется запутанной, но SQL решает её легко и красиво.

Задача: «Отслеживание продаж и популярности товаров с временными ограничениями»

Представьте, что у нас есть интернет-магазин с двумя таблицами:

  1. sales — информация о продажах:
    • sale_id — уникальный идентификатор продажи.
    • product_id — идентификатор товара.
    • sale_date — дата продажи.
    • amount — сумма продажи.
  2. products — информация о товарах:
    • product_id — уникальный идентификатор товара.
    • category — категория товара.
    • price — цена товара.

Задача: Нужно составить SQL-запрос, который вернёт для каждой категории товаров следующую информацию за последние 6 месяцев:

  • Общее количество проданных товаров.
  • Суммарную сумму продаж.
  • Средний чек (среднюю сумму одной продажи).
  • Название самого продаваемого товара (по количеству проданных единиц).

Решение

Вот наш SQL-запрос:

WITH product_sales AS (
    SELECT 
        p.category,
        s.product_id,
        SUM(s.amount) AS total_sales,
        COUNT(s.sale_id) AS total_transactions,
        ROW_NUMBER() OVER (PARTITION BY p.category ORDER BY SUM(s.amount) DESC) AS sales_rank
    FROM sales s
    JOIN products p ON s.product_id = p.product_id
    WHERE s.sale_date >= NOW() - INTERVAL '6 months'
    GROUP BY p.category, s.product_id
)
SELECT 
    ps.category,
    SUM(ps.total_sales) AS total_category_sales,
    SUM(ps.total_transactions) AS total_transactions,
    ROUND(AVG(ps.total_sales::numeric), 2) AS avg_sale_per_transaction,
    (SELECT product_id 
     FROM product_sales 
     WHERE sales_rank = 1 AND category = ps.category) AS most_popular_product
FROM product_sales ps
GROUP BY ps.category;

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

  1. WITH product_sales — временная таблица, которая собирает данные по категориям и товарам. Она считает сумму продаж и количество транзакций для каждого товара. В дополнение, с помощью оконной функции ROW_NUMBER(), мы ранжируем товары внутри каждой категории по объему продаж.
  2. Основной запрос — извлекает обобщенные данные для каждой категории:
    • Суммарная сумма продаж для категории.
    • Общее количество транзакций для категории.
    • Средний чек, рассчитанный как среднее значение суммы каждой продажи.
    • Самый популярный товар (с помощью подзапроса, который находит товар с наибольшими продажами).

Почему это лучше, чем писать много кода на бэкенде?

  1. Производительность: Базы данных SQL специально оптимизированы для работы с большими объемами данных. Они эффективно агрегируют, фильтруют и ранжируют данные на уровне ядра базы. Бэкенд, написанный на любом языке программирования, не всегда сможет работать с той же скоростью. В SQL запросы и операции проходят через тщательно оптимизированные алгоритмы, что сводит к минимуму задержки.
  2. Меньше кода на бэкенде: Представьте себе, сколько кода пришлось бы писать для решения этой задачи на бэкенде! Надо было бы сначала загрузить данные, потом фильтровать, группировать, агрегировать и, наконец, ранжировать результаты. В SQL же всё это делается одним элегантным запросом.
  3. Читаемость: Правильно написанный SQL-запрос говорит сам за себя. С помощью различных операций вроде JOIN, GROUP BY, оконных функций и подзапросов можно выразить сложную логику в сжатом и ясном виде. А вот в бэкенд-коде такая логика может занимать десятки строк, что усложняет сопровождение и отладку.
  4. Масштабируемость: SQL-запросы легко адаптировать к изменениям в данных. Нужно увеличить период анализа до 12 месяцев или добавить новые условия фильтрации? Внесли одно изменение в запрос — и готово! А в коде это может повлечь за собой изменение логики работы с массивами, оптимизацию циклов, работу с коллекциями и прочие дополнительные задачи.