Производительность EXISTS и JOIN
Выбирайте более быстрый шаблон
«Производительность EXISTS и JOIN» — бесплатный урок SQL Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения SQL Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс SQL Academy содержит 4 уроков всего.
Почему здесь важна производительность
Когда нужно проверить, существуют ли связанные строки в другой таблице, SQL предоставляет несколько инструментов: EXISTS, IN и JOIN. Каждый из них даёт правильные результаты, но их производительность может сильно различаться в зависимости от размера данных, индексов и движка базы данных.
В этом уроке Вы узнаете, как каждый подход работает внутри базы данных и в каких случаях выбирать тот или иной.
Таблицы для примеров
На протяжении всего урока мы будем использовать две таблицы: customers и orders. У клиента может не быть заказов или может быть один либо несколько заказов. Это классическое отношение один-ко-многим, идеально подходящее для проверки вариантов использования EXISTS и JOIN.
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INT REFERENCES customers(id),
total NUMERIC(10,2)
);
INSERT INTO customers (name) VALUES
('Alice'), ('Bob'), ('Carol'), ('Dave');
INSERT INTO orders (customer_id, total) VALUES
(1, 120.00), (1, 85.50), (3, 200.00);Подход с JOIN
Распространённый подход — использовать INNER JOIN, чтобы найти клиентов хотя бы с одним заказом. Это работает, но обратите внимание на проблему: если у клиента пять заказов, до того как DISTINCT устранит дубликаты, он появится в наборе результатов пять раз.
Такое дублирование — лишняя работа для базы данных: она формирует полный результат соединения, а затем удаляет дубликаты.
SELECT DISTINCT c.id, c.name
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id;Подход с EXISTS
EXISTS отвечает на вопрос «да или нет»: существует ли хотя бы одна подходящая строка? Как только движок находит первое совпадение, он прекращает сканирование — это называется вычислением с коротким замыканием.
Дубликаты не создаются, и DISTINCT не нужен, поскольку EXISTS никогда не возвращает внутренние строки.
SELECT c.id, c.name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);Ключевую роль играет вычисление с коротким замыканием
Вычисление с коротким замыканием означает, что подзапрос останавливается, как только найдена одна подходящая строка. Независимо от того, есть у клиента 1 заказ или 10 000 заказов, EXISTS читает данные только до первого совпадения.
JOIN должен прочитать все подходящие строки, чтобы сформировать набор результатов, даже если Вас интересует только наличие. В широких таблицах с множеством дочерних строк для каждой родительской записи эта разница быстро становится существенной.
-- EXISTS stops after finding row #1
SELECT c.name
FROM customers c
WHERE EXISTS (
SELECT 1 -- 'SELECT 1' is conventional; the value does not matter
FROM orders o
WHERE o.customer_id = c.id
);
-- JOIN scans ALL matching order rows
SELECT DISTINCT c.name
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id;NOT EXISTS и LEFT JOIN ... IS NULL
Для противоположной проверки — поиска клиентов, у которых нет заказов, — можно использовать NOT EXISTS или шаблон LEFT JOIN ... WHERE IS NULL. Оба варианта распространены, но NOT EXISTS обычно читается понятнее, и оптимизатор часто отдаёт ему предпочтение.
-- NOT EXISTS
SELECT c.id, c.name
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.id
);
-- LEFT JOIN ... IS NULL (equivalent result)
SELECT c.id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;Роль индексов
И EXISTS, и JOIN значительно выигрывают от наличия индекса по столбцу внешнего ключа. Без индекса по orders.customer_id каждая строка внешнего запроса вызывает полное сканирование таблицы orders.
Добавление такого индекса часто даёт самый большой прирост производительности — он важнее, чем выбор между EXISTS и JOIN.
-- Create an index on the foreign key
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
-- Now both patterns use an index lookup instead of a full scan
EXPLAIN
SELECT c.name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id
);Чтение результата EXPLAIN
Используйте EXPLAIN (или EXPLAIN ANALYZE, чтобы также выполнить запрос), чтобы увидеть, как база данных выполняет запрос. Обратите внимание на следующие признаки:
- Сканирование по индексу — хорошо: используется индекс.
- Последовательное сканирование большой таблицы — потенциальный тревожный сигнал; возможно, поможет индекс.
- Хеш-соединение / вложенный цикл — выбранный алгоритм соединения; вложенный цикл хорошо сочетается со сканированием по индексу.
EXPLAIN ANALYZE
SELECT c.name
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id
GROUP BY c.id, c.name
HAVING COUNT(o.id) > 0;Когда JOIN лучше
EXISTS отлично подходит для проверок наличия. Но если Вам также нужны данные из связанной таблицы — например, общая сумма заказа или дата заказа, — необходимо использовать JOIN. Вернуть столбцы из подзапроса EXISTS невозможно.
Выбирайте инструмент в соответствии с задачей: EXISTS для вопроса «существует ли?», JOIN для запроса «покажите данные из обеих таблиц».
-- Need order data? JOIN is the only option.
SELECT c.name, o.total, o.id AS order_id
FROM customers c
INNER JOIN orders o ON o.customer_id = c.id
ORDER BY c.name;IN и EXISTS для больших наборов
IN (subquery) сначала полностью выполняет вложенный запрос, создает список значений в памяти, а затем сравнивает каждую внешнюю строку с этим списком. При миллионах строк такой список может исчерпать доступную память.
EXISTS обрабатывается построчно и прекращает работу при первом совпадении, поэтому полный внутренний набор результатов никогда не материализуется. При больших коррелированных проверках EXISTS почти всегда работает быстрее, чем IN.
-- IN builds the full list first
SELECT name
FROM customers
WHERE id IN (
SELECT customer_id FROM orders
);
-- EXISTS evaluates per-row and short-circuits
SELECT name
FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.customer_id = c.id
);Краткая памятка по выбору
Ниже приведена краткая памятка по выбору подходящего шаблона:
- EXISTS — нужно только определить, существует ли совпадение; дочерние таблицы большие; NOT EXISTS используется для антисоединения.
- JOIN — нужны столбцы из связанной таблицы или агрегирование по обеим таблицам.
- IN — короткие статические списки значений (
WHERE status IN ('active', 'pending')); избегайте больших вложенных запросов. - Всегда индексируйте столбец внешнего ключа — это важнее, чем выбор синтаксиса.
Быстрая проверка
Какое утверждение лучше всего объясняет, почему EXISTS может работать быстрее, чем INNER JOIN + DISTINCT, при проверке наличия связанных строк?
Итоги урока
В этом уроке Вы научились выбирать между EXISTS и JOIN с учетом производительности SQL:
- EXISTS прекращает работу при первом совпадении — сканирование останавливается сразу после обнаружения первой подходящей строки, поэтому дубликаты не возникают и не требуется DISTINCT.
- JOIN возвращает все совпадающие строки — используйте его, когда нужны данные из связанной таблицы, но добавьте DISTINCT или GROUP BY, если важна только родительская строка.
- NOT EXISTS — понятный шаблон антисоединения; LEFT JOIN ... IS NULL эквивалентен ему, но более многословен.
- Избегайте IN с большими вложенными запросами — он материализует весь внутренний результат; EXISTS эффективнее использует память.
- Индексируйте внешние ключи — этот шаг часто дает наибольший прирост производительности независимо от выбранного синтаксиса.
- Используйте EXPLAIN / EXPLAIN ANALYZE, чтобы проверить план выполнения и убедиться, что используются индексы.
Часто задаваемые вопросы
Урок «Производительность EXISTS и JOIN» бесплатный?
Да — полный текст урока «Производительность EXISTS и JOIN» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс SQL Academy, подпишись на CoddyKit PRO. Курс SQL Academy содержит 4 уроков всего.
Чему я научусь в уроке «Производительность EXISTS и JOIN»?
Выбирайте более быстрый шаблон Ты практикуешь SQL Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать SQL Academy?
Предыдущий опыт не требуется. SQL Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Производительность EXISTS и JOIN»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке SQL Academy?
Да. Каждый урок SQL Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Коррелированные подзапросы
- EXISTS и NOT EXISTS
- IN, ANY и ALL
- Производительность EXISTS и JOIN