Подзапрос — это SELECT внутри SELECT
Подзапрос в SQLite — это обычный SELECT, вложенный внутрь другого запроса и заключённый в круглые скобки. SQLite сначала выполняет внутренний запрос, а его результат передаёт во внешний.
Подготовим небольшой пример, к которому будем возвращаться:
Пять заказов, четыре клиента, двое из которых ничего не заказывали. Этот набор будем использовать дальше во всех примерах.
Подзапрос в WHERE: фильтрация по списку
Самый частый сценарий — это вложенный SELECT в SQLite: внутренний запрос возвращает список id, а внешний по нему фильтрует.
Внутренний запрос возвращает все customer_id, которые встречаются в таблице orders. Внешний запрос оставляет только тех клиентов, чей id есть в этом списке. Клео, Борис и Ада попадают в результат, а Дмитрий (у него нет заказов) — нет.
IN (SELECT ...) — это рабочая лошадка для запросов вида «строки из A, у которых есть совпадение в B». Читайте это про себя так: «где значение этой колонки совпадает с одним из значений, которые вернул вложенный SELECT».
NOT IN в SQLite: осторожно с NULL
Обратный вопрос — «кто из клиентов ещё не делал заказов?» — решается буквально одной строкой:
Здесь это срабатывает. Но у NOT IN есть подводный камень: если подзапрос вернёт хотя бы один NULL, то всё выражение NOT IN превратится в NULL (а это не TRUE), и вы получите ноль строк. Сюрприз — причём молчаливый.
Безопасная привычка при использовании NOT IN со столбцом, в котором могут быть NULL:
Либо просто используйте NOT EXISTS, у которого этой проблемы нет вообще. До него мы ещё доберёмся.
Скалярный подзапрос в SQLite: одна строка, одно значение
Скалярный подзапрос возвращает ровно одно значение — одну строку и один столбец — и его можно подставлять в любое место, где ожидается значение.
Внутренний SELECT MAX(total) FROM orders возвращает 200, а внешний запрос отбирает заказы с таким значением. Удобный приём, когда нужно сравнить строки с агрегатом.
Скалярный подзапрос в SQLite можно использовать и в списке SELECT — чтобы прицепить вычисленное значение к каждой строке:
Для каждой строки customers внутренний запрос выполняется заново — туда подставляется customers.id. Это и есть коррелированный подзапрос, ниже разберём подробнее. Когда нужно "одно число на строку", связка LEFT JOIN + GROUP BY обычно работает быстрее, но скалярный подзапрос читается куда приятнее.
EXISTS: просто проверить, есть ли совпадения
EXISTS — это спокойный родственник IN. Ему вообще не интересны значения: он лишь проверяет, вернул ли подзапрос хоть одну строку. Внутри обычно пишут SELECT 1 — какая разница, какой там столбец.
Такой запрос находит клиентов, у которых есть хотя бы один заказ дороже 100. Внутренний запрос ссылается на c.id из внешнего — именно эта связь делает подзапрос коррелированным. SQLite прекращает сканировать внутреннюю таблицу сразу после первого совпадения, поэтому EXISTS обычно работает быстрее, чем IN, когда нужно ответить на вопрос «есть ли у этой строки связанная запись?».
Обратная форма, NOT EXISTS, — это безопасный относительно NULL способ спросить «нет связанной строки»:
Подзапрос в FROM: производная таблица
Подзапрос можно поставить везде, где допустима таблица, — в том числе в FROM. Внутренний запрос превращается во временную именованную «производную таблицу», по которой дальше можно делать JOIN, фильтровать или агрегировать.
Внутренний запрос считает сумму по каждому клиенту, а внешний — усредняет эти суммы по странам. Двухэтапная агрегация вроде этой — как раз то, ради чего и нужны производные таблицы (derived tables): когда уложить всё в один GROUP BY не получается.
Алиас AS per_customer обязателен: у каждой производной таблицы должно быть имя.
Коррелированный подзапрос в SQLite: выполнение для каждой внешней строки
Подзапрос называется коррелированным, когда он ссылается на столбец из внешнего запроса. SQLite вынужден пересчитывать такой вложенный SELECT для каждой строки внешнего запроса — это даёт гибкость, но может дорого обойтись по производительности.
Для каждого клиента находим его самый крупный заказ. Внутренний запрос зависит от customers.id, поэтому выполняется по одному разу для каждого клиента. Клиенты без заказов получат NULL — как раз то, что нужно.
Коррелированный подзапрос в SQLite — естественный выбор для задач вида «для каждой строки из A посчитать что-то по B». Если таблица небольшая или поиск идёт по индексу — всё в порядке. А вот на больших таблицах без подходящих индексов перед выкаткой обязательно прогоните EXPLAIN: связка JOIN с GROUP BY нередко оказывается быстрее.
Подзапрос или JOIN: что выбрать?
Оба запроса ниже отвечают на один и тот же вопрос:
Оба варианта возвращают одни и те же строки. Оптимизатор SQLite вообще часто переписывает одну форму в другую под капотом. Так что выбирайте по читаемости:
- Берите подзапрос, когда нужна только фильтрация и колонки из внутренней таблицы в результате не нужны.
- Берите JOIN, когда в выборку должны попасть колонки из обеих таблиц.
- Берите EXISTS, когда вопрос звучит как «существует ли хотя бы одна связанная строка?» — так понятнее, и вы обходите грабли с
NULLуIN/NOT IN.
Если сомневаетесь — пишите тот вариант, который понятен сам по себе при чтении вслух.
Типичная ловушка: подзапрос возвращает несколько строк
Подзапрос с оператором = обязан вернуть не больше одной строки. Если строк окажется больше, SQLite молча выберет одну (фактически случайную) — никакой ошибки не будет, зато результат тихо окажется неверным.
Используйте IN, когда вложенный запрос может вернуть несколько строк:
Если вы рассчитываете ровно на одну строку и хотите это гарантировать, добавьте LIMIT 1 вместе с ORDER BY — тогда хотя бы выбор будет детерминированным. А ещё лучше — построить запрос так, чтобы единственная строка была гарантирована самими данными (например, фильтр по уникальной колонке).
Дальше: обобщённые табличные выражения (CTE)
Подзапрос в FROM sqlite быстро превращается в кашу — особенно когда одну и ту же производную таблицу нужно использовать дважды или когда вложенность уходит на три уровня вглубь. Обобщённые табличные выражения (WITH ... AS (...)) позволяют заранее дать подзапросу имя и потом обращаться к нему по этому имени во всём остальном запросе. Об этом — на следующей странице.
Часто задаваемые вопросы
Что такое подзапрос в SQLite?
Подзапрос — это SELECT, вложенный внутрь другого запроса и взятый в круглые скобки. SQLite сначала выполняет внутренний запрос, а его результат передаёт во внешний. Подзапросы можно ставить в WHERE, FROM, SELECT и ещё в нескольких местах.
В чём разница между IN и EXISTS в SQLite?
IN (SELECT ...) сравнивает значение со списком строк, который вернул подзапрос. EXISTS (SELECT ...) же проверяет только сам факт — есть хоть одна строка или нет, а на конкретные значения ему всё равно. Для коррелированных подзапросов (когда внутренний SELECT ссылается на внешнюю строку) EXISTS, как правило, работает быстрее и читается естественнее.
Что выбрать в SQLite — подзапрос или JOIN?
Если в результат нужны столбцы из обеих таблиц — берите JOIN. Если задача только отфильтровать строки или достать одно значение — подойдёт подзапрос. Оптимизатор SQLite во многих случаях сам перепишет одно в другое, так что ориентируйтесь на читаемость.
Что такое коррелированный подзапрос?
Коррелированный подзапрос ссылается на столбец внешнего запроса, поэтому пересчитывается заново для каждой строки снаружи. Гибко, но на больших таблицах легко получить тормоза. Если профилирование показало, что узкое место именно тут, обычно помогает переписать его через JOIN или CTE (WITH).