Синтаксис подзапросов
Подзапрос - это оператор SELECT внутри другого оператора. Начиная
с MySQL 4.1, поддерживаются все формы подзапросов, которых требует
стандарт SQL, равно как и некоторые средства, специфичные для MySQL. В
ранних версиях MySQL приходилось искать избегать подзапросов вообще
либо искать обходные пути, но разработчики, которые начинают писать
новый код сейчас, обнаружат, что подзапросы - очень удобная часть
инструментального набора MySQL. В MySQL до 4.1 большинство
подзапросов могут быть успешно реализовано в виде соединений или другими
способами. См. раздел Замена подзапросов соединениями для ранних версий
MySQL. Вот пример подзапроса: SELECT * FROM tl WHERE columnl = (SELECT columnl FROM t2); В этом примере SELECT * FROM tl является внешним запросом (или внешним оператором),a (SELECT columnl FROM t2) - подзапросом. Говорят,
что подзапрос вложен во внешний запрос. Фактически, можно вкладывать
подзапросы в подзапросы на большую глубину. Подзапрос всегда должен
появляться в скобках. Основные преимущества подзапросов:
- Они позволяют писать структурированные запросы таким образом, что можно изолировать части оператора.
- Они представляют альтернативный способ выполнения операций, которые
требуют применения сложных соединений и слияний (JOIN и UNION).
- По мнению многих, они более читабельны. Действительно, новизна
подзапросов в том, что они наглядно представляют людям исходную идею SQL
как структурированного языка запросов.
Ниже приведен пример оператора, который демонстрирует основные
понятия о синтаксисе подзапросов, поддерживаемых MySQL, как того требует
стандарт SQL: DELETE FROM tl WHERE sll > ANY (SELECT COUNT(*) /* без подсказок */ FROM t2 WHERE NOT EXISTS (SELECT * FROM t3 WHERE ROW(5*t2.sl,77) = (SELECT 50,11*51 FROM t4 UNION SELECT 50,77 FROM (SELECT * FROM t5) AS t5)));
Подзапрос, как скалярный операндВ своей простейшей форме {скалярный подзапрос представляет собой противоположность строчным или табличным подзапросам,
описанным ниже), подзапрос - это простой операнд. То есть вы можете
использовать его в любом месте, где допустимо значение столбца или
литерал, и вы можете ожидать, что он имеет те же характеристики, которые
имеют все операнды: тип данных, длину, признак того, может ли он
принимать значение NULL, и так далее. Например: CREATE TABLE tl (si INT, s2 CHAR(5) NOT NULL); SELECT (SELECT s2 FROM tl); Подзапрос
в этом запросе имеет тип данных CHAR, длину 5, набор символов и порядок
сопоставления по умолчанию такие, как были при выполнении CREATE TABLE,
и признак того, что значение столбца допускает значение NULL.
Фактически почти все подзапросы могут быть NULL, поскольку если таблица
пуста, как в этом примере, значение подзапроса будет равно NULL.
Существует несколько ограничений.
- Внешний оператор подзапроса может быть одним из следующих: SELECT, INSERT, UPDATE, DELETE, SET ИЛИ DO.
- Подзапрос может содержать любые ключевые слова и конструкции,
которые допустимы В обычном операторе SELECT: DISTINCT, GROUP BY, ORDER
BY, LIMIT, Kонструкции JOIN, UNION, подсказки, комментарии, функции и
так далее.
Поэтому, когда вы видите примеры, приведенные ниже, которые включают
довольно краткие конструкции подзапросов (SELECT columnl FROM tl), то
представляйте себе свой собственный код, который будет содержать куда
более разнообразные и сложные конструкции. Например, представим, что созданы две таблицы: CREATE TABLE tl (si INT); INSERT INTO tl VALUES (1); CREATE TABLE t2 (si INT); INSERT INTO t2 VALUES (2); Затем выполняется такой запрос:
SELECT (SELECT si FROM t2) FROM tl; Результатом будет 2, потому что существует строка в t2, содержащая столбец si, который имеет значение 2. Подзапрос может быть частью выражения. Если это операнд функции, не забудьте указать скобки. Например: SELECT UPPER((SELECT si FROM tl)) FROM t2;
Сравнения с использованием подзапросовНаиболее часто подзапросы применяются в такой форме: операнд_не_подзапроса операция_сравнения {подзапрос) Здесь операция_сравнения - это одна из следующих операций: Например: ... 'а1 = (SELECT columnl FROM tl) Единственное
разрешенное место для подзапроса - в правой части выражения сравнения,
и вы можете найти некоторые старые системы управления базами данных,
которые настаивают на этом. Ниже приведен пример общей формы
сравнения с подзапросом, которую нельзя применять в соединении. Он
найдет все значения таблицы tl, которые равны максимальному значению в
таблице t2: SELECT columnl FROM tl WHERE columnl = (SELECT MAX(column2) FROM t2); А
вот другой пример, также невозможный в виде соединения, поскольку
включает агрегатную функцию в одной из таблиц. Он найдет все строки
таблицы tl, содержащие значение, которое встречается дважды: SELECT * FROM tl WHERE 2 = (SELECT COUNT(columnl) FROM tl);
Подзапросы с ANY, IN и SOMEСинтаксис: операнд операция_сравнения ANY {подзапрос) операнд IN {подзапрос) операнд операция_сравнения SOME {подзапрос) Ключевое
слово ANY, которое должно следовать за операцией сравнения, означает
"возвратить TRUE, если сравнение дает TRUE для ЛЮБОЙ из строк, которые
возвращает подзапрос". Например: SELECT si FROM tl WHERE si > ANY (SELECT si FROM t2); Предположим,
что в таблице tl есть строка, которая содержит (10). Выражение
истинно, если таблица t2 содержит (21,14,7), поскольку в t2 есть
значение 7, которое меньше 10. Выражение ложно, если таблица t2 содержит
(20,10), либо таблица t2 пуста. Выражение равно UNKNOWN, если таблица
t2 содержит значения (NULL, NULL, NULL). Слово IN - это псевдоним для = ANY, поэтому следующие два оператора одинаковы:
SELECT si FROM tl WHERE si = ANY (SELECT si FROM t2) ; SELECT si FROM tl WHERE si IN (SELECT si FROM t2); Слово SOME — это псевдоним для ANY, поэтому показанные ниже два оператора одинаковы: SELECT si FROM tl WHERE si <> ANY (SELECT si FROM t2); SELECT si FROM tl WHERE si <> SOME (SELECT si FROM t2); Слово
SOME применяется редко, но предыдущий пример показывает, почему оно
может оказаться удобным. Для слуха большинства людей английская фраза
"a is not equal to any b" ("а не равно любому b") означает "there is no b
which is equal to а" ("нет таких b, которые равны а"), но это не то,
что подразумевает синтаксис SQL. Применение о SOME помогает всем
правильно понимать действительный смысл запроса.
Подзапросы с ALLСинтаксис: операнд операция_сравнения ALL {подзапрос) Слово
ALL, которое должно следовать за операцией сравнения, означает
"возвратить TRUE, если сравнение дает TRUE для всех строк, возвращаемых
подзапросом". Например: SELECT si FROM tl WHERE si > ALL (SELECT si FROM t2); Предположим,
что в таблице tl есть строка, которая содержит (10). Выражение
истинно, если таблица t2 содержит (-5, 0, +5), потому что 10 больше,
чем все три значения из таблицы t2. Выражение ложно, если таблица t2
содержит (12,б,NULL,-100), поскольку только одно значение 12 в таблице
t2 больше 10. Выражение неопределенно (UNKNOWN), если таблица t2
содержит (0,NULL,1). Наконец, если таблица t2 пуста, результат равен
TRUE. Вы можете подумать, что результат будет UNKNOWN, но, сожалеем, он
будет именно TRUE. Поэтому, как ни странно, следующий оператор вернет
TRUE, если таблица t2 пуста: SELECT * FROM tl WHERE 1 > ALL (SELECT si FROM t2); Однако следующий оператор вернет UNKNOWN, если таблица t2 пуста: SELECT * FROM tl WHERE 1 > (SELECT si FROM t2); И вдобавок следующий оператор также вернет UNKNOWN, если таблица t2 пуста: SELECT * FROM tl WHERE 1 > ALL (SELECT MAX(si) FROM t2); В общем случае, таблицы со значениями NULLи пустые таблицы — это крайние случаи. Поэтому при написании кода подзапросов всегда принимайте во внимание две упомянутых возможности.
Коррелированные подзапросыКоррелированный подзапрос - это такой подзапрос, который содержит ссылку на столбец, который есть во внешнем запросе. Например: SELECT * FROM tl WHERE columnl = ANY (SELECT columnl FROM t2 WHERE t2.column2 = tl.column2); Обратите
внимание, что подзапрос содержит ссылку на столбец таблицы tl, даже
несмотря на то, что конструкция FROM подзапроса не упоминает таблицу
tl. Таким образом, MySQL выполняет просмотр за пределами подзапроса и
находит tl во внешнем запросе. Предположим, что таблица tl содержит
строку, в которой columnl = 5 и column2 = б, в то же время таблица t2
содержит строку, в которой columnl = 5 и column2 = 7. Простое выражение
... WHERE columnl=ANY (SELECT columnl FROM t2) будет TRUE, НО в ЭТОМ
примере конструкция WHERE внутри подзапроса даст FALSE (поскольку 7 не
равно 5), а поэтому и весь подзапрос вернет FALSE. Правило видимости: MySQL вычисляет выражения от внутреннего к внешнему. Например: SELECT columnl FROM tl AS x WHERE x.columnl = (SELECT columnl FROM t2 AS x WHERE x.columnl = (SELECT columnl FROM t3 WHERE x.column2 = t3.columnl)); В
этом запросе x.column2 должен быть столбцом таблицы t2, потому что
SELECT columnl FROM t2 AS x ... переименовывает t2. Это не столбец
таблицы tl, так как SELECT columnl FROM tl ...- другой запрос, который
находится во внешнем контексте. Для подзапросов, находящихся в конструкциях having или ORDER BY, MySQL также ищет имена в списке столбцов внешнего запроса. В некоторых случаях коррелированный подзапрос оптимизируется. Например: значение IN (SELECT значение_ключа FROM имя_таблицы WHERE коррелированное_условие) Иначе
они могут оказаться неэффективными и, скорее всего, медленными. Если
переписать запрос в виде соединения, это может увеличить
производительность.
EXISTS и NOT EXISTSЕсли подзапрос вообще возвращает какие-нибудь значения, то EXISTS подзапрос возвращает TRUE, a NOT EXISTS подзапрос - FALSE, например: SELECT columnl FROM tl WHERE EXISTS (SELECT * FROM t2); Традиционно
подзапрос в EXISTS начинается с SELECT *, но он может начинаться с
SELECT 5 или SELECT columnl, либо с еще чего-нибудь. MySQL игнорирует
список SELECT в таком подзапросе, потому это не важно. Для
предыдущего примера, если t2 содержит любые строки, даже строки, в
которых нет ничего кроме значений NULL, то условие EXISTS истинно.
Вообще это неправдоподобный пример, поскольку почти всегда подзапрос
[NOT] EXISTS содержит корреляцию. Ниже представлены более реалистичные
примеры:
- Какие типы магазинов есть в одном или более городов?
SELECT DISTINCT store_type FROM Stores WHERE EXISTS (SELECT * FROM Cities_Stores WHERE Cities_Stores.store_type = Stores.store_type);
- Каких типов магазинов нет ни в одном городе?
SELECT DISTINCT store_type FROM Stores WHERE NOT EXISTS (SELECT * FROM Cities_Stores WHERE Cities_Stores.store_type = Stores.store_type);
- Какой тип магазинов есть во всех городах?
SELECT DISTINCT store_type FROM Stores SI WHERE NOT EXISTS ( SELECT * FROM Cities WHERE NOT EXISTS ( SELECT * FROM Cities_Stores WHERE Cities_Stores.city = Cities.city AND Cities_Stores.store_type = Stores.store_type)); В
последнем примере представлен дважды вложенный подзапрос NOT EXISTS. To
есть конструкция NOT EXISTS содержится внутри другой конструкции NOT
EXISTS. Формально он отвечает на вопрос "есть ли город с магазином,
которого нет в Stores?". Но проще сказать, что вложенный NOT EXISTS
отвечает на вопрос "истинно ли х для всех у?".
Подзапросы, возвращающие строкуДо сих пор мы обсуждали подзапросы, возвращающие столбец (скалярные), то есть подзапросы, возвращающие единственное значение столбца. Строчные подзапросы -это вариант подзапросов, которые возвращают более одного значения столбца. Ниже представлены два примера: SELECT
* FROM tl WHERE (1,2) = (SELECT columnl, column2 FROM t2); SELECT *
FROM tl WHERE ROW(1,2) = (SELECT columnl, column2 FROM t2); Оба эти запроса истинны, если в таблице t2 присутствует строка, в которой columnl = 1и column2 = 2. Выражения (1,2) иROW(1,2) иногда называют конструктором строки. Эти
два выражения эквивалентны. Они вполне корректны и в других
контекстах. Например, следующие два оператора семантически эквивалентны
(несмотря на то, что только второй из них может быть оптимизирован): SELECT * FROM tl WHERE (columnl, column2) = (1,1); SELECT * FROM tl WHERE columnl = 1 AND column2 = 1; Как
правило, конструкторы строки используются для сравнения с подзапросами,
возвращающими два или более столбцов. Например, представленный ниже
запрос выполняет следующую директиву: "найти все строки таблицы tl,
которые есть также и в таблице t2": SELECT columnl,column2,column3 FROM tl WHERE (columnl,column2,column3) IN (SELECT columnl,column2,column3 FROM tl);
Подзапросы в конструкции FROMПодзапросы разрешены и в конструкции FROM оператора SELECT. Их синтаксис выглядит следующим образом: SELECT ... FROM {подзапрос) AS имя ... Конструкция AS имя является
обязательной, поскольку каждая таблица в конструкции FROM должна иметь
имя. Все столбцы в списке подзапроса подзапрос также должны иметь
уникальные имена. Для того чтобы проиллюстрировать это, предположим, что имеется такая таблица: CREATE TABLE tl (si INT, s2 CHAR(5), s3 FLOAT);
Вот как использовать подзапросы в конструкции from для данного примера таблицы:
INSERT INTO tl VALUES (l,'l',1.0); INSERT INTO tl VALUES (2,'2',2.0); SELECT sbl,sb2,sb3 FROM (SELECT si AS sbl, s2 AS sb2, s3*2 AS sb3 FROM tl) AS sb WHERE sbl > 1; Результат: 2, '2', 4.0. А
вот другой пример. Предположим, что вы хотите знать среднее значение
сумм в сгруппированной таблице. Следующий вариант работать не будет:
SELECT AVG(SUM(columnl)) FROM tl GROUP BY columnl; Однако приведенный
ниже запрос выдаст нужную информацию: SELECT AVG(sum_columnl) FROM (SELECT SUM(columnl) AS sum_columnl FROM tl GROUP BY columnl) AS tl; Отметим, что имя столбца, используемое в подзапросе (sum_columnl) распознается во внешнем запросе. На данный момент подзапросы в конструкции FROM не могут быть коррелированными подзапросами. Подзапрос
в конструкции FROM будет выполнен (то есть будет создана временная
таблица) даже для оператора EXPLAIN, потому что запросы верхнего уровня
нуждаются в информации обо всех таблицах на стадии оптимизации.
Ошибки подзапросовПоявился ряд новых типов ошибок, которые
имеют отношение к подзапросам. В этом разделе они сгруппированы вместе,
поскольку их просмотр поможет запомнить некоторые важные моменты.
- Неподдерживаемый синтаксис подзапроса:
ERROR 1235 (ER_NOT_SUPPORTED_YET) SQLSTATE = 42000 Message = "This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" Это
означает, что операторы следующей формы работать не будут, хотя это и
случается только в ранних версиях, подобных MySQL 4.1.1: SELECT * FROM tl WHERE si IN (SELECT s2 FROM t2 ORDER BY si LIMIT 1)
- Неверное число столбцов в подзапросе:
ERROR 1241 (ER_OPERAND_COLUMNF) SQLSTATE = 21000 Message = "Operand should contain 1 column(s)" Эта ошибка возникает в случаях наподобие следующего: SELECT (SELECT columnl, column2 FROM t2) FROM tl; Допустимо
применять подзапросы, которые возвращают несколько столбцов с целью
сравнения. См. раздел 6.1.8.7. Но в других контекстах подзапрос должен
быть скалярным операндом.
- Неверное количество строк в подзапросе:
ERROR 1242 (ER_SUBSELECT_NO_1_ROW)
SQLSTATE = 21000 Message = "Subquery returns more than 1 row" Эта ошибка происходит в операторах вроде приведенного ниже, но только если в таблице t2 имеется более одной строки: SELECT * FROM tl WHERE columnl = (SELECT columnl FROM t2); Такая
ошибка может вдруг обнаружиться в коде, который работал в течение
нескольких лет, по той причине, что кто-то внес модификацию, изменившую
количество строк, возвращаемых подзапросом. Помните, что если нужно
найти любое число строк, а не только одну, то правильная форма запроса
будет выглядеть так: SELECT * FROM tl WHERE columnl = ANY (SELECT columnl FROM t2) ;
- Неправильно используется таблица в подзапросе:
Error 1093 (ER_UPDATE_TABLE_USED) SQLSTATE = HY000 Message = "You can't specify target table 'x' for update in FROM clause" Такая ошибка происходит в случае запроса, подобного представленному ниже: UPDATE tl SET column2 = (SELECT MAX(columnl) FROM tl) ; Вполне
корректно использовать подзапрос для присвоения в операторе UPDATE,
поскольку подзапросы разрешены в операторах UPDATE и DELETE, равно как и
в операторе SELECT. Однако вы не можете использовать одну и ту же
таблицу - в данном случае tl - и в конструкции FROM подзапроса, и в
качестве объекта для манипуляции UPDATE. Обычно сбой подзапроса приводит
к сбою всего внешнего оператора.
Оптимизация подзапросовКогда идет разработка, то долгое время
никаких подсказок оптимизации запросов не применяется. Но вы можете
попробовать некоторые интересные трюки:
- Использовать конструкции подзапросов, которые касаются количества или порядка строк в подзапросе, например:
ELECT * FROM tl WHERE tl.columnl IN (SELECT columnl FROM t2 ORDER BY columnl); SELECT * FROM tl WHERE tl.columnl IN (SELECT DISTINCT columnl FROM t2); SELECT * FROM tl WHERE EXISTS (SELECT * FROM t2 LIMIT 1);
- Заменить соединение подзапросом. Например, используйте такой запрос:
SELECT DISTINCT columnl FROM tl WHERE tl.columnl IN ( SELECT columnl FROM t2); вместо такого: SELECT DISTINCT tl.columnl FROM tl, t2 WHERE tl.columnl = t2.columnl;
- Переместить конструкции из внешнего запроса в подзапрос. Например, используйте такой запрос:
SELECT * FROM tl WHERE Si IK [SELECT si FROM tl UNION ALL SELECT si FROM t2); вместо такого: SELECT * FROM tl WHERE si IN [SELECT si FROM tl) OR si IN (SELECT si FROM t2); И другой пример; используйте следующий запрос: SELECT [SELECT columnl + 5 FROM tl) FROM t2; вместо такого: SELECT [SELECT columnl FROM tl) + 5 FROM t2;
- Применять строковый подзапрос вместо коррелированного, например, такой:
SELECT * FROM tl WHERE (columnl,column2) IN (SELECT columnl,column2 FROM t2); вместо такого: SELECT * FROM tl WHERE EXISTS (SELECT * FROM t2 WHERE t2.columnl=tl. columnl AND t2.column2=tl.column2);
- Применять NOT (a = ANY (...)) вместо а о ALL (...).
- Применятьx = ANY{таблица_содержащая(1,2))
вместо х=1 OR x=2. Использовать = ANY вместо EXISTS. Эти
трюки могут заставить программы работать быстрее или медленнее.
Используя средства MySQL, подобные функции BENCHMARK (), вы можете
получить представление о том, что может помочь в вашей конкретной
ситуации. Не беспокойтесь слишком о трансформации подзапросов в
соединения, если только вам не нужно обеспечить совместимость с
версиями MySQL до 4.1, которые подзапросы не поддерживали. Ниже представлены некоторые меры по оптимизации, которые принимает сам MySQL.
- MySQL выполняет некоррелированные подзапросы только один
раз. Используйте EXPLAIN, чтобы убедиться, что ваш запрос
некоррелированный.
- MySQL перезаписывает подзапросы IN/ALL/ANY/SOME, пытаясь получить выигрыш от использования индексов.
- MySQL заменяет подзапросы следующей формы функциями
просмотра индексов, которые конструкция EXPLAIN будет описывать как
специальный тип соединения:
... IN (SELECT индексированный_столбец FROM одиночная таблица ...) • MySQL
заменяет выражения следующей формы выражениями с применением MIN () или
МАХ (), если только не вовлечены NULL-значения и пустые наборы: значение {ALL|ANY|SOME} {> I < I >= I <=} {не-коррелированный-подзапрос) например, следующая конструкция WHERE: WHERE 5 > ALL (SELECT x FROM t) может быть преобразована оптимизатором в такую: WHERE 5 > (SELECT MAX(x) FROM t)
В руководстве по внутреннему устройству MySQL ("MySQL Internals
Manual") имеется глава "How MySQL Transforms Subqueries" ("Как MySQL
трансформирует подзапросы"). Вы можете получить этот документ, выгрузив
исходный дистрибутив MySQL и найдя файл internals. text в каталоге
Docs.
Замена подзапросов соединениями для ранних версий MySQLДо
MySQL 4.1 поддерживались только вложенные запросы для форм INSERT...
SELECT... и replace. . .SELECT... Конструкция IN() может использоваться в
других контекстах для проверки вхождения в множество значений. Часто можно переписать запрос без подзапроса: SELECT * FROM tl WHERE id IN (SELECT id FROM t2); Это можно заменить на: SELECT DISTINCT tl.* FROM tl,t2 WHERE tl.id=t2.id; Следующие запросы: SELECT * FROM tl WHERE id NOT IN (SELECT id FROM t2); SELECT * FROM tl WHERE NOT EXISTS (SELECT id FROM t2 WHERE tl.id=t2.id); могут быть переписаны так: SELECT tablel.* FROM tablel LEFT JOIN table2 ON tablel.id=table2. id WHERE table2.id IS NULL; LEFT
[OUTER] JOIN может оказаться быстрее, чем эквивалентный подзапрос,
поскольку сервер может его лучше оптимизировать, и это касается не
только одного MySQL. До выхода стандарта SQL-92 внешние соединения не
допускались, поэтому подзапросы были единственным способом делать
определенные вещи в эти прошедшие времена. Сегодня сервер MySQL и многие
другие современные системы баз данных предлагают широкий диапазон типов
внешних соединений. Для более сложных подзапросов часто можно
создавать временные таблицы, в которых хранятся промежуточные
результаты. Однако в некоторых случаях такой способ неприемлем. Наиболее
часто это случается с операторами DELETE, для которых стандарт SQL не
поддерживает соединений (кроме как в подзапросах). В этой ситуации
возможны три пути:
- Первый путь — обновить сервер до версии MySQL 4.1, которая поддерживает подзапросы в операторах DELETE.
- Второй путь - использовать процедурный язык программирования
(такой как Perl или РНР), чтобы выполнить запрос SELECT для получения
значений первичных ключей записей, подлежащих удалению, а затем
использовать эти значения для построения оператора DELETE (DELETE FROM .
. . WHERE ключевой_столбец IN {ключ1,ключ2, ...)).
- Третий путь - использовать интерактивный SQL для
автоматического построения набора операторов delete с использованием
MySQL-расширения CONCATO (вместо стандартного оператора | |). Например:
SELECT CONCAT('DELETE FROM tablel WHERE pkid = ', "'", tablel.pkid, ""', ';') FROM tablel, table2 WHERE tablel.columnl = table2.column2;
Этот запрос можно поместить в файл сценария, указать его в качестве
входного для одного экземпляра программы mysql и перенаправить его
выходной поток на вход другому экземпляру mysql: shell> mysql -skip-column-names mydb < my script. sql | mysql mydb Сервер MySQL 4.0
поддерживает многотабличные операторы DELETE, которые могут
использоваться для эффективного удаления строк на основе информации из
одной или даже многих таблиц одновременно. Многотабличные операторы
UPDATE также поддерживаются в MySQL 4.O
|