Какие знаешь соединения в таблицах SQL?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
SQL JOINs: типы соединений таблиц
SQL JOINs — один из самых важных навыков для Product Analyst-а. Это основа анализа: мы постоянно соединяем таблицы пользователей с таблицами событий, подписок с платежами и так далее. Рассмотрю все типы JOIN-ов, с примерами из реальной аналитики.
1. INNER JOIN (пересечение)
Что это: Возвращает только те строки, которые есть в ОБЕИХ таблицах.
SELECT
u.user_id,
u.name,
o.order_id,
o.amount
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
WHERE u.created_at > '2024-01-01';
Визуально:
Таблица Users Таблица Orders
user_id | name user_id | order_id
1 | Alice 1 | 101
2 | Bob 1 | 102
3 | Carol 2 | 103
4 | David (3 и 4 нет в orders)
Результат INNER JOIN:
user_id | name | order_id
1 | Alice | 101
1 | Alice | 102
2 | Bob | 103
(Кэрол и Дэвид исчезли — у них нет заказов)
Когда использую:
- Анализ покупающих пользователей (не включаю тех, кто не купил)
- Анализ активности: пользователи, которые совершили событие
- Считаю метрику: какой % пользователей имеют заказы
Пример из работы:
-- Средняя стоимость заказа для пользователей из США
SELECT
COUNT(DISTINCT u.user_id) as users_count,
AVG(o.amount) as avg_order_value
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
WHERE u.country = 'USA';
-- Результат: только US пользователи с заказами
2. LEFT JOIN (левое соединение)
Что это: Возвращает ВСЕ строки из левой таблицы, плюс совпадающие строки из правой. Если совпадения нет — NULL.
SELECT
u.user_id,
u.name,
COUNT(o.order_id) as order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.name
ORDER BY order_count DESC;
Визуально:
Таблица Users (LEFT) Таблица Orders
user_id | name user_id | order_id
1 | Alice 1 | 101
2 | Bob 1 | 102
3 | Carol 2 | 103
4 | David (нет заказов)
Результат LEFT JOIN:
user_id | name | order_id
1 | Alice | 101
1 | Alice | 102
2 | Bob | 103
3 | Carol | NULL ← Кэрол осталась, но без заказа
4 | David | NULL ← Дэвид осталась, но без заказа
Когда использую:
- Анализ включая неактивных пользователей
- Расчет % пользователей, совершивших действие
- Распределение: сколько пользователей с 0, 1, 2+ действиями
Пример из работы:
-- Процент пользователей, совершивших покупку
SELECT
COUNT(DISTINCT u.user_id) as total_users,
COUNT(DISTINCT o.user_id) as users_with_orders,
ROUND(100.0 * COUNT(DISTINCT o.user_id) / COUNT(DISTINCT u.user_id), 2) as conversion_pct
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE u.created_at > '2024-01-01';
-- Результат: всех пользователей, затем считаем, сколько с заказами
Важный момент: COUNT при LEFT JOIN
-- НЕПРАВИЛЬНО (считает NULLs как количество)
SELECT
u.user_id,
COUNT(*) as order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id;
-- ПРАВИЛЬНО (считает только реальные заказы)
SELECT
u.user_id,
COUNT(o.order_id) as order_count ← COUNT(NULL) = 0
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id;
3. RIGHT JOIN (правое соединение)
Что это: Ревер LEFT JOIN. Возвращает ВСЕ строки из правой таблицы, плюс совпадающие из левой.
SELECT
u.user_id,
u.name,
o.order_id
FROM users u
RIGHT JOIN orders o ON u.user_id = o.user_id;
Визуально:
Результат RIGHT JOIN (ВСЕ orders, с пользователями если есть):
user_id | name | order_id
1 | Alice | 101
1 | Alice | 102
2 | Bob | 103
NULL | NULL | 104 ← Заказ от неизвестного пользователя (ошибка в БД)
Когда использую:
- Редко в практике
- Обычно переписываю как LEFT JOIN, меняя таблицы местами
- Помогает найти «сиротские» записи (заказы без пользователя)
Пример:
-- Найти заказы от несуществующих пользователей (data quality issue)
SELECT
o.order_id,
o.user_id,
o.amount
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
WHERE u.user_id IS NULL; ← Заказы без пользователя
4. FULL OUTER JOIN (полное соединение)
Что это: Возвращает ВСЕ строки из обеих таблиц. Если совпадения нет — NULL.
SELECT
COALESCE(u.user_id, o.user_id) as user_id,
u.name,
o.order_id
FROM users u
FULL OUTER JOIN orders o ON u.user_id = o.user_id;
Визуально:
Результат FULL OUTER JOIN:
user_id | name | order_id
1 | Alice | 101
1 | Alice | 102
2 | Bob | 103
3 | Carol | NULL ← Есть в users, нет в orders
4 | David | NULL ← Есть в users, нет в orders
NULL | NULL | 104 ← Есть в orders, нет в users
Когда использую:
- Data reconciliation: проверка целостности данных
- Аудит: найти несоответствия между таблицами
- Редко в день-в-день аналитике, но полезно для QA
Пример из работы:
-- Найти все несоответствия между таблицами users и user_profiles
SELECT
COALESCE(u.user_id, up.user_id) as user_id,
u.email,
up.profile_completed,
CASE
WHEN u.user_id IS NULL THEN 'ORPHAN_PROFILE'
WHEN up.user_id IS NULL THEN 'MISSING_PROFILE'
ELSE 'OK'
END as status
FROM users u
FULL OUTER JOIN user_profiles up ON u.user_id = up.user_id;
5. CROSS JOIN (декартово произведение)
Что это: Соединяет каждую строку левой таблицы с каждой строкой правой. Результат: M × N строк.
SELECT
u.user_id,
p.product_id
FROM users u
CROSS JOIN products p;
-- Если users = 100, products = 50 → результат 5000 строк
Когда использую:
- Создание картезианского произведения для анализа
- Пример: все комбинации пользователей и товаров
- Для расчета потенциальных покупок
Пример:
-- Какой % товаров купил каждый пользователь?
WITH user_product_combinations AS (
SELECT
u.user_id,
p.product_id
FROM users u
CROSS JOIN products p
),
purchases AS (
SELECT
user_id,
product_id,
1 as purchased
FROM orders
)
SELECT
upc.user_id,
COUNT(DISTINCT upc.product_id) as total_products,
COUNT(DISTINCT p.product_id) as purchased_products,
ROUND(100.0 * COUNT(DISTINCT p.product_id) / COUNT(DISTINCT upc.product_id), 2) as purchase_rate
FROM user_product_combinations upc
LEFT JOIN purchases p ON upc.user_id = p.user_id AND upc.product_id = p.product_id
GROUP BY upc.user_id;
6. SELF JOIN (соединение таблицы с самой собой)
Что это: Соединяешь таблицу с копией самой себя. Полезно для анализа иерархии или сравнения.
SELECT
e1.employee_id,
e1.name as employee_name,
e2.name as manager_name
FROM employees e1
LEFT JOIN employees e2 ON e1.manager_id = e2.employee_id;
Визуально:
Таблица employees:
employee_id | name | manager_id
1 | Alice | NULL
2 | Bob | 1 (Alice — его менеджер)
3 | Carol | 1 (Alice — её менеджер)
4 | David | 2 (Bob — его менеджер)
Результат SELF JOIN:
employee_id | employee_name | manager_name
1 | Alice | NULL
2 | Bob | Alice
3 | Carol | Alice
4 | David | Bob
Пример из аналитики:
-- Сравни метрики пользователя с его друзьями
SELECT
u1.user_id,
u1.name,
AVG(u1.revenue) as user_revenue,
AVG(u2.revenue) as friends_avg_revenue
FROM users u1
INNER JOIN friendships f ON u1.user_id = f.user_id_1
INNER JOIN users u2 ON f.user_id_2 = u2.user_id
GROUP BY u1.user_id, u1.name;
7. Практические примеры для аналитики
Задача 1: Найти пользователей, которые купили товар в категории A и В
SELECT
u.user_id,
u.name
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
INNER JOIN products p ON o.product_id = p.product_id
WHERE p.category = 'A'
INTERSECT
SELECT
u.user_id,
u.name
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
INNER JOIN products p ON o.product_id = p.product_id
WHERE p.category = 'B';
-- Или через GROUP BY (часто быстрее):
SELECT
u.user_id,
u.name,
COUNT(DISTINCT CASE WHEN p.category = 'A' THEN 1 END) as has_category_a,
COUNT(DISTINCT CASE WHEN p.category = 'B' THEN 1 END) as has_category_b
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
INNER JOIN products p ON o.product_id = p.product_id
GROUP BY u.user_id, u.name
HAVING COUNT(DISTINCT CASE WHEN p.category = 'A' THEN 1 END) > 0
AND COUNT(DISTINCT CASE WHEN p.category = 'B' THEN 1 END) > 0;
Задача 2: Распределение пользователей по количеству заказов
SELECT
'No Orders' as order_bucket,
COUNT(*) as user_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL
UNION ALL
SELECT
CASE
WHEN order_count = 1 THEN '1 Order'
WHEN order_count <= 5 THEN '2-5 Orders'
WHEN order_count <= 10 THEN '6-10 Orders'
ELSE '10+ Orders'
END as order_bucket,
COUNT(*) as user_count
FROM (
SELECT
u.user_id,
COUNT(o.order_id) as order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id
) subquery
GROUP BY order_bucket;
Задача 3: Когортный анализ с несколькими JOIN-ами
SELECT
DATE_TRUNC('month', u.created_at) as cohort_month,
COUNT(DISTINCT u.user_id) as cohort_size,
COUNT(DISTINCT o.user_id) as users_with_orders,
COUNT(DISTINCT s.user_id) as users_with_subscriptions,
AVG(o.amount) as avg_order_value
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
LEFT JOIN subscriptions s ON u.user_id = s.user_id
GROUP BY DATE_TRUNC('month', u.created_at)
ORDER BY cohort_month DESC;
8. Частые ошибки при JOINах
⚠️ Ошибка 1: Забыли, что LEFT JOIN может дублировать строки
-- НЕПРАВИЛЬНО
SELECT
u.user_id,
COUNT(*) as order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id;
-- Если у пользователя 3 заказа и 2 подписки → COUNT(*) = 6 (декартово произведение!)
-- ПРАВИЛЬНО
SELECT
u.user_id,
COUNT(DISTINCT o.order_id) as order_count,
COUNT(DISTINCT s.subscription_id) as subscription_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
LEFT JOIN subscriptions s ON u.user_id = s.user_id
GROUP BY u.user_id;
⚠️ Ошибка 2: Неправильный ON условие
-- НЕПРАВИЛЬНО (производит много дубликатов)
SELECT *
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id AND u.country = 'USA';
-- Пользователь из другой страны получит NULLs в orders
-- ПРАВИЛЬНО (если нужны только US пользователи)
SELECT *
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE u.country = 'USA';
⚠️ Ошибка 3: Забыли про NULL при подсчете
-- НЕПРАВИЛЬНО
SELECT
u.user_id,
SUM(o.amount) as total_spent
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id;
-- SUM(NULL) = NULL, пользователи без заказов покажут NULL вместо 0
-- ПРАВИЛЬНО
SELECT
u.user_id,
COALESCE(SUM(o.amount), 0) as total_spent ← Конвертируем NULL в 0
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id;
Вывод
Быстрая шпаргалка:
INNER JOIN → Только совпадения (пересечение)
LEFT JOIN → Все левые + совпадающие правые (используется в 80% аналитики)
RIGHT JOIN → Все правые + совпадающие левые (используется редко)
FULL OUTER JOIN → Все строки из обеих таблиц
CROSS JOIN → Декартово произведение (каждая с каждой)
SELF JOIN → Соединение таблицы с собой (иерархия, сравнение)
Для Product Analyst-а: 90% времени используешь INNER JOIN и LEFT JOIN. Если ты понимаешь, когда использовать каждый и как избежать дубликатов — ты на половину экспертом в аналитике.