mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-27 10:02:01 +00:00
240 lines
18 KiB
Markdown
240 lines
18 KiB
Markdown
# Операторы IN {#select-in-operators}
|
||
|
||
Операторы `IN`, `NOT IN`, `GLOBAL IN`, `GLOBAL NOT IN` рассматриваются отдельно, так как их функциональность достаточно богатая.
|
||
|
||
В качестве левой части оператора, может присутствовать как один столбец, так и кортеж.
|
||
|
||
Примеры:
|
||
|
||
``` sql
|
||
SELECT UserID IN (123, 456) FROM ...
|
||
SELECT (CounterID, UserID) IN ((34, 123), (101500, 456)) FROM ...
|
||
```
|
||
|
||
Если слева стоит один столбец, входящий в индекс, а справа - множество констант, то при выполнении запроса, система воспользуется индексом.
|
||
|
||
Не перечисляйте слишком большое количество значений (миллионы) явно. Если множество большое - лучше загрузить его во временную таблицу (например, смотрите раздел [Внешние данные для обработки запроса](../../engines/table-engines/special/external-data.md)), и затем воспользоваться подзапросом.
|
||
|
||
В качестве правой части оператора может быть множество константных выражений, множество кортежей с константными выражениями (показано в примерах выше), а также имя таблицы или подзапрос SELECT в скобках.
|
||
|
||
Если типы данных в левой и правой частях подзапроса `IN` различаются, ClickHouse преобразует значение в левой части к типу данных из правой части. Преобразование выполняется по аналогии с функцией [accurateCastOrNull](../functions/type-conversion-functions.md#type_conversion_function-accurate-cast_or_null), т.е. тип данных становится [Nullable](../../sql-reference/data-types/nullable.md), а если преобразование не может быть выполнено, возвращается значение [NULL](../../sql-reference/syntax.md#null-literal).
|
||
|
||
**Пример**
|
||
|
||
Запрос:
|
||
|
||
``` sql
|
||
SELECT '1' IN (SELECT 1);
|
||
```
|
||
|
||
Результат:
|
||
|
||
``` text
|
||
┌─in('1', _subquery49)─┐
|
||
│ 1 │
|
||
└──────────────────────┘
|
||
```
|
||
|
||
Если в качестве правой части оператора указано имя таблицы (например, `UserID IN users`), то это эквивалентно подзапросу `UserID IN (SELECT * FROM users)`. Это используется при работе с внешними данными, отправляемым вместе с запросом. Например, вместе с запросом может быть отправлено множество идентификаторов посетителей, загруженное во временную таблицу users, по которому следует выполнить фильтрацию.
|
||
|
||
Если в качестве правой части оператора, указано имя таблицы, имеющий движок Set (подготовленное множество, постоянно находящееся в оперативке), то множество не будет создаваться заново при каждом запросе.
|
||
|
||
В подзапросе может быть указано более одного столбца для фильтрации кортежей.
|
||
Пример:
|
||
|
||
``` sql
|
||
SELECT (CounterID, UserID) IN (SELECT CounterID, UserID FROM ...) FROM ...
|
||
```
|
||
|
||
Типы столбцов слева и справа оператора IN, должны совпадать.
|
||
|
||
Оператор IN и подзапрос могут встречаться в любой части запроса, в том числе в агрегатных и лямбда функциях.
|
||
Пример:
|
||
|
||
``` sql
|
||
SELECT
|
||
EventDate,
|
||
avg(UserID IN
|
||
(
|
||
SELECT UserID
|
||
FROM test.hits
|
||
WHERE EventDate = toDate('2014-03-17')
|
||
)) AS ratio
|
||
FROM test.hits
|
||
GROUP BY EventDate
|
||
ORDER BY EventDate ASC
|
||
```
|
||
|
||
``` text
|
||
┌──EventDate─┬────ratio─┐
|
||
│ 2014-03-17 │ 1 │
|
||
│ 2014-03-18 │ 0.807696 │
|
||
│ 2014-03-19 │ 0.755406 │
|
||
│ 2014-03-20 │ 0.723218 │
|
||
│ 2014-03-21 │ 0.697021 │
|
||
│ 2014-03-22 │ 0.647851 │
|
||
│ 2014-03-23 │ 0.648416 │
|
||
└────────────┴──────────┘
|
||
```
|
||
|
||
за каждый день после 17 марта считаем долю хитов, сделанных посетителями, которые заходили на сайт 17 марта.
|
||
Подзапрос в секции IN на одном сервере всегда выполняется только один раз. Зависимых подзапросов не существует.
|
||
|
||
## Обработка NULL {#in-null-processing}
|
||
|
||
При обработке запроса оператор IN будет считать, что результат операции с [NULL](../syntax.md#null-literal) всегда равен `0`, независимо от того, находится `NULL` в правой или левой части оператора. Значения `NULL` не входят ни в какое множество, не соответствуют друг другу и не могут сравниваться, если [transform_null_in = 0](../../operations/settings/settings.md#transform_null_in).
|
||
|
||
Рассмотрим для примера таблицу `t_null`:
|
||
|
||
``` text
|
||
┌─x─┬────y─┐
|
||
│ 1 │ ᴺᵁᴸᴸ │
|
||
│ 2 │ 3 │
|
||
└───┴──────┘
|
||
```
|
||
|
||
При выполнении запроса `SELECT x FROM t_null WHERE y IN (NULL,3)` получим следующий результат:
|
||
|
||
``` text
|
||
┌─x─┐
|
||
│ 2 │
|
||
└───┘
|
||
```
|
||
|
||
Видно, что строка, в которой `y = NULL`, выброшена из результатов запроса. Это произошло потому, что ClickHouse не может решить входит ли `NULL` в множество `(NULL,3)`, возвращает результат операции `0`, а `SELECT` выбрасывает эту строку из финальной выдачи.
|
||
|
||
``` sql
|
||
SELECT y IN (NULL, 3)
|
||
FROM t_null
|
||
```
|
||
|
||
``` text
|
||
┌─in(y, tuple(NULL, 3))─┐
|
||
│ 0 │
|
||
│ 1 │
|
||
└───────────────────────┘
|
||
```
|
||
|
||
## Распределённые подзапросы {#select-distributed-subqueries}
|
||
|
||
Существует два варианта IN-ов с подзапросами (аналогично для JOIN-ов): обычный `IN` / `JOIN` и `GLOBAL IN` / `GLOBAL JOIN`. Они отличаются способом выполнения при распределённой обработке запроса.
|
||
|
||
!!! attention "Attention"
|
||
Помните, что алгоритмы, описанные ниже, могут работать иначе в зависимости от [настройки](../../operations/settings/settings.md) `distributed_product_mode`.
|
||
|
||
При использовании обычного IN-а, запрос отправляется на удалённые серверы, и на каждом из них выполняются подзапросы в секциях `IN` / `JOIN`.
|
||
|
||
При использовании `GLOBAL IN` / `GLOBAL JOIN-а`, сначала выполняются все подзапросы для `GLOBAL IN` / `GLOBAL JOIN-ов`, и результаты складываются во временные таблицы. Затем эти временные таблицы передаются на каждый удалённый сервер, и на них выполняются запросы, с использованием этих переданных временных данных.
|
||
|
||
Если запрос не распределённый, используйте обычный `IN` / `JOIN`.
|
||
|
||
Следует быть внимательным при использовании подзапросов в секции `IN` / `JOIN` в случае распределённой обработки запроса.
|
||
|
||
Рассмотрим это на примерах. Пусть на каждом сервере кластера есть обычная таблица **local_table**. Пусть также есть таблица **distributed_table** типа **Distributed**, которая смотрит на все серверы кластера.
|
||
|
||
При запросе к распределённой таблице **distributed_table**, запрос будет отправлен на все удалённые серверы, и на них будет выполнен с использованием таблицы **local_table**.
|
||
|
||
Например, запрос
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM distributed_table
|
||
```
|
||
|
||
будет отправлен на все удалённые серверы в виде
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM local_table
|
||
```
|
||
|
||
, выполнен параллельно на каждом из них до стадии, позволяющей объединить промежуточные результаты; затем промежуточные результаты вернутся на сервер-инициатор запроса, будут на нём объединены, и финальный результат будет отправлен клиенту.
|
||
|
||
Теперь рассмотрим запрос с IN-ом:
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34)
|
||
```
|
||
|
||
- расчёт пересечения аудиторий двух сайтов.
|
||
|
||
Этот запрос будет отправлен на все удалённые серверы в виде
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34)
|
||
```
|
||
|
||
То есть, множество в секции IN будет собрано на каждом сервере независимо, только по тем данным, которые есть локально на каждом из серверов.
|
||
|
||
Это будет работать правильно и оптимально, если вы предусмотрели такой случай, и раскладываете данные по серверам кластера таким образом, чтобы данные одного UserID-а лежали только на одном сервере. В таком случае все необходимые данные будут присутствовать на каждом сервере локально. В противном случае результат будет посчитан неточно. Назовём этот вариант запроса «локальный IN».
|
||
|
||
Чтобы исправить работу запроса, когда данные размазаны по серверам кластера произвольным образом, можно было бы указать **distributed_table** внутри подзапроса. Запрос будет выглядеть так:
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
|
||
```
|
||
|
||
Этот запрос будет отправлен на все удалённые серверы в виде
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
|
||
```
|
||
|
||
На каждом удалённом сервере начнёт выполняться подзапрос. Так как в подзапросе используется распределённая таблица, то подзапрос будет, на каждом удалённом сервере, снова отправлен на каждый удалённый сервер, в виде
|
||
|
||
``` sql
|
||
SELECT UserID FROM local_table WHERE CounterID = 34
|
||
```
|
||
|
||
Например, если у вас кластер из 100 серверов, то выполнение всего запроса потребует 10 000 элементарных запросов, что, как правило, является неприемлемым.
|
||
|
||
В таких случаях всегда следует использовать GLOBAL IN вместо IN. Рассмотрим его работу для запроса
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID GLOBAL IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
|
||
```
|
||
|
||
На сервере-инициаторе запроса будет выполнен подзапрос
|
||
|
||
``` sql
|
||
SELECT UserID FROM distributed_table WHERE CounterID = 34
|
||
```
|
||
|
||
, и результат будет сложен во временную таблицу в оперативке. Затем запрос будет отправлен на каждый удалённый сервер в виде
|
||
|
||
``` sql
|
||
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID GLOBAL IN _data1
|
||
```
|
||
|
||
, и вместе с запросом, на каждый удалённый сервер будет отправлена временная таблица `_data1` (имя временной таблицы - implementation defined).
|
||
|
||
Это гораздо более оптимально, чем при использовании обычного IN. Но при этом, следует помнить о нескольких вещах:
|
||
|
||
1. При создании временной таблицы данные не уникализируются. Чтобы уменьшить объём передаваемых по сети данных, укажите в подзапросе DISTINCT (для обычного IN-а этого делать не нужно).
|
||
2. Временная таблица будет передана на все удалённые серверы. Передача не учитывает топологию сети. Например, если 10 удалённых серверов расположены в удалённом относительно сервера-инициатора запроса дата-центре, то по каналу в удалённый дата-центр данные будет переданы 10 раз. Старайтесь не использовать большие множества при использовании GLOBAL IN.
|
||
3. При передаче данных на удалённые серверы не настраивается ограничение использования сетевой полосы. Вы можете перегрузить сеть.
|
||
4. Старайтесь распределять данные по серверам так, чтобы в GLOBAL IN-ах не было частой необходимости.
|
||
5. Если в GLOBAL IN есть частая необходимость, то спланируйте размещение кластера ClickHouse таким образом, чтобы в каждом дата-центре была хотя бы одна реплика каждого шарда, и среди них была быстрая сеть - чтобы запрос целиком можно было бы выполнить, передавая данные в пределах одного дата-центра.
|
||
|
||
В секции `GLOBAL IN` также имеет смысл указывать локальную таблицу - в случае, если эта локальная таблица есть только на сервере-инициаторе запроса, и вы хотите воспользоваться данными из неё на удалённых серверах.
|
||
|
||
### Распределенные подзапросы и max_parallel_replicas {#max_parallel_replica-subqueries}
|
||
|
||
Когда настройка max_parallel_replicas больше чем 1, распределенные запросы преобразуются. Например, следующий запрос:
|
||
|
||
```sql
|
||
SELECT CounterID, count() FROM distributed_table_1 WHERE UserID IN (SELECT UserID FROM local_table_2 WHERE CounterID < 100)
|
||
SETTINGS max_parallel_replicas=3
|
||
```
|
||
|
||
преобразуются на каждом сервере в
|
||
|
||
```sql
|
||
SELECT CounterID, count() FROM local_table_1 WHERE UserID IN (SELECT UserID FROM local_table_2 WHERE CounterID < 100)
|
||
SETTINGS parallel_replicas_count=3, parallel_replicas_offset=M
|
||
```
|
||
|
||
где M значение между 1 и 3 зависящее от того на какой реплике выполняется локальный запрос. Эти параметры влияют на каждую таблицу семейства MergeTree в запросе и имеют тот же эффект, что и применение `SAMPLE 1/3 OFFSET (M-1)/3` для каждой таблицы.
|
||
|
||
Поэтому применение настройки max_parallel_replicas даст корректные результаты если обе таблицы имеют одинаковую схему репликации и семплированы по UserID выражению от UserID. В частности, если local_table_2 не имеет семплирующего ключа, будут получены неверные результаты. Тоже правило применяется для JOIN.
|
||
|
||
Один из способов избежать этого, если local_table_2 не удовлетворяет требованиям, использовать `GLOBAL IN` или `GLOBAL JOIN`.
|