Операторы IN
Операторы IN
, NOT IN
, GLOBAL IN
, GLOBAL NOT IN
рассматриваются отдельно, так как их функциональность достаточно богатая.
В качестве левой части оператора, может присутствовать как один столбец, так и кортеж.
Примеры:
SELECT UserID IN (123, 456) FROM ...
SELECT (CounterID, UserID) IN ((34, 123), (101500, 456)) FROM ...
Если слева стоит один столбец, входящий в индекс, а справа - множество констант, то при выполнении запроса, система воспользуется индексом.
Не перечисляйте слишком большое количество значений (миллионы) явно. Если множество большое - лучше загрузить его во временную таблицу (например, смотрите раздел Внешние данные для обработки запроса), и затем воспользоваться подзапросом.
В качестве правой части оператора может быть множество константных выражений, множество кортежей с константными выражениями (показано в примерах выше), а также имя таблицы или подзапрос SELECT в скобках.
Если типы данных в левой и правой частях подзапроса IN
различаются, ClickHouse преобразует значение в левой части к типу данных из правой части. Преобразование выполняется по аналогии с функцией accurateCastOrNull, т.е. тип данных становится Nullable, а если преобразование не может быть выполнено, возвращается значение NULL.
Пример
Запрос:
SELECT '1' IN (SELECT 1);
Результат:
┌─in('1', _subquery49)─┐
│ 1 │
└──────────────────────┘
Если в качестве правой части оператора указано имя таблицы (например, UserID IN users
), то это эквивалентно подзапросу UserID IN (SELECT * FROM users)
. Это используется при работе с внешними данными, отправляемыми вместе с запросом. Например, вместе с запросом может быть отправлено множество идентификаторов посетителей, загруженное во временную таблицу users, по которому следует выполнить фильтрацию.
Если в качестве правой части оператора, указано имя таблицы, имеющей движок Set (подготовленное множество, постоянно находящееся в оперативке), то множество не будет создаваться заново при каждом запросе.
В подзапросе может быть указано более одного столбца для фильтрации кортежей. Пример:
SELECT (CounterID, UserID) IN (SELECT CounterID, UserID FROM ...) FROM ...
Типы столбцов слева и справа оператора IN должны совпадать.
Оператор IN и подзапрос могут встречаться в любой части запроса, в том числе в агрегатных и лямбда-функциях. Пример:
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
┌──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 всегда равен 0
, независимо от того, находится NULL
в правой или левой части оператора. Значения NULL
не входят ни в какое множество, не соответствуют друг другу и не могут сравниваться, если transform_null_in = 0.
Рассмотрим для примера таблицу t_null
:
┌─x─┬────y─┐
│ 1 │ ᴺᵁᴸᴸ │
│ 2 │ 3 │
└───┴──────┘
При выполнении запроса SELECT x FROM t_null WHERE y IN (NULL,3)
получим следующий результат:
┌─x─┐
│ 2 │
└───┘
Видно, что строка, в которой y = NULL
, выброшена из результатов запроса. Это произошло потому, что ClickHouse не может решить входит ли NULL
в множество (NULL,3)
, возвращает результат операции 0
, а SELECT
выбрасывает эту строку из финальной выдачи.
SELECT y IN (NULL, 3)
FROM t_null
┌─in(y, tuple(NULL, 3))─┐
│ 0 │
│ 1 │
└───────────────────────┘
Распределённые подзапросы
Существует два варианта IN-ов с подзапросами (аналогично для JOIN-ов): обычный IN
/ JOIN
и GLOBAL IN
/ GLOBAL JOIN
. Они отличаются способом выполнения при распределённой обработке запроса.
Помните, что алгоритмы, описанные ниже, могут работать иначе в зависимости от настройки 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.
Например, запрос
SELECT uniq(UserID) FROM distributed_table
будет отправлен на все удалённые серверы в виде
SELECT uniq(UserID) FROM local_table
, выполнен параллельно на каждом из них до стадии, позволяющей объединить промежуточные результаты; затем промежуточные результаты вернутся на сервер-инициатор запроса, будут на нём объединены, и финальный результат будет отправлен клиенту.
Теперь рассмотрим запрос с IN-ом:
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM local_table WHERE CounterID = 34)
- расчёт пересечения аудиторий двух сайтов.
Этот запрос будет отправлен на все удалённые серверы в виде
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 внутри подзапроса. Запрос будет выглядеть так:
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
Этот запрос будет отправлен на все удалённые серверы в виде
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
На каждом удалённом сервере начнёт выполняться подзапрос. Так как в подзапросе используется распределённая таблица, то подзапрос будет, на каждом удалённом сервере, снова отправлен на каждый удалённый сервер, в виде
SELECT UserID FROM local_table WHERE CounterID = 34
Например, если у вас кластер из 100 серверов, то выполнение всего запроса потребует 10 000 элементарных запросов, что, как правило, является неприемлемым.
В таких случаях всегда следует использовать GLOBAL IN вместо IN. Рассмотрим его работу для запроса
SELECT uniq(UserID) FROM distributed_table WHERE CounterID = 101500 AND UserID GLOBAL IN (SELECT UserID FROM distributed_table WHERE CounterID = 34)
На сервере-инициаторе запроса будет выполнен подзапрос
SELECT UserID FROM distributed_table WHERE CounterID = 34
, и результат будет сложен во временную таблицу в оперативке. Затем запрос будет отправлен на каждый удалённый сервер в виде
SELECT uniq(UserID) FROM local_table WHERE CounterID = 101500 AND UserID GLOBAL IN _data1
, и вместе с запросом, на каждый удалённый сервер будет отправлена временная таблица _data1
(имя временной таблицы - implementation defined).
Это гораздо более оптимально, чем при использовании обычного IN. Но при этом, следует помнить о нескольких вещах:
- При создании временной таблицы данные не уникализируются. Чтобы уменьшить объём передаваемых по сети данных, укажите в подзапросе DISTINCT (для обычного IN-а этого делать не нужно).
- Временная таблица будет передана на все удалённые серверы. Передача не учитывает топологию сети. Например, если 10 удалённых серверов расположены в удалённом относительно сервера-инициатора запроса дата-центре, то по каналу в удалённый дата-центр данные будет переданы 10 раз. Старайтесь не использовать большие множества при использовании GLOBAL IN.
- При передаче данных на удалённые серверы не настраивается ограничение использования сетевой полосы. Вы можете перегрузить сеть.
- Старайтесь распределять данные по серверам так, чтобы в GLOBAL IN-ах не было частой необходимости.
- Если в GLOBAL IN есть частая необходимость, то спланируйте размещение кластера ClickHouse таким образом, чтобы в каждом дата-центре была хотя бы одна реплика каждого шарда, и среди них была быстрая сеть - чтобы запрос целиком можно было бы выполнить, передавая данные в пределах одного дата-центра.
В секции GLOBAL IN
также имеет смысл указывать локальную таблицу - в случае, если эта локальная таблица есть только на сервере-инициаторе запроса, и вы хотите воспользоваться данными из неё на удалённых серверах.
Распределенные подзапросы и max_parallel_replicas
Когда настройка max_parallel_replicas больше чем 1, распределенные запросы преобразуются. Например, следующий запрос:
SELECT CounterID, count() FROM distributed_table_1 WHERE UserID IN (SELECT UserID FROM local_table_2 WHERE CounterID < 100)
SETTINGS max_parallel_replicas=3
преобразуется на каждом сервере в
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
.