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

В чем разница между IN и LEFT JOIN?

2.0 Middle🔥 191 комментариев
#Базы данных (SQL)

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

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

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

Разница между IN и LEFT JOIN: выбор оптимального подхода

IN и LEFT JOIN часто используются для связи данных из разных таблиц, но имеют существенные различия в производительности, семантике и результатах. Понимание различий критично для написания эффективного SQL.

1. Семантическое различие

IN — проверка принадлежности к списку

-- Найти пользователей, которые находятся в списке ID
SELECT * FROM users WHERE user_id IN (1, 2, 3);

-- Эквивалент с OR
SELECT * FROM users WHERE user_id = 1 OR user_id = 2 OR user_id = 3;

-- С подзапросом
SELECT * FROM users WHERE user_id IN (
  SELECT user_id FROM active_users
);

LEFT JOIN — связь с выводом всех данных левой таблицы

-- Связать пользователей с их заказами, выводить всех пользователей
SELECT users.*, orders.order_id, orders.total
FROM users
LEFT JOIN orders ON users.user_id = orders.user_id;

-- Результат: каждый пользователь может быть в нескольких строках
-- Пользователи без заказов будут с NULL в order_id

2. Различие в результатах

IN возвращает одну строку на пользователя

# Python пример
users = db.execute("""
  SELECT user_id, name FROM users 
  WHERE user_id IN (1, 2, 3)
""").fetchall()

# Результат: максимум 3 строки (по одной на каждый user_id)
# [(1, Alice), (2, Bob), (3, Charlie)]

LEFT JOIN может возвращать несколько строк на пользователя

# Python пример
results = db.execute("""
  SELECT users.user_id, users.name, orders.order_id, orders.total
  FROM users
  LEFT JOIN orders ON users.user_id = orders.user_id
  WHERE users.user_id IN (1, 2, 3)
""").fetchall()

# Результат: может быть > 3 строк
# Пример:
# (1, Alice, 101, 100),  -- Alice с заказом 101
# (1, Alice, 102, 200),  -- Alice с заказом 102
# (2, Bob, None, None),  -- Bob без заказов
# (3, Charlie, 103, 150) -- Charlie с заказом 103

3. Производительность: IN vs LEFT JOIN

Сценарий 1: Просто проверка принадлежности (нужна информация только из левой таблицы)

-- ✅ БЫСТРО: IN
SELECT user_id, name, email FROM users WHERE user_id IN (1, 2, 3, 4, 5);
-- Простая индексная работа

-- ⚠️ МЕДЛЕННЕЕ: LEFT JOIN для той же цели
SELECT DISTINCT users.user_id, users.name, users.email
FROM users
LEFT JOIN some_filter_table ON users.user_id = some_filter_table.user_id
WHERE some_filter_table.user_id IS NOT NULL;
-- Лишний JOIN, deduplicate в конце

Сценарий 2: Нужны данные из обеих таблиц

-- ❌ МЕДЛЕННО: IN + подзапрос
SELECT users.*, order_data
FROM users
WHERE user_id IN (
  SELECT user_id FROM orders WHERE total > 100
)
-- Подзапрос выполняется отдельно, может быть неоптимален

-- ✅ БЫСТРО: LEFT JOIN с условием
SELECT users.user_id, users.name, COUNT(orders.order_id) as order_count
FROM users
LEFT JOIN orders ON users.user_id = orders.user_id
GROUP BY users.user_id
HAVING COUNT(orders.order_id) > 0;
-- Планировщик видит полную картину, оптимизирует лучше

4. Детальное сравнение на практических примерах

Пример 1: Получить активных пользователей

-- Способ 1: IN (просто и быстро)
SELECT * FROM users 
WHERE user_id IN (
  SELECT user_id FROM active_users
);

-- Способ 2: LEFT JOIN (может быть медленнее)
SELECT DISTINCT users.*
FROM users
LEFT JOIN active_users ON users.user_id = active_users.user_id
WHERE active_users.user_id IS NOT NULL;

-- ✅ Вывод: для простой проверки IN эффективнее

Пример 2: Пользователи и их последний заказ

-- ❌ IN не подходит (нужны данные из orders)
SELECT users.* FROM users 
WHERE user_id IN (SELECT user_id FROM orders);
-- Потеряли информацию о самом заказе

-- ✅ LEFT JOIN правильно
SELECT users.*, orders.order_id, orders.created_at
FROM users
LEFT JOIN orders ON users.user_id = orders.user_id
WHERE (users.user_id, orders.created_at) IN (
  SELECT user_id, MAX(created_at)
  FROM orders
  GROUP BY user_id
)
OR orders.order_id IS NULL;  -- Пользователи без заказов

-- ✅ Ещё лучше: оконная функция
SELECT users.*, orders.order_id
FROM users
LEFT JOIN (
  SELECT user_id, order_id,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rn
  FROM orders
) orders ON users.user_id = orders.user_id AND orders.rn = 1;

Пример 3: Дублирование в LEFT JOIN

-- ⚠️ Проблема с LEFT JOIN
SELECT users.user_id, COUNT(orders.order_id) as total_orders
FROM users
LEFT JOIN orders ON users.user_id = orders.user_id
LEFT JOIN order_items ON orders.order_id = order_items.order_id;
-- РЕЗУЛЬТАТ: дублирование! Если у заказа 3 items, row повторится 3 раза

-- ✅ Решение: подзапрос
SELECT users.user_id, o.total_orders
FROM users
LEFT JOIN (
  SELECT user_id, COUNT(*) as total_orders
  FROM orders
  GROUP BY user_id
) o ON users.user_id = o.user_id;

5. Важное: NULL в результатах LEFT JOIN

-- LEFT JOIN гарантирует, что все строки левой таблицы вернутся
SELECT users.user_id, users.name, orders.order_id
FROM users
LEFT JOIN orders ON users.user_id = orders.user_id;

-- Результат:
-- user_id | name    | order_id
-- --------|---------|----------
-- 1       | Alice   | 101
-- 1       | Alice   | 102
-- 2       | Bob     | NULL      <- Нет заказов
-- 3       | Charlie | 103

-- Это отличается от INNER JOIN:
SELECT users.user_id, users.name, orders.order_id
FROM users
INNER JOIN orders ON users.user_id = orders.user_id;
-- Боб не будет в результатах вообще

6. Производительность с большими объёмами

# Python: демонстрация проблемы
import time

# ❌ Неэффективный подзапрос в IN
start = time.time()
db.execute("""
  SELECT * FROM users 
  WHERE user_id IN (
    SELECT user_id FROM orders 
    WHERE created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
  )
""")  # Подзапрос может выполниться неоптимально
print(f"IN: {time.time() - start}s")

# ✅ Оптимальнее: JOIN
start = time.time()
db.execute("""
  SELECT DISTINCT users.*
  FROM users
  INNER JOIN orders ON users.user_id = orders.user_id
  WHERE orders.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
""")  # Планировщик видит индексы на обеих таблицах
print(f"JOIN: {time.time() - start}s")

Выбор между IN и LEFT JOIN

СценарийИспользоватьПричина
Просто проверка принадлежностиINПроще, быстрее, семантически ясно
Нужны данные из связанной таблицыLEFT JOINПолучаем обе таблицы за раз
Нужны все строки левой таблицыLEFT JOININ не вернёт пользователей без заказов
Множество значений (1000+)LEFT JOIN или подзапросБольшой IN может быть медленнее
Нужна дедупликацияОба, но с DISTINCTLEFT JOIN часто выгоднее
Несколько JOINJOININ усложняет логику с несколькими связями

Резюме

  • IN — для проверки членства в списке, семантически ясно, обычно быстрее
  • LEFT JOIN — когда нужны данные из обеих таблиц или гарантия возврата всех строк левой таблицы
  • Всегда смотри план запроса (EXPLAIN) для оптимизации
  • Большие IN списки лучше заменять на JOIN
  • Если нужны NULL для немаршруток — только LEFT JOIN подходит