Как вы строите SQL-запрос с вложенными JOIN, фильтрацией и группировкой?
Построение сложного SQL-запроса с вложенными JOIN, фильтрацией (WHERE), группировкой (GROUP BY) и агрегатными функциями — это типичная задача при аналитике или разработке отчетов на больших, взаимосвязанных данных. Ниже подробно объясню, как подходить к таким запросам, на примере и с описанием каждого этапа.
Исходные таблицы для примера
Предположим, у нас есть следующая структура данных:
- **users
**
id | name | country
-------------------------
1 | Alice | USA
2 | Bob | Canada
3 | Charlie | USA
- **orders
**
id | user_id | order_date | total_amount
----------------------------------------
1 | 1 | 2023-01-01 | 100
2 | 1 | 2023-01-03 | 200
3 | 2 | 2023-01-02 | 150
4 | 3 | 2023-01-04 | 300
- **order_items
**
id | order_id | product_id | quantity | price_per_unit
-------------------------------------------------------
1 | 1 | 101 | 2 | 25
2 | 1 | 102 | 1 | 50
3 | 2 | 103 | 3 | 40
4 | 3 | 104 | 1 | 150
- **products
**
id | name | category
-----------------------------
101 | Laptop | Electronics
102 | Mouse | Electronics
103 | T-Shirt | Apparel
104 | Headphones | Electronics
Цель запроса
Получить агрегированную информацию:
-
по каждой стране,
-
только по заказам с общей суммой более 100,
-
в разбивке по категориям товаров,
-
с суммой проданных товаров (total_sales),
-
количеством проданных единиц (total_quantity).
Построение SQL-запроса поэтапно
Шаг 1: Соединяем таблицы
Для этого используем несколько JOIN, чтобы получить все необходимые поля:
SELECT
u.country,
p.category,
SUM(oi.quantity \* oi.price_per_unit) AS total_sales,
SUM(oi.quantity) AS total_quantity
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
Шаг 2: Добавляем фильтрацию
Нам нужны только те заказы, где общая сумма заказа > 100. Это значит, мы не можем просто использовать o.total_amount, потому что его может не быть (в реальных БД total_amount может быть вычисляемым).
Мы можем:
-
либо использовать подзапрос (CTE) для фильтрации по total,
-
либо фильтровать постфактум.
В данном случае — воспользуемся подзапросом с WHERE и EXISTS.
Вариант 1: Через подзапрос-CTE для фильтрации заказов
WITH valid_orders AS (
SELECT id
FROM orders
WHERE total_amount > 100
)
SELECT
u.country,
p.category,
SUM(oi.quantity \* oi.price_per_unit) AS total_sales,
SUM(oi.quantity) AS total_quantity
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN valid_orders vo ON o.id = vo.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
GROUP BY u.country, p.category
ORDER BY u.country, total_sales DESC;
Вариант 2: Через EXISTS (если у orders нет total_amount, нужно вычислить на лету)
Допустим, total_amount не хранится в таблице, а считается как сумма price * quantity. Тогда:
WITH order_totals AS (
SELECT
o.id AS order_id,
SUM(oi.quantity \* oi.price_per_unit) AS total_amount
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
GROUP BY o.id
),
valid_orders AS (
SELECT order_id
FROM order_totals
WHERE total_amount > 100
)
SELECT
u.country,
p.category,
SUM(oi.quantity \* oi.price_per_unit) AS total_sales,
SUM(oi.quantity) AS total_quantity
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN valid_orders vo ON o.id = vo.order_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
GROUP BY u.country, p.category
ORDER BY u.country, total_sales DESC;
Пояснение по используемым компонентам
-
JOIN:
-
Соединяет таблицы users, orders, order_items, products.
-
Благодаря JOIN, мы получаем всю цепочку от клиента до категории продукта.
-
-
WITH (CTE):
-
Удобен для разбиения сложной логики.
-
В первом CTE вычисляется сумма заказа.
-
Во втором CTE — фильтруются только те заказы, где сумма > 100.
-
-
WHERE:
- Применяется к отфильтрованным заказам через CTE или EXISTS.
-
GROUP BY:
- Используется по country и category, т.е. итоговые агрегаты на уровне "страна-продуктовая категория".
-
Агрегатные функции SUM():
-
SUM(quantity * price) — выручка по товарам.
-
SUM(quantity) — количество проданных единиц.
-
-
ORDER BY:
- Сортировка по стране и убыванию выручки.
Альтернативные подходы
Если нужно ещё более сложное поведение (например, фильтрация внутри агрегатов), можно использовать HAVING:
GROUP BY u.country, p.category
HAVING SUM(oi.quantity \* oi.price_per_unit) > 100
Но HAVING применяется после агрегации, а в нашей задаче фильтрация применяется к заказу, а не к группе.
Частые ошибки
-
Забывают фильтрацию до JOIN, и потом агрегаты искажаются.
-
Не учитывают дубли при JOIN, особенно при соединении 1:N.
-
Группируют не по всем неагрегированным полям, что вызывает ошибки в некоторых СУБД (например, PostgreSQL требует, чтобы все SELECT-поля были в GROUP BY, если не агрегат).
Такой подход позволяет построить производительный, читаемый и расширяемый SQL-запрос, пригодный для реального аналитического дашборда или репортинга.