← Назад к вопросам

Какие знаешь соединения в таблицах SQL?

1.0 Junior🔥 221 комментариев
#SQL и базы данных

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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. Если ты понимаешь, когда использовать каждый и как избежать дубликатов — ты на половину экспертом в аналитике.

Какие знаешь соединения в таблицах SQL? | PrepBro