Учебное пособие по подзапросам SQL: типы, коррелирующие подзапросы и EXISTS с примерами

  1. Главная
  2. Блог
  3. Базы данных и SQL

Базы данных и SQL

Учебное пособие по подзапросам SQL: Типы, коррелирующие подзапросы и EXISTS с примерами

Подзапрос — это запрос внутри другого запроса. Изучите каждый тип и то, где он подходит, правильно поймите коррелированные подзапросы и избегайте ловушки NOT IN, которая вообще не возвращает никаких строк.

9 мин чтения
Учебное пособие по подзапросам SQL: типы, коррелированные подзапросы и EXISTS с примерами

Главное

  • Подзапрос — это запрос, вложенный в другой, и внешний запрос использует его результат.
  • Подзапросы могут появляться в WHERE, FROM, SELECT и HAVING.
  • коррелированный подзапрос относится к внешней строке, поэтому он логически оценивается для каждой строки.
  • Предпочитайте от NOT EXISTS до NOT IN, если возможны значения NULL.

На некоторые вопросы требуется два шага, чтобы ответить. «Какие заказы выше среднего?» требует, чтобы вы сначала нашли среднее значение, а затем сравнивали с ним каждый заказ. Подзапросы SQL позволяют записать оба шага в один оператор.

В этом руководстве каждый тип рассматривается с использованием двух небольших таблиц. Мы выполнили эти запросы в SQLite 3.50, и показанные результаты получены в результате этого выполнения. Стандартный синтаксис подзапросов широко поддерживается, и мы указываем места, где базы данных различаются.

Что такое подзапрос?

Подзапрос, также называемый внутренним или вложенным запросом, представляет собой оператор SELECT, помещенный внутри другого оператора в круглых скобках. Концептуально внутренний запрос выдает результат, который затем использует внешний запрос.

SELECT id, total
FROM orders
WHERE total > (SELECT AVG(total) FROM orders);

Примеры таблиц

CREATE TABLE customers (id INT, name TEXT, city TEXT);
CREATE TABLE orders (id INT, customer_id INT, total INT);

INSERT INTO customers VALUES
 (1,'Asha','Pune'), (2,'Ben','Delhi'), (3,'Chitra','Pune'), (4,'Dev','Goa');

INSERT INTO orders VALUES
 (101,1,1200), (102,1,800), (103,2,300), (104,3,2500), (105,3,100);

Обратите внимание, что у Дева нет заказов. Эта деталь важна для нескольких примеров ниже.

Типы подзапросов с первого взгляда

Тип Возврат Типичное расположение Общие операторы
Скаляр Одно значение WHERE, SELECT =, >, <
Многорядный Один столбец, много строк ГДЕ IN, ANY, ALL
Таблица (производная) Много столбцов и строк ОТ Используется как стол
Коррелированный Зависит от внешнего ряда WHERE, SELECT Любой и EXISTS

Скалярные подзапросы

Скалярный подзапрос возвращает ровно одно значение, поэтому его можно использовать везде, где имеет смысл одно значение. Вот запрос, полученный ранее, с его результатом. Среднее количество всех заказов составляет 980:

SELECT id, total
FROM orders
WHERE total > (SELECT AVG(total) FROM orders);

-- 101 | 1200
-- 104 | 2500

Вы не могли бы написать WHERE total > AVG(total), потому что агрегаты не разрешены непосредственно в WHERE. Подзапрос позволяет обойти эту проблему, вычисляя значение отдельно.

Что, если «скалярный» подзапрос вернет несколько строк? Большинство баз данных, включая PostgreSQL, MySQL и SQL Server, выдают ошибку. SQLite нет. В нашем тесте он молча использовал только первую строку, что могло скрыть реальную ошибку, поэтому не полагайтесь на такое поведение.

В и НЕ В

Когда внутренний запрос возвращает список, используйте IN. При этом будут найдены клиенты, у которых хотя бы один заказ превышает 1000:

SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE total > 1000);

-- Asha
-- Chitra

NOT IN делает обратное, находя клиентов, которые никогда не заказывали:

SELECT name FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);

-- Dev

Здесь это работает, но NOT IN имеет опасный крайний случай, который мы рассмотрим ниже.

Подзапросы в FROM

Подзапрос в FROM действует как временная таблица, часто называемая производной таблицей. Это полезно, когда вам нужно дважды агрегировать данные, как в этом запросе для получения средних расходов на одного клиента в каждом городе:

SELECT city, AVG(spent) AS avg_spent
FROM (
  SELECT c.city, SUM(o.total) AS spent
  FROM customers c
  JOIN orders o ON o.customer_id = c.id
  GROUP BY c.id, c.city
) AS per_customer
GROUP BY city;

-- Delhi |  300.0
-- Pune  | 2300.0

Внутренний запрос подсчитывает итоги по каждому клиенту, а внешний усредняет эти итоги. Присвоение производной таблице псевдонима — хорошая привычка, поскольку этого требуют некоторые базы данных. Если GROUP BY для вас новичок, см. SQL GROUP BY и HAVING.

Подзапросы в SELECT

Скалярный подзапрос также может добавить вычисляемый столбец. Здесь подсчитываются заказы на одного клиента:

SELECT name,
       (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS order_count
FROM customers c;

-- Asha   | 2
-- Ben    | 1
-- Chitra | 2
-- Dev    | 0

Дев появляется с нулем. Внутреннее соединение полностью исключило бы Dev, поскольку у Dev нет соответствующих строк. Тот же результат можно получить с помощью LEFT JOIN и GROUP BY, которые мы объясняем в разделе Объяснение соединений SQL.

Связанные подзапросы

Предыдущий пример также коррелирует, поскольку внутренний запрос ссылается на c.id из внешнего запроса. Некоррелированный подзапрос может выполняться отдельно. Коррелированный вариант невозможен, поскольку он зависит от значения из текущей внешней строки.

При этом количество заказов превышает средний показатель того же клиента:

SELECT o.id, o.customer_id, o.total
FROM orders o
WHERE o.total > (
  SELECT AVG(i.total)
  FROM orders i
  WHERE i.customer_id = o.customer_id
);

-- 101 | 1 | 1200
-- 104 | 3 | 2500

Вот как это читать. Для заказа 101 внутренний запрос усредняет заказы клиента 1 (1200 и 800), что дает 1000. Поскольку 1200 больше, строка сохраняется. Для порядка 102 применяется то же среднее значение, но значение 800 не превышает его, поэтому оно отбрасывается. Одиночный заказ клиента 2 равен его среднему значению, поэтому он тоже отбрасывается.

Особенность Некоррелированный Коррелированный
Использует значения внешнего запроса? Нет Да
Может ли работать самостоятельно? Да Нет
Логическое исполнение Однажды Один раз на внешний ряд
Риск производительности Обычно низкий Может быть высоко на больших столах.

«Один раз в строке» описывает логику, и оптимизаторы запросов часто переписывают ее во что-то гораздо более эффективное. Тем не менее, медленный коррелированный подзапрос заслуживает изучения. Индекс во внутреннем столбце (здесь orders.customer_id) может иметь большое значение. См. Объяснение индексов SQL.

СУЩЕСТВУЕТ и НЕ СУЩЕСТВУЕТ

EXISTS задает вопрос «да» или «нет»: возвращает ли внутренний запрос хотя бы одну строку? Он может остановиться на первом же совпадении, и то, что вы выберете внутри, не имеет значения, поэтому люди часто пишут SELECT 1.

SELECT c.name FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

-- Asha, Ben, Chitra

SELECT c.name FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

-- Dev

Ловушка NOT IN NULL

Это одна из наиболее распространенных ошибок подзапросов. Мы создали таблицу с одним реальным идентификатором и одним NULL, а затем запустили против нее обе формы:

CREATE TABLE r (customer_id INT);
INSERT INTO r VALUES (1), (NULL);

SELECT name FROM customers
WHERE id NOT IN (SELECT customer_id FROM r);
-- (no rows at all)

SELECT c.name FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM r WHERE r.customer_id = c.id);
-- Ben, Chitra, Dev

Почему первый ничего не возвращает? id NOT IN (1, NULL) означает id <> 1 AND id <> NULL. Любое сравнение с NULL скорее неизвестно, чем истинно, поэтому все условие никогда не может быть истинным. Нет ни сообщения об ошибке, ни предупреждения, просто пустой результат, который выглядит правдоподобно.

Совет. По умолчанию используйте NOT EXISTS для запрета объединений. Если вы используете NOT IN, добавьте WHERE customer_id IS NOT NULL внутри подзапроса.

Подзапрос против JOIN против CTE

Многие запросы могут быть написаны более чем одним способом. Ранее мы находили клиентов с большим заказом с помощью IN. Вот тот же результат с соединением:

SELECT DISTINCT c.name FROM customers c
JOIN orders o ON o.customer_id = c.id AND o.total > 1000;

-- Asha
-- Chitra

Обратите внимание на DISTINCT. Соединение может создавать дубликаты, если у клиента есть несколько совпадений, тогда как IN и EXISTS не могут.

Использование Когда
ПРИСОЕДИНЯЙТЕСЬ Вам нужны столбцы из обеих таблиц
В или СУЩЕСТВУЕТ Вам нужно только фильтровать по тому, существует ли совпадение.
Производная таблица Сначала вам нужно агрегировать, и он остается коротким
КТР Логика состоит из нескольких шагов или используется более одного раза.
Функция окна Вам нужно число для каждой группы рядом с каждой строкой.

Производительность зависит от базы данных и данных, и современные оптимизаторы часто создают схожие планы. Поэтому сначала выберите самую понятную форму, а затем проверьте EXPLAIN, если она окажется медленной.

Распространенные ошибки

  • Используйте = с многострочным результатом. Используйте IN или убедитесь, что внутренний запрос возвращает одну строку.
  • Использование NOT IN для столбца, допускающего значение NULL. Вы можете незаметно потерять каждую строку.
  • Использование непонятных псевдонимов. В коррелированных запросах уточняйте каждый столбец, иначе он может быть привязан к неправильной таблице.
  • Глубокая вложенность. Три уровня трудно читать, поэтому преобразуйте их в CTE.
  • Добавление ORDER BY внутрь. Обычно это не влияет на внешний результат, и некоторые базы данных отклоняют его.

Итоги

Подзапросы — это естественный способ выражения двухэтапных вопросов. Начните со скалярных подзапросов и IN, затем попрактикуйтесь в коррелированных, рассматривая по одной внешней строке за раз. И помните NOT EXISTS всякий раз, когда возможны значения NULL. Для практики напишите один вопрос тремя способами: с подзапросом, соединением и CTE, и сравните результаты и читаемость.

Часто задаваемые вопросы

Что такое подзапрос в SQL?

Подзапрос — это оператор SELECT, вложенный в другой оператор SQL в скобках. Внешний оператор использует свой результат в виде значения, списка или таблицы.

Что такое коррелированный подзапрос?

Это подзапрос, который ссылается на столбец из внешнего запроса. Это зависит от текущей внешней строки, поэтому не может работать самостоятельно.

Что быстрее: подзапрос или JOIN?

Это зависит от базы данных и данных, и оптимизаторы часто создают аналогичные планы. Сначала напишите самую понятную версию, а затем сравните планы выполнения, если производительность имеет значение.

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

IN сравнивает значение со списком результатов. EXISTS проверяет, возвращается ли вообще какая-либо строка. Они ведут себя по-другому с NULL, когда они отрицаются.

Может ли подзапрос возвращать несколько столбцов?

Да, при использовании в FROM в качестве производной таблицы, а некоторые базы данных также допускают сравнение строк. Скалярный подзапрос должен возвращать только один столбец и одну строку.

Продолжайте учиться

Работайте с Stack Quora

Нужна помощь с тестированием, WordPress или SEO?

Мы помогаем командам выпускать лучшее программное обеспечение и развивать свои сайты. Расскажите нам, над чем вы работаете, и мы предложим четкий следующий шаг.

Продолжайте читать

Больше руководств по тестирование, искусственный интеллект и современные разработки

Понравилась эта статья? Выберите тему и сразу приступайте к нашим новейшим руководствам.

Искусственный интеллект

Объяснение галлюцинаций искусственного интеллекта: почему языковые модели все выдумывают и как их уменьшить

Галлюцинации ИИ объяснили: что это такое, почему языковые модели их создают, распространенные типы, как уменьшить их с помощью заземления и подсказо…

Читать статью →
DevOps

Объяснение переменных среды: файлы .env, секреты и правильно выполненная конфигурация

Переменные среды объясняются на примерах Python, которые мы запускали: их чтение и установка, почему каждое значение является строкой, файлы .env, н…

Читать статью →
Разработка ПО

Объяснение ограничения скорости API: алгоритмы, заголовки и обработка ошибок 429

Ограничение скорости API объясняется с помощью проведенного нами моделирования: сегмент токенов, фиксированное окно, скользящее окно, 429 ответов, з…

Читать статью →
Автоматизация тестирования

Издевательство в Python: unittest.mock, патч и ошибки, из-за которых тесты лгут

Издевательство на Python с помощью unittest.mock, протестировано: Mock и return_value, Side_effect, исправление использования имени, спецификация и …

Читать статью →
Тестирование ПО

Объяснение покрытия кода: покрытие линий или ветвей и что на самом деле означает 100%

Покрытие кода объяснено с помощью реального запуска покрытия.py: покрытие строк, ветвей и условий, чтение отчета, почему 100%-ное покрытие все еще м…

Читать статью →
Базы данных и SQL

DELETE, TRUNCATE и DROP в SQL: различия, откат и когда использовать каждый из них

Объяснение DELETE, TRUNCATE и DROP на примере протестированного SQL: что каждый из них удаляет, откат, триггеры, сброс идентификаторов, скорость и ч…

Читать статью →
Базы данных и SQL

Объяснение представлений SQL: создавайте, используйте и избегайте ловушек, с примерами

Представления SQL, объясненные с помощью запросов, которые мы выполняли: создание представления, результаты в реальном времени, скрытие столбцов, пр…

Читать статью →
Разработка ПО

Интерфейс TypeScript и тип: реальные различия, протестированные с помощью компилятора

Интерфейс TypeScript против типа, проверенный реальным компилятором: что могут делать оба, что может делать только один, слияние объявлений, расшире…

Читать статью →
Разработка ПО

Объяснение Python *args и **kwargs: гибкие функции с понятными примерами

Python *args и **kwargs, объясненные с помощью кода, который мы запускали: упаковка и распаковка, порядок параметров, аргументы только по ключевым с…

Читать статью →
Разработка ПО

Лямбда-функции Python: синтаксис, реальное использование и когда их не следует использовать

Лямбда-функции Python, объясненные с помощью кода, который мы запускали: синтаксис, сортировка с помощью ключа, карты и фильтра, замыкания, ловушка …

Читать статью →
Искусственный интеллект

Что такое ИИ-агенты? Как они работают: можно запустить простой цикл агента

Что такое агенты ИИ? Простое руководство по циклу агента, инструментам, памяти и ограничениям, с небольшим циклом Python, который мы запустили, реал…

Читать статью →
DevOps

Мониторинг и наблюдаемость: реальная разница с журналами, метриками и трассировками

Объяснение мониторинга и наблюдаемости: известные и неизвестные сбои, журналы, метрики и трассировки, пошаговое руководство по инцидентам при оформл…

Читать статью →
DevOps

Docker Volumes против Bind Mounts: правильное сохранение данных контейнера

Объяснение томов Docker и монтирования привязки: почему исчезают данные контейнера, именованные тома, монтирование привязки, tmpfs, примеры создания…

Читать статью →
Автоматизация тестирования

TDD против BDD: ключевые различия, одна функция, созданная для обоих способов, и когда использовать каждый

TDD и BDD объясняются с помощью одной функции скидки, созданной в обоих направлениях: красно-зеленый рефакторинг с кодом, который мы запускали, сцен…

Читать статью →
Тестирование ПО

Пользовательское приемочное тестирование (UAT): процесс, контрольный список и реальные примеры

Приемочное тестирование пользователей объяснило: кто его проводит, чем оно отличается от контроля качества, семиэтапный процесс, шаблон тестового сц…

Читать статью →
Базы данных и SQL

Объяснение значений NULL в SQL: IS NULL, COALESCE, NULLIF и ловушки, которых следует избегать

Значения SQL NULL объяснены с помощью запросов, которые мы выполнили: трехзначная логика, IS NULL, COALESCE, NULLIF, как агрегаты, соединения и сорт…

Читать статью →
Базы данных и SQL

Оператор SQL CASE WHEN: синтаксис, реальные примеры и распространенные ошибки

SQL CASE WHEN объяснен с помощью запросов, которые мы выполняли: простые формы и формы для поиска, порядок условий, повороты с помощью SUM, трюки CO…

Читать статью →
Разработка ПО

Что такое JWT? Веб-токены JSON, объясненные на рабочем примере

Что такое JWT? Объяснение JSON Web Tokens: три части: токен, созданный и измененный в коде, который мы запускали, JWT и сеансы, хранилище, токены об…

Читать статью →
Разработка ПО

Объяснение цикла событий JavaScript: стек вызовов, микрозадачи и таймеры с примерами

Цикл событий JavaScript объясняется с помощью кода, который мы запускали: стек вызовов, очереди задач и микрозадач, почему setTimeout 0 не является …

Читать статью →
Разработка ПО

Генераторы Python и объяснение доходности: экономьте память с помощью реальных примеров

Генераторы Python и доходность, объясненные с помощью кода, который мы запускали: как они приостанавливаются и возобновляются, измеряют экономию пам…

Читать статью →
Искусственный интеллект

Что такое вложения? Простое объяснение векторов и косинусного сходства

Что такое вложения? Простое руководство по векторам, косинусному подобию и семантическому поиску с небольшим примером, который мы использовали, общи…

Читать статью →
DevOps

Сине-зеленое и канареечное развертывание: различия, компромиссы и как выбрать

Объяснение сине-зеленого и канареечного развертывания: как каждый из них работает, стоимость, риски и откат, стабильное группирование пользователей,…

Читать статью →
Автоматизация тестирования

Ненадежные тесты: что их вызывает и как их исправить навсегда

Ненадежные тесты объясняются на демонстрациях, которые мы запускали: основные причины, способы подтверждения и диагностики, правильные исправления в…

Читать статью →
Тестирование ПО

Нагрузочное тестирование и стресс-тестирование: различия, метрики и примеры k6

Объяснение нагрузочного и стресс-тестирования: что доказывает каждое из них, тесты на всплески и выдержки, важные метрики, почему средние значения в…

Читать статью →
Тестирование ПО

Тестовый пример и тестовый сценарий: ключевые различия, примеры и когда использовать каждый

Объяснение тестового случая и тестового сценария: четкие определения, параллельная таблица, одна функция сброса пароля, взятая из сценариев в случаи…

Читать статью →
Базы данных и SQL

Оптимизация SQL-запросов: чтение планов выполнения и исправление медленных запросов с указанием времени

Оптимизация SQL-запросов проверена на миллионе строк: читайте планы выполнения, добавляйте правильные индексы, исправляйте неразмещаемые фильтры, ис…

Читать статью →
Базы данных и SQL

SQL UNION против UNION ALL: различия, примеры и проверка производительности

SQL UNION и UNION ALL объяснены с помощью запросов, которые мы выполнили: дубликаты, правила, которым следуют оба, реальная ошибка данных, измеренна…

Читать статью →
Разработка ПО

Учебное пособие по API JavaScript Fetch: GET, POST, обработка ошибок и тайм-ауты

Учебное пособие по API JavaScript Fetch, протестированное на локальном сервере: GET и POST, почему не выдается ошибка 404, многоразовый помощник, ст…

Читать статью →
Разработка ПО

Обработка файлов Python: чтение, запись, CSV, JSON и pathlib с примерами

Обработка файлов Python объясняется с помощью кода, который мы запускали: открытые режимы, чтение больших файлов, CSV, JSON, pathlib, двоичные файлы…

Читать статью →
Разработка ПО

Классы и объекты Python: учебник ООП с реальными примерами

Классы и объекты Python, объясненные с помощью кода, который мы запускали: __init__, методы, атрибуты класса и экземпляра, наследование, свойства, м…

Читать статью →
Искусственный интеллект

Что такое большая языковая модель? Как работают LLM, объяснено без шумихи

Что такое большая языковая модель? LLM объясняется простым языком: токены, встраивания, внимание, обучение, температурные и контекстные окна, крошеч…

Читать статью →
DevOps

Команды Linux для начинающих: основное руководство по терминалу с примерами

Команды Linux для начинающих, сгруппированные по задачам: навигация, файлы, просмотр журналов, grep, каналы, разрешения, процессы и архивы, с практи…

Читать статью →
Автоматизация тестирования

Selenium vs Playwright: ключевые различия, сравнение кода и что выбрать

Сравнение Selenium и Playwright: архитектура, ожидание, поддержка браузера и языка, отладка, одинаковый тест в обоих инструментах, советы по миграци…

Читать статью →
Тестирование ПО

Разделение эквивалентности: метод проектирования тестов, объясненный на рабочих примерах

Объяснение эквивалентного разделения: как найти действительные и недействительные разделы, рабочие примеры для возрастов, паролей и купонов, проведе…

Читать статью →
Тестирование ПО

Серьезность и приоритет в тестировании программного обеспечения: различия, матрица и реальные примеры

Объяснение серьезности и приоритета при тестировании программного обеспечения: четкие определения, четырехсторонняя матрица с примерами, шкалы уровн…

Читать статью →
Базы данных и SQL

Учебное пособие по подзапросам SQL: типы, коррелирующие подзапросы и EXISTS с примерами

Руководство по подзапросам SQL с выполненными нами запросами: скалярные подзапросы, IN, FROM и SELECT, коррелированные подзапросы, EXISTS, ловушка N…

Читать статью →
Базы данных и SQL

Объяснение транзакций SQL и ACID: COMMIT, ROLLBACK и уровни изоляции

Транзакции SQL и ACID, объясненные с помощью банковского перевода, который мы выполнили: COMMIT, ROLLBACK, точки сохранения, блокировки, уровни изол…

Читать статью →
Разработка ПО

Git merge vs rebase: отличия, примеры и когда что использовать

Git merge vs rebase с реальным выводом log, сравнительной таблицей, золотым правилом rebase и безопасным рабочим процессом для команды.

Читать статью →
Разработка ПО

Генераторы списков в Python: синтаксис, примеры и когда их избегать

Генераторы списков в Python: синтаксис, фильтры, if-else, вложенные циклы, измеренное время и случаи, когда обычный цикл for лучше.

Читать статью →
Базы данных и SQL

SQL JOIN: объяснение с примерами (Inner, Left, Right, Full)

Разберите SQL JOIN шаг за шагом на примерах таблиц и запросов: INNER, LEFT, RIGHT, FULL, CROSS и SELF, а также частые ошибки и советы по производите…

Читать статью →