IN, NOT IN, GLOBAL IN и GLOBAL NOT IN рассматриваются отдельно, поскольку их функциональные возможности достаточно широки.
Левая часть оператора — это либо один столбец, либо кортеж.
Примеры:
SELECT в скобках.
Из соображений обратной совместимости, если правая часть представляет собой одно выражение tuple, оно может интерпретироваться либо как набор значений, либо как одно значение кортежа — в зависимости от левой части оператора IN. Если левая часть является скалярным значением, ClickHouse рассматривает элементы этого единственного выражения tuple в правой части как отдельные значения IN:
Query
Response
SELECT 1 IN (1, 2). Если левая часть также является кортежем, правая часть интерпретируется как набор значений кортежа:
Query
Response
tuple. Скалярная левая часть не может сопоставляться с правой частью, содержащей несколько значений кортежа:
Query
Response
IN.
В этом случае значение правой части приводится к типу левой части, как если бы к правой части была применена функция accurateCastOrNull.
Это означает, что тип данных становится Nullable, и если преобразование
невозможно, возвращается NULL.
Пример
Query
Response
UserID IN users), это эквивалентно подзапросу UserID IN (SELECT * FROM users). Используйте это при работе с внешними данными, передаваемыми вместе с запросом. Например, запрос можно отправить вместе с набором идентификаторов пользователей, загруженных во временную таблицу ‘users’, по которой нужно выполнить фильтрацию.
Если правая часть оператора — это имя таблицы с движком Set (подготовленный набор данных, который всегда хранится в оперативной памяти), набор данных не будет создаваться заново при каждом запросе.
Подзапрос может задавать более одного столбца для фильтрации Tuple.
Пример:
Query
IN должны иметь одинаковый тип.
Оператор IN и подзапрос могут находиться в любой части запроса, в том числе в агрегатных функциях и лямбда-функциях.
Пример:
Query
Response
IN всегда выполняется только один раз на одном сервере. Зависимых подзапросов нет.
Обработка NULL
IN считает, что результат операции с NULL всегда равен 0, независимо от того, находится NULL справа или слева от оператора. Значения NULL не включаются ни в один набор данных, не считаются равными друг другу и не могут сравниваться, если transform_null_in = 0.
Вот пример с таблицей t_null:
SELECT x FROM t_null WHERE y IN (NULL,3) вы получите следующий результат:
y = NULL, исключается из результатов запроса. Это происходит потому, что ClickHouse не может определить, входит ли NULL в множество (NULL,3), возвращает 0 в качестве результата операции, а SELECT исключает эту строку из итогового вывода.
Распределённые подзапросы
IN с подзапросами (аналогично операторам JOIN) существует два варианта: обычный IN / JOIN и GLOBAL IN / GLOBAL JOIN. Они различаются способом выполнения при распределённой обработке запросов.
Помните, что описанные ниже алгоритмы могут работать по-разному в зависимости от значения настройки
distributed_product_mode из раздела настроек.IN запрос отправляется на удалённые серверы, и каждый из них выполняет подзапросы в секции IN или JOIN.
При использовании GLOBAL IN / GLOBAL JOIN сначала выполняются все подзапросы для GLOBAL IN / GLOBAL JOIN, а результаты сохраняются во временные таблицы. Затем эти временные таблицы отправляются на каждый удалённый сервер, где запросы выполняются с использованием полученных временных данных.
Для GLOBAL ... JOIN то, какая сторона JOIN вычисляется как подзапрос, зависит от типа JOIN: для LEFT и INNER JOIN вычисляется правая таблица; для RIGHT JOIN вместо неё вычисляется левая таблица, поскольку правая таблица является сохраняемой стороной и должна считываться из сегментов.
Для нераспределённого запроса используйте обычные IN / JOIN.
Будьте осторожны при использовании подзапросов в секциях IN / JOIN при распределённой обработке запросов.
Рассмотрим несколько примеров. Предположим, что на каждом сервере кластера есть обычная таблица local_table. Кроме того, на каждом сервере есть таблица distributed_table с движком Distributed, которая охватывает все серверы кластера.
При выполнении запроса к distributed_table он будет отправлен на все удалённые серверы и выполнен на них с использованием local_table.
Например, запрос
IN:
- Вычисление пересечения аудиторий двух сайтов.
IN будет формироваться на каждом сервере независимо — только из тех данных, которые хранятся локально на этом сервере.
Это будет работать корректно и оптимально, если вы заранее предусмотрели такой сценарий и распределили данные по серверам кластера так, что данные для каждого отдельного UserID целиком хранятся на одном сервере. В этом случае все необходимые данные будут доступны локально на каждом сервере. В противном случае результат окажется неточным. Такой вариант запроса называется “local IN”.
Чтобы исправить поведение запроса в случае, когда данные распределены случайным образом по серверам кластера, можно указать distributed_table внутри подзапроса. Запрос будет выглядеть следующим образом:
GLOBAL IN вместо IN. Рассмотрим, как это работает для следующего запроса:
_data1 будет отправлена на каждый удаленный сервер вместе с запросом (имя временной таблицы зависит от реализации).
Это более эффективно, чем использование обычного IN. Однако имейте в виду следующее:
- При создании временной таблицы данные не становятся уникальными. Чтобы уменьшить объем данных, передаваемых по сети, укажите DISTINCT в подзапросе. (Для обычного
INэтого делать не нужно.) - Временная таблица будет отправлена на все удаленные серверы. При передаче не учитывается топология сети. Например, если 10 удаленных серверов находятся в датацентре, значительно удаленном от сервера, инициировавшего запрос, данные будут 10 раз переданы по каналу в этот удаленный датацентр. Старайтесь избегать больших наборов данных при использовании
GLOBAL IN. - При передаче данных на удаленные серверы ограничения пропускной способности сети не настраиваются. Это может привести к перегрузке сети.
- Старайтесь распределять данные по серверам так, чтобы не приходилось регулярно использовать
GLOBAL IN. - Если вам часто требуется использовать
GLOBAL IN, спланируйте размещение кластера ClickHouse так, чтобы одна группа реплик находилась не более чем в одном датацентре, а между ними была быстрая сеть, — тогда запрос можно будет полностью обработать в пределах одного датацентра.
GLOBAL IN, если эта локальная таблица доступна только на сервере, инициировавшем запрос, и вы хотите использовать данные из нее на удаленных серверах.
Распределённые подзапросы и max_rows_in_set
max_rows_in_set и max_bytes_in_set, чтобы управлять объёмом данных, передаваемых при распределённых запросах.
Это особенно важно, если запрос GLOBAL IN возвращает большой объём данных. Рассмотрим следующий SQL:
some_predicate недостаточно селективен, он вернет большой объем данных, что приведет к проблемам с производительностью. В таких случаях стоит ограничить передачу данных по сети. Также обратите внимание, что set_overflow_mode имеет значение throw (по умолчанию), то есть при достижении этих пороговых значений генерируется исключение.
Распределённые подзапросы и max_parallel_replicas
M находится в диапазоне от 1 до 3 в зависимости от того, на какой реплике выполняется локальный запрос.
Эти настройки влияют на каждую таблицу семейства MergeTree в запросе и дают тот же эффект, что и применение SAMPLE 1/3 OFFSET (M-1)/3 к каждой из них.
Поэтому добавление настройки max_parallel_replicas даст корректные результаты только в том случае, если обе таблицы используют одну и ту же схему репликации и сэмплируются по UserID или по одному из его подключей. В частности, если у local_table_2 нет ключа выборки, результаты будут некорректными. То же правило применяется и к JOIN.
Один из способов обойти это ограничение, если local_table_2 не удовлетворяет этим требованиям, — использовать GLOBAL IN или GLOBAL JOIN.
Если у таблицы нет ключа выборки, можно использовать более гибкие варианты parallel_replicas_custom_key, которые могут давать иное и более оптимальное поведение.