Подзапросы
Обзор
Подзапрос — это SQL-запрос, вложенный в другой (родительский) запрос или подзапрос. Сначала выполняется подзапрос, затем полученный результат передается родительскому запросу. Таким образом, подзапросы могут быть полезны, когда для выполнения запроса требуется результат другого запроса. Например, подзапросы можно использовать в запросах SELECT
: в выражениях FROM
и JOIN
для определения источника, из которого должны читаться данные, или в выражении WHERE
для фильтрации данных.
От типа возвращаемого подзапросом значения зависит то, для каких операций можно использовать подзапрос. Результатом выполнения подзапроса может быть:
-
Скалярное значение, то есть значение одного столбца из одной строки или одно вычисленное значение (например, значение агрегатной функции — сумма, среднее/минимальное/максимальное значение столбца). Такой подзапрос можно использовать с операторами сравнения
>
,<
,=
и др. (см. Comparison Operators в документации ClickHouse), передавать как аргумент функции или как значение столбца в оператореSELECT
. -
Табличное значение, то есть несколько строк (возможно ноль строк). Такой подзапрос можно использовать с операторами для работы с множествами (
IN
,NOT IN
,ANY
,ALL
,EXISTS
) — в этом случае будет проверяться вхождение строки родительского запроса в результат, возвращаемый подзапросом. Также табличное значение подзапроса можно использовать в выраженииFROM
илиJOIN
, чтобы указать таблицу-источник для чтения данных.
ПРИМЕЧАНИЕ
|
Ниже в статье приведены различные варианты использования подзапросов в запросах SELECT
.
Чтобы воспроизвести приведенные в данной статье примеры, создайте предварительно таблицы:
-
employees
CREATE TABLE employees ( id UInt16, full_name String, hire_date Date, position Enum('c developer' = 1, 'python developer' = 2, 'java developer' = 3, 'frontend developer' = 4, 'test automation engineer' = 5, 'manual testing engineer' = 6, 'load testing engineer' = 7, 'support engineer 1st line' = 8, 'support engineer 2nd line' = 9), department Enum('development' = 10, 'qa' = 20, 'tech support' = 30), location String, email String) ENGINE = MergeTree ORDER BY id;
INSERT INTO employees VALUES (1, 'James Wilson', '2020-11-23', 1, 10, 'New York', 'jwilson@example.com'), (2, 'Mary Burton', '2023-05-06', 1, 10, 'Chicago', 'mburton@example.com'), (3, 'Patricia Robinson', '2021-10-11', 2, 10, 'Los Angeles', 'probinson@example.com'), (4, 'Robert Gray', '2019-07-03', 2, 10, 'Seattle', 'rgray@example.com'), (5, 'Elizabeth Tucker', '2024-02-03', 4, 10, 'Chicago', 'etucker@example.com'), (6, 'Joseph Lewis', '2023-04-05', 4, 10, 'New York', 'jlewis@example.com'), (7, 'Paul Brown', '2024-02-03', 3, 10, 'Chicago', 'pbrown@example.com'), (8, 'Andrew Clarke', '2024-11-23', 3, 10, 'Los Angeles', 'aclarke@example.com') (9, 'William Ferguson', '2023-11-12', 5, 20, 'Chicago', 'wferguson@example.com'), (10, 'Linda Black', '2021-07-20', 6, 20, 'New York', 'lblack@example.com'), (11, 'David Green', '2019-10-10', 7, 20, 'Chicago', 'dgreen@example.com'), (12, 'Daniel Gray', '2020-08-03', 7, 20, 'New York', 'dgray@example.com'), (13, 'Mark Armstrong', '2020-04-05', 8, 30, 'New York', 'marmstrong@example.com'), (14, 'Donald Carter', '2021-07-07', 8, 30, 'Seattle', 'dcarter@example.com'), (15, 'Elizabeth Collins', '2022-01-22', 9, 30, 'Chicago', 'ecollins@example.com');
┌─id─┬─full_name─────────┬──hire_date─┬─position──────────────────┬─department───┬─location────┬─email──────────────────┐ 1. │ 1 │ James Wilson │ 2020-11-23 │ c developer │ development │ New York │ jwilson@example.com │ 2. │ 2 │ Mary Burton │ 2023-05-06 │ c developer │ development │ Chicago │ mburton@example.com │ 3. │ 3 │ Patricia Robinson │ 2021-10-11 │ python developer │ development │ Los Angeles │ probinson@example.com │ 4. │ 4 │ Robert Gray │ 2019-07-03 │ python developer │ development │ Seattle │ rgray@example.com │ 5. │ 5 │ Elizabeth Tucker │ 2024-02-03 │ frontend developer │ development │ Chicago │ etucker@example.com │ 6. │ 6 │ Joseph Lewis │ 2023-04-05 │ frontend developer │ development │ New York │ jlewis@example.com │ 7. │ 7 │ Paul Brown │ 2024-02-03 │ java developer │ development │ Chicago │ pbrown@example.com │ 8. │ 8 │ Andrew Clarke │ 2024-11-23 │ java developer │ development │ Los Angeles │ aclarke@example.com │ 9. │ 9 │ William Ferguson │ 2023-11-12 │ test automation engineer │ qa │ Chicago │ wferguson@example.com │ 10. │ 10 │ Linda Black │ 2021-07-20 │ manual testing engineer │ qa │ New York │ lblack@example.com │ 11. │ 11 │ David Green │ 2019-10-10 │ load testing engineer │ qa │ Chicago │ dgreen@example.com │ 12. │ 12 │ Daniel Gray │ 2020-08-03 │ load testing engineer │ qa │ New York │ dgray@example.com │ 13. │ 13 │ Mark Armstrong │ 2020-04-05 │ support engineer 1st line │ tech support │ New York │ marmstrong@example.com │ 14. │ 14 │ Donald Carter │ 2021-07-07 │ support engineer 1st line │ tech support │ Seattle │ dcarter@example.com │ 15. │ 15 │ Elizabeth Collins │ 2022-01-22 │ support engineer 2nd line │ tech support │ Chicago │ ecollins@example.com │ └────┴───────────────────┴────────────┴───────────────────────────┴──────────────┴─────────────┴────────────────────────┘
-
employee_bonus_oct_24
CREATE TABLE employee_bonus_oct_24 (employee_id UInt16, bonus UInt32) ENGINE = MergeTree ORDER BY employee_id;
INSERT INTO employee_bonus_oct_24 VALUES (1, 700), (2, 450), (3, 500), (4, 950), (5, 370), (6, 600),(7, 1000), (8, 950), (9, 770), (10, 350), (11, 500), (12, 670), (13, 600), (14, 600), (15, 480);
┌─employee_id─┬─bonus─┐ 1. │ 1 │ 700 │ 2. │ 2 │ 450 │ 3. │ 3 │ 500 │ 4. │ 4 │ 950 │ 5. │ 5 │ 370 │ 6. │ 6 │ 600 │ 7. │ 7 │ 1000 │ 8. │ 8 │ 950 │ 9. │ 9 │ 770 │ 10. │ 10 │ 350 │ 11. │ 11 │ 500 │ 12. │ 12 │ 670 │ 13. │ 13 │ 600 │ 14. │ 14 │ 600 │ 15. │ 15 │ 480 │ └─────────────┴───────┘
-
employee_bonus_oct_24_full
CREATE TABLE employee_bonus_oct_24_full ( employee_id UInt16, full_name String, position String, department String, bonus UInt32) ENGINE = MergeTree ORDER BY employee_id;
INSERT INTO employee_bonus_oct_24_full SELECT employee_bonus_oct_24.employee_id, employees.full_name, employees.position, employees.department, employee_bonus_oct_24.bonus FROM employee_bonus_oct_24 INNER JOIN employees ON employee_bonus_oct_24.employee_id = employees.id;
┌─employee_id─┬─full_name─────────┬─position──────────────────┬─department───┬─bonus─┐ 1. │ 1 │ James Wilson │ c developer │ development │ 700 │ 2. │ 2 │ Mary Burton │ c developer │ development │ 450 │ 3. │ 3 │ Patricia Robinson │ python developer │ development │ 500 │ 4. │ 4 │ Robert Gray │ python developer │ development │ 950 │ 5. │ 5 │ Elizabeth Tucker │ frontend developer │ development │ 370 │ 6. │ 6 │ Joseph Lewis │ frontend developer │ development │ 600 │ 7. │ 7 │ Paul Brown │ java developer │ development │ 1000 │ 8. │ 8 │ Andrew Clarke │ java developer │ development │ 950 │ 9. │ 9 │ William Ferguson │ test automation engineer │ qa │ 770 │ 10. │ 10 │ Linda Black │ manual testing engineer │ qa │ 350 │ 11. │ 11 │ David Green │ load testing engineer │ qa │ 500 │ 12. │ 12 │ Daniel Gray │ load testing engineer │ qa │ 670 │ 13. │ 13 │ Mark Armstrong │ support engineer 1st line │ tech support │ 600 │ 14. │ 14 │ Donald Carter │ support engineer 1st line │ tech support │ 600 │ 15. │ 15 │ Elizabeth Collins │ support engineer 2nd line │ tech support │ 480 │ └─────────────┴───────────────────┴───────────────────────────┴──────────────┴───────┘
Подзапросы в FROM
В качестве источника данных для чтения запросом SELECT
в выражении FROM
можно указывать не только имя таблицы или табличную функцию, но и подзапрос:
SELECT <expression> FROM (<subquery>) [AS <alias>]
В отличие от стандартного SQL, после подзапроса не обязательно указывать синоним (AS <alias>
). Это можно сделать для совместимости, но указанное имя нигде не будет использоваться.
В следующем запросе подзапрос в выражении FROM
возвращает премии сотрудников отдела development
за октябрь и оценку премии каждого сотрудника относительно размера максимальной премии в компании:
SELECT employee, bonus, relative_to_max
FROM
(
SELECT
full_name AS employee,
bonus,
round((bonus/(SELECT max(bonus) FROM employee_bonus_oct_24_full)), 2) AS relative_to_max
FROM employee_bonus_oct_24_full
WHERE department = 'development'
)
WHERE relative_to_max < 0.5;
┌─employee─────────┬─bonus─┬─relative_to_max─┐ 1. │ Mary Burton │ 450 │ 0.45 │ 2. │ Elizabeth Tucker │ 370 │ 0.37 │ └──────────────────┴───────┴─────────────────┘
Подзапросы в JOIN
В запросе SELECT
с оператором JOIN не оптимизируется порядок выполнения соединения по отношению к другим стадиям запроса. Сначала выполняется соединение (поиск в правой таблице), а затем — фильтрация WHERE
и агрегация. Чтобы явно указать порядок вычислений и повысить производительность запроса, рекомендуется применять фильтр как можно раньше — для этого можно соединять подзапросы.
Например, в следующем запросе сначала вычисляются подзапросы в выражениях FROM
(количество сотрудников в каждом отделе) и LEFT ANY JOIN
(количество сотрудников в каждом отделе, получивших премию меньше 500) с группировкой и фильтрацией, а затем результаты этих подзапросов соединяются:
SELECT department, number_of_employees, bonus_less_500
FROM
(
SELECT department, count() AS number_of_employees
FROM employees
GROUP BY department
)
LEFT ANY JOIN
(
SELECT department, count() AS bonus_less_500
FROM employee_bonus_oct_24_full
WHERE bonus < 500
GROUP BY department
) USING (department)
SETTINGS joined_subquery_requires_alias = 0;
┌─department───┬─number_of_employees─┬─bonus_less_500─┐ 1. │ development │ 8 │ 2 │ 2. │ qa │ 4 │ 1 │ 3. │ tech support │ 3 │ 1 │ └──────────────┴─────────────────────┴────────────────┘
Подзапросы в WHERE
Подзапросы часто применяются в выражении WHERE
запросов SELECT
для фильтрации данных. Как упоминалось выше, для определения условий фильтрации данных подзапросы можно использовать с различными операторами сравнения:
-
подзапросы, возвращающие скалярные значения — до или после операторов сравнения
=
(==
),!=
(<>
),>
,>=
,<
,<=
. Например, следующий запрос находит всех сотрудников, получивших в октябре премию выше средней:SELECT full_name AS employee, position, department FROM employee_bonus_oct_24_full WHERE bonus > (SELECT avg(bonus) FROM employee_bonus_oct_24_full);
┌─employee─────────┬─position─────────────────┬─department──┐ 1. │ James Wilson │ c developer │ development │ 2. │ Robert Gray │ python developer │ development │ 3. │ Paul Brown │ java developer │ development │ 4. │ Andrew Clarke │ java developer │ development │ 5. │ William Ferguson │ test automation engineer │ qa │ 6. │ Daniel Gray │ load testing engineer │ qa │ └──────────────────┴──────────────────────────┴─────────────┘
-
подзапросы, возвращающие наборы строк данных — с операторами
IN
,NOT IN
,ANY
,ALL
,EXISTS
, которые позволяют проверять наличие конкретной строки в наборе строк. Примеры использования подзапросов с этими операторами приведены в разделах ниже.
IN
Синтаксис оператора IN
с использованием подзапроса:
<IN_expression> IN (<IN_subquery>)
Оператор IN
вычисляет выражение IN_expression
, сравнивает его со значениями во всех строках результата подзапроса IN_subquery
и возвращает:
-
1
— если в результатах подзапросаIN_subquery
найдена строка, равнаяIN_expression
; -
0
— в следующих случаях:-
в результатах подзапроса
IN_subquery
не найдено ни одной строки, равнойIN_expression
; -
IN_subquery
не возвращает ни одной строки; -
в правой или левой части оператора
IN
присутствуетNULL
при условии, что параметр transform_null_in установлен в0
(по умолчанию).
-
В следующем запросе используется оператор IN
, чтобы вывести информацию о выплаченных в октябре премиях только для сотрудников, проживающих в Нью-Йорке:
SELECT full_name AS employee, bonus FROM employee_bonus_oct_24_full
WHERE employee_id IN (SELECT id FROM employees WHERE location='New York');
┌─employee───────┬─bonus─┐ 1. │ James Wilson │ 700 │ 2. │ Joseph Lewis │ 600 │ 3. │ Linda Black │ 350 │ 4. │ Daniel Gray │ 670 │ 5. │ Mark Armstrong │ 600 │ └────────────────┴───────┘
ПРИМЕЧАНИЕ
|
NOT IN
Синтаксис оператора NOT IN
с подзапросом:
<NOT_IN_expression> NOT IN (<NOT_IN_subquery>)
Выражение NOT_IN_expression
вычисляется и сравнивается с каждой строкой результата подзапроса NOT_IN_subquery
. Возвращаемое значение:
-
1
— если в результатахNOT_IN_expression
иNOT_IN_subquery
не найдено совпадений, или подзапрос не возвращает ни одной строки; -
0
— если в результатах подзапросаNOT_IN_subquery
есть хоть одна строка, равнаяNOT_IN_expression
.
Например, следующий запрос выводит данные о премиях сотрудников, проживающих не в Нью-Йорке:
SELECT full_name AS employee, bonus FROM employee_bonus_oct_24_full
WHERE employee_id NOT IN (SELECT id FROM employees WHERE location='New York');
┌─employee──────────┬─bonus─┐ 1. │ Mary Burton │ 450 │ 2. │ Patricia Robinson │ 500 │ 3. │ Robert Gray │ 950 │ 4. │ Elizabeth Tucker │ 370 │ 5. │ Paul Brown │ 1000 │ 6. │ Andrew Clarke │ 950 │ 7. │ William Ferguson │ 770 │ 8. │ David Green │ 500 │ 9. │ Donald Carter │ 600 │ 10. │ Elizabeth Collins │ 480 │ └───────────────────┴───────┘
ANY
Синтаксис оператора ANY
с подзапросом:
<ANY_expression> <operator> ANY (<ANY_subquery>)
где operator
может быть =
, <>
, <
, <=
, >
, >=
.
Оператор ANY
выполняет сравнение результатов выражения ANY_expression
и подзапроса ANY_subquery
с помощью оператора operator
и возвращает 1
, если хотя бы один элемент из результата подзапроса удовлетворяет указанному условию.
Например, следующий запрос находит сотрудников не из отдела development
, премия которых в октябре больше чем хотя бы у одного сотрудника отдела development
в этом же месяце:
SELECT full_name AS employee, bonus, department FROM employee_bonus_oct_24_full
WHERE bonus > ANY (SELECT bonus FROM employee_bonus_oct_24_full WHERE department='development') AND department!='development';
┌─employee──────────┬─bonus─┬─department───┐ 1. │ William Ferguson │ 770 │ qa │ 2. │ David Green │ 500 │ qa │ 3. │ Daniel Gray │ 670 │ qa │ 4. │ Mark Armstrong │ 600 │ tech support │ 5. │ Donald Carter │ 600 │ tech support │ 6. │ Elizabeth Collins │ 480 │ tech support │ └───────────────────┴───────┴──────────────┘
Сравнение = ANY
эквивалентно оператору IN. Если приведенный выше пример с IN
переписать следующим образом, результат запроса будет тем же:
SELECT full_name AS employee, bonus FROM employee_bonus_oct_24_full
WHERE employee_id = ANY (SELECT id FROM employees WHERE location='New York');
┌─employee───────┬─bonus─┐ 1. │ James Wilson │ 700 │ 2. │ Joseph Lewis │ 600 │ 3. │ Linda Black │ 350 │ 4. │ Daniel Gray │ 670 │ 5. │ Mark Armstrong │ 600 │ └────────────────┴───────┘
ALL
Синтаксис оператора ALL
с подзапросом:
<ALL_expression> <operator> ALL (<ALL_subquery>)
где operator
может быть =
, <>
, <
, <=
, >
, >=
.
Оператор ALL
сравнивает значение выражения ALL_expression
с каждым значением в наборе, полученным подзапросом ALL_subquery
, и возвращает 1
, если условие сравнения верно для всех значений в наборе.
Например, следующий запрос возвращает список сотрудников, размер премий которых меньше чем у любого сотрудника отдела tech support
:
SELECT full_name AS employee, bonus, department FROM employee_bonus_oct_24_full
WHERE bonus < ALL (SELECT bonus FROM employee_bonus_oct_24_full WHERE department='tech support');
┌─employee─────────┬─bonus─┬─department──┐ 1. │ Mary Burton │ 450 │ development │ 2. │ Elizabeth Tucker │ 370 │ development │ 3. │ Linda Black │ 350 │ qa │ └──────────────────┴───────┴─────────────┘
Оператор != ALL
эквивалентен NOT IN. Приведенный выше пример с NOT IN
можно переписать следующим образом:
SELECT full_name AS employee, bonus FROM employee_bonus_oct_24_full
WHERE employee_id != ALL (SELECT id FROM employees WHERE location='New York');
Результат будет такой же:
┌─employee──────────┬─bonus─┐ 1. │ Mary Burton │ 450 │ 2. │ Patricia Robinson │ 500 │ 3. │ Robert Gray │ 950 │ 4. │ Elizabeth Tucker │ 370 │ 5. │ Paul Brown │ 1000 │ 6. │ Andrew Clarke │ 950 │ 7. │ William Ferguson │ 770 │ 8. │ David Green │ 500 │ 9. │ Donald Carter │ 600 │ 10. │ Elizabeth Collins │ 480 │ └───────────────────┴───────┘
EXISTS
Синтаксис оператора EXISTS
:
EXISTS (<subquery>)
Оператор EXISTS
проверяет, сколько строк содержит результат выполнения подзапроса. Если результат пустой, то оператор возвращает 0
. В остальных случаях оператор возвращает 1
.
ВНИМАНИЕ
Ссылки на таблицы или столбцы родительского запроса не поддерживаются в подзапросе.
|
Например, следующий запрос выведет данные таблицы employees
, только если в ней есть хотя бы один сотрудник, проживающий в Чикаго:
SELECT full_name AS employee, location AS city
FROM employees
WHERE EXISTS (SELECT * FROM employees WHERE location='Chicago');
┌─employee──────────┬─city────────┐ 1. │ James Wilson │ New York │ 2. │ Mary Burton │ Chicago │ 3. │ Patricia Robinson │ Los Angeles │ 4. │ Robert Gray │ Seattle │ 5. │ Elizabeth Tucker │ Chicago │ 6. │ Joseph Lewis │ New York │ 7. │ Paul Brown │ Chicago │ 8. │ Andrew Clarke │ Los Angeles │ 9. │ William Ferguson │ Chicago │ 10. │ Linda Black │ New York │ 11. │ David Green │ Chicago │ 12. │ Daniel Gray │ New York │ 13. │ Mark Armstrong │ New York │ 14. │ Donald Carter │ Seattle │ 15. │ Elizabeth Collins │ Chicago │ └───────────────────┴─────────────┘