JOIN создает новую таблицу, объединяя столбцы одной или нескольких таблиц на основе общих значений. Это распространенная операция в базах данных с поддержкой SQL, соответствующая операции JOIN в реляционной алгебре. Частный случай, когда таблица соединяется сама с собой, часто называют “self-join”.
Синтаксис
ON и столбцы из предложения USING называются “ключами JOIN”. Если не указано иное, JOIN создаёт декартово произведение строк с совпадающими “ключами JOIN”, из-за чего в результате может оказаться гораздо больше строк, чем в исходных таблицах.
Поддерживаемые типы JOIN
Поддерживаются все стандартные типы SQL JOIN:JOINбез указания типа подразумеваетINNER.- Ключевое слово
OUTERможно без опасений опустить. - Альтернативный синтаксис для
CROSS JOIN— указать несколько таблиц вFROMclause, разделив их запятыми. - Если у
NATURAL JOINнет совпадающих столбцов, он работает какCROSS JOIN.
При использовании анализатора отключение настройки
semi_join_include_columns_from_both_sides или anti_join_include_columns_from_both_sides ограничивает доступ к столбцам соответствующего JOIN только сохраняемой стороной для выражений, разрешаемых после формирования результата JOIN.LEFT SEMI JOINиLEFT ANTI JOINпредоставляют доступ только к столбцам левой стороны.RIGHT SEMI JOINиRIGHT ANTI JOINпредоставляют доступ только к столбцам правой стороны.- Это влияет на такие секции, как
SELECT,PREWHERE,WHERE,GROUP BY,HAVING,QUALIFY,ORDER BYиLIMIT BY, включая подстановочные шаблоны с квалификатором, такие какt1.*. - Выражение
ONтого жеJOINпо-прежнему может ссылаться на обе стороны.
SELECT * раскрывается в список столбцов обеих таблиц.Когда для join_algorithm задано значение
partial_merge, RIGHT JOIN и FULL JOIN поддерживаются только со строгостью ALL (SEMI, ANTI, ANY и ASOF не поддерживаются).LATERAL JOIN
JOIN LATERAL позволяет подзапросу в правой части JOIN ссылаться на столбцы табличных
выражений, расположенных слева от него. Подзапрос вычисляется для каждой уникальной комбинации значений тех столбцов
левой части, на которые он ссылается, а его результат соединяется с каждой строкой левой части, имеющей эту комбинацию:
ON true обязателен, как и для любого другого INNER или LEFT JOIN; его отсутствие — синтаксическая ошибка.
Эта возможность экспериментальная и по умолчанию отключена; включите её с помощью
настройки allow_experimental_lateral_join.
Пока поддерживаются только перечисленные ниже возможности; всё остальное вызывает ошибку:
- Только
INNER JOIN LATERALиLEFT JOIN LATERAL; JOIN типовRIGHT,FULL,PASTEиNATURALне поддерживаются, аLATERALнельзя сочетать сCROSSили JOIN через запятую. - Только строгость
ALL, используемая по умолчанию;ANY,SEMI,ANTIиASOFне поддерживаются. - Допускается только предикат JOIN
ON true(также принимаетсяON 1);USINGне поддерживается, и предикат нельзя опускать. Разместите фильтры, связывающие обе стороны JOIN, в предложенииWHEREлатерального подзапроса. - Модификаторы JOIN
GLOBALиLOCALне поддерживаются. - Латеральный подзапрос должен ссылаться хотя бы на один столбец левой стороны. Для некоррелированного подзапроса используйте обычный JOIN.
- Латеральный подзапрос выполняется один раз для каждого уникального сочетания значений столбцов левой стороны, на которые он ссылается, а не
для каждой строки левой стороны, поэтому он не должен содержать функции, недетерминированные в пределах запроса, такие как
randилиgenerateUUIDv4, либо табличные функции, генерирующие случайные строки, такие какgenerateRandom. Функции, значение которых постоянно в пределах запроса, такие какnow, допускаются. - В качестве латерального табличного выражения поддерживается только подзапрос. Формы записи источника данных PostgreSQL
LATERAL unnest(...)иCROSS JOIN UNNEST(...)не поддерживаются — вместо них используйте секциюARRAY JOIN. GROUP BYиORDER BYлатерального подзапроса выполняются один раз над совокупными результатами всех его выполнений, поэтому ограниченияmax_rows_to_group_by,max_rows_to_sortиmax_bytes_to_sortучитывают строки всех выполнений, а не одного. Они поддерживаются только с режимом переполненияthrow;anyиbreakвызывают ошибку.- Строки, полученные при всех выполнениях подзапроса, сопоставляются со строками левой стороны с помощью одного JOIN. Как и для любого hash join,
max_rows_in_joinиmax_bytes_in_joinограничивают объём данных на стороне JOIN, которая хранится в памяти, а планировщик выбирает эту сторону: при настройках по умолчанию (correlated_subqueries_use_in_memory_buffer = 1) это левая сторонаJOIN LATERAL, поскольку её строки необходимо полностью прочитать до выполнения латерального подзапроса; в противном случае в памяти могут храниться совокупные результаты всех выполнений подзапроса. Ограничения всегда применяются так, как если быjoin_overflow_modeимел значениеthrow: приbreakJOIN без предупреждения отбрасывал бы строки левой стороны, не связанные с переполнением.
Настройки
Тип JOIN по умолчанию можно переопределить с помощью настройкиjoin_default_strictness.
Поведение сервера ClickHouse для операций ANY JOIN зависит от настройки any_join_distinct_right_table_keys.
См. также
join_algorithmjoin_any_take_last_rowjoin_use_nullspartial_merge_join_rows_in_right_blocksjoin_on_disk_max_files_to_mergeany_join_distinct_right_table_keys
cross_to_inner_join_rewrite, чтобы задать поведение, если ClickHouse не удаётся преобразовать CROSS JOIN в INNER JOIN. Значение по умолчанию — 1, что позволяет продолжить выполнение JOIN, но он будет медленнее. Установите cross_to_inner_join_rewrite в 0, если хотите, чтобы было сгенерировано исключение, и в 2, чтобы не выполнять cross joins, а вместо этого принудительно преобразовывать все joins через запятую и cross joins. Если преобразование не удастся при значении 2, вы получите сообщение об ошибке: “Please, try to simplify WHERE section”.
Условия в секции ON
СекцияON может содержать несколько условий, объединённых с помощью операторов AND и OR. Условия, задающие ключи JOIN, должны:
- ссылаться как на левую, так и на правую таблицу
- использовать оператор равенства
JOIN. Обратите внимание: если поместить те же условия в раздел WHERE и они не выполняются, то строки всегда отфильтровываются из результата.
Оператор OR в условии ON работает на основе алгоритма hash join — для каждого аргумента OR, содержащего ключи JOIN, создаётся отдельная хеш-таблица, поэтому расход памяти и время выполнения запроса растут линейно по мере увеличения числа выражений OR в условии ON.
Ключом JOIN условие делает только оператор равенства (
=). Другие операторы между столбцами разных таблиц поддерживаются, однако если равенства нет, JOIN вынужден перебирать все пары строк и работает значительно медленнее; см. JOIN с произвольным условием ON.table_1 и table_2:
table_2:
Query
C и пустым текстовым столбцом. Она включена в результат, поскольку используется JOIN типа OUTER.
Response
INNER и несколькими условиями:
Query
Response
INNER и условием с OR:
Query
Response
INNER и условиями с OR и AND:
Условие неравенства, в котором используются столбцы только из одной таблицы, например
t1.a = t2.key AND t1.b > 0 AND t2.b > t2.c, применяется только к этой таблице: в t1.b > 0 используются только столбцы из t1, а в t2.b > t2.c — только столбцы из t2.
Условие неравенства, сравнивающее столбцы разных таблиц, например t1.a = t2.key AND t1.b > t2.key, также поддерживается; подробнее см. в разделах ниже.Query
Response
JOIN с условиями неравенства для столбцов из разных таблиц
ClickHouse поддерживаетALL/ANY/SEMI/ANTI INNER/LEFT/RIGHT/FULL JOIN не только с условиями равенства, но и с условиями неравенства.
Если секция ON наряду с неравенством содержит равенство между двумя таблицами, это равенство служит ключом JOIN, а неравенство проверяется для строк, сопоставленных по этому ключу. Такое смешанное условие выполняется только алгоритмами JOIN hash, parallel_hash и grace_hash.
Если секция ON не содержит равенства между двумя таблицами, ключа для сопоставления нет. Такой JOIN выполняется алгоритмом ie_join, если условие представляет собой пару неравенств, а в остальных случаях — как блочный JOIN с вложенными циклами.
Условие, вычисляемое во время JOIN, не может содержать arrayJoin, поскольку оно должно сохранять количество строк; вместо этого используйте ARRAY JOIN в подзапросе.
Пример
Таблица t1:
t2
JOIN с произвольным условием ON
СекцияON может содержать произвольное булево выражение над столбцами обеих таблиц, например проверку вхождения в диапазон, арифметическое сравнение или вызов скалярной функции. Если в ней нет условия равенства между столбцами двух таблиц, ключ JOIN для сопоставления строк отсутствует. Пара условий неравенства обрабатывается алгоритмом ie_join, если он указан в join_algorithm, как это настроено по умолчанию. Для любого другого условия используется блочный JOIN с вложенными циклами: правая таблица материализуется, а условие проверяется для каждой пары строк. ALL INNER JOIN в этом случае эквивалентен CROSS JOIN с этим условием в качестве фильтра.
Блочный JOIN с вложенными циклами поддерживает все типы JOIN и варианты строгости, кроме ASOF JOIN, PASTE JOIN и ANY FULL JOIN. Он не входит в число значений join_algorithm и используется лишь в крайнем случае, когда ни один другой алгоритм не может обработать условие. В выводе EXPLAIN он отображается как шаг BlockNestedLoopJoin. Если allow_block_nested_loop_join отключён, запрос, которому необходим этот алгоритм, отклоняется с ошибкой INVALID_JOIN_ON_EXPRESSION.
Пример
Query
Response
join_use_nulls, как и в любом другом JOIN: значением по умолчанию, как в примере выше, или NULL, если этот параметр включён.
Строгость
ANY и SEMI обычно оставляют одну строку на каждую группу строк с общим ключом JOIN. Здесь таких групп нет, поэтому они оставляют одну строку на каждую строку ведущей таблицы JOIN: LEFT ANY и LEFT SEMI выводят каждую левую строку не более одного раза, RIGHT ANY и RIGHT SEMI — каждую правую строку не более одного раза. ANY INNER ограничивает обе стороны сразу: каждая строка любой из таблиц используется не более одного раза, поэтому в результате строк не больше, чем в меньшей из таблиц. Какие именно пары войдут в такой результат, не определено — ровно так, как и подразумевает ANY, — и он может различаться между двумя запусками одного и того же запроса. Для ANY INNER это касается и количества строк: при жадном сопоставлении строк обеих сторон в том порядке, в котором они просматриваются, без пары может остаться разное число строк, поэтому результат count() для JOIN ANY INNER без ключа JOIN невоспроизводим и зависит от количества потоков и физического порядка строк в правой таблице.
Производительность
Просматривается каждая пара строк, поэтому объём работы растёт пропорционально произведению количества строк двух таблиц, а не их сумме. Из-за этого блочный JOIN с вложенными циклами на порядки дороже hash join на тех же данных, и разрыв увеличивается с ростом таблиц. Если запрос можно записать хотя бы с одним условием равенства в секции ON, сделайте это.
JOIN типов LEFT ANY, ANY INNER, LEFT SEMI и LEFT ANTI прекращают сканирование правой таблицы, как только для левой строки найдена первая подходящая строка, поэтому обычно они обходятся дешевле соответствующего JOIN ALL. Их аналоги с ведущей правой таблицей (RIGHT ANY, RIGHT SEMI, RIGHT ANTI) просматривают все пары, поскольку результат зависит от того, для каких правых строк нашлось соответствие. join_any_take_last_row здесь не действует: без ключа JOIN нет группы подходящих строк, из которой можно было бы взять последнюю.
Объём используемой памяти ограничен размером материализованной правой таблицы и не растёт с увеличением результата. На правую таблицу распространяются те же настройки, что и в других алгоритмах JOIN (они перечислены в разделе Ограничения памяти): её размер ограничивают max_rows_in_join, max_bytes_in_join и join_overflow_mode, а max_bytes_before_external_join включает сброс данных на диск.
Значения NULL и NaN в ключах JOIN
NULL не равно никакому значению, включая само себя. Это означает, что если ключ JOIN в одной таблице имеет значение NULL, он не будет сопоставлен со значением NULL в другой таблице.
Пример
Таблица A:
B:
Charlie из таблицы A и строка с оценкой 88 из таблицы B отсутствуют в результате из-за значения NULL в ключе JOIN.
Если вы хотите сопоставлять значения NULL, используйте функцию isNotDistinctFrom для сравнения ключей JOIN.
NaN в ключах JOIN типа float не подчиняются приведённому выше правилу для NULL.
Скалярное сравнение двух значений NaN (NaN = NaN) равно 0, однако ключи JOIN сравниваются не по правилам скалярной семантики — ключи NaN могут сопоставляться.
Будут ли они фактически сопоставлены, зависит от деталей реализации, алгоритма JOIN, типа ключа и настроек сеанса.
Не полагайтесь на конкретное поведение.
Если необходимо, чтобы строки с NaN не сопоставлялись, преобразуйте их в значения NULL: ON if(isNaN(A.id), NULL, A.id) = B.id.
В ASOF JOIN столбец для поиска ближайшего совпадения сравнивается по порядку, который NaN не поддерживает — отфильтруйте такие строки с обеих сторон.
Использование ASOF JOIN
ASOF JOIN полезен, когда нужно соединить записи без точного совпадения.
Для этого алгоритма JOIN в таблицах требуется специальный столбец. Этот столбец:
- должен содержать упорядоченную последовательность;
- может иметь один из следующих типов: Int, UInt, Float, Date, DateTime, Decimal;
- для алгоритма
hashJOIN он не может быть единственным столбцом в условииJOIN.
ASOF JOIN ... ON:
SELECT count() FROM table_1 ASOF LEFT JOIN table_2 ON table_1.a == table_2.b AND table_2.t <= table_1.t.
Для поиска ближайшего соответствия поддерживаются следующие условия: >, >=, <, <=.
Синтаксис ASOF JOIN ... USING:
ASOF JOIN использует equi_columnX для JOIN по равенству и asof_column для JOIN по ближайшему совпадению с условием table_1.asof_column >= table_2.asof_column. Столбец asof_column всегда указывается последним в секции USING.
Например, рассмотрим следующие таблицы:
ASOF JOIN может брать временную метку пользовательского события из table_1 и находить событие в table_2, временная метка которого наиболее близка к временной метке события из table_1 в соответствии с условием ближайшего совпадения. Если доступны равные значения временной метки, именно они считаются ближайшими. Здесь столбец user_id можно использовать для JOIN по равенству, а столбец ev_time — для JOIN по ближайшему совпадению. В нашем примере event_1_1 можно соединить с event_2_1, а event_1_2 — с event_2_3, но event_2_2 соединить нельзя.
ASOF JOIN поддерживается только алгоритмами JOIN hash и full_sorting_merge.
Он не поддерживается в движке таблицы Join.Использование PASTE JOIN
РезультатомPASTE JOIN является таблица, содержащая все столбцы левого подзапроса, а затем все столбцы правого подзапроса.
Строки сопоставляются по их позициям в исходных таблицах (порядок строк должен быть определён).
Если подзапросы возвращают разное количество строк, лишние строки будут отброшены.
Пример:
Распределенный JOIN
Существует два способа выполнить JOIN с distributed таблицами:- При использовании обычного
JOINзапрос отправляется на удаленные серверы. Подзапросы выполняются на каждом из них, чтобы сформировать правую таблицу, после чего выполняется JOIN с этой таблицей. Иными словами, правая таблица формируется отдельно на каждом сервере. - При использовании
GLOBAL ... JOINсервер-инициатор сначала выполняет подзапрос, чтобы вычислить одну из сторон JOIN, и собирает результат во временную таблицу. Затем эта временная таблица передается на каждый удаленный сервер, и на них выполняются запросы с использованием переданных временных данных. ДляLEFTиINNERJOIN правая таблица вычисляется подзапросом. ДляRIGHTJOIN вместо этого вычисляется левая таблица, поскольку сохраняется правая таблица, и ее следует читать из сегментов.
GLOBAL. Дополнительные сведения см. в разделе Распределенные подзапросы.
Неявное преобразование типов
ЗапросыINNER JOIN, LEFT JOIN, RIGHT JOIN и FULL JOIN поддерживают неявное преобразование типов для “ключей JOIN”. Однако запрос не может быть выполнен, если ключи JOIN из левой и правой таблиц нельзя привести к одному типу (например, не существует типа данных, который мог бы хранить все значения и из UInt64, и из Int64, или из String и Int32).
Пример
Рассмотрим таблицу t_1:
t_2:
Рекомендации по использованию
Обработка пустых ячеек или ячеек со значением NULL
При JOIN таблиц могут появляться пустые ячейки. Настройка join_use_nulls определяет, как ClickHouse заполняет эти ячейки. Если ключиJOIN — это поля Nullable, то строки, в которых хотя бы один из ключей имеет значение NULL, не объединяются.
Синтаксис
Столбцы, указанные вUSING, должны иметь одинаковые имена в обоих подзапросах, а остальные столбцы должны называться по-разному. Чтобы изменить имена столбцов в подзапросах, можно использовать псевдонимы.
Предложение USING задаёт один или несколько столбцов для JOIN, тем самым устанавливая равенство этих столбцов. Список столбцов указывается без скобок. Более сложные условия JOIN не поддерживаются.
Ограничения синтаксиса
При использовании нескольких секцийJOIN в одном запросе SELECT:
- Получение всех столбцов через
*доступно, только если объединяются таблицы, а не подзапросы. - Секция
PREWHEREнедоступна. - Предложение
USINGнедоступно.
ON, WHERE и GROUP BY:
- Произвольные выражения нельзя использовать в секциях
ON,WHEREиGROUP BY, но можно определить выражение в секцииSELECT, а затем использовать его в этих секциях через алиас.
Производительность
При выполненииJOIN порядок выполнения по отношению к другим этапам запроса не оптимизируется. JOIN (поиск в правой таблице) выполняется до фильтрации в WHERE и до агрегации.
Каждый раз, когда выполняется запрос с одним и тем же JOIN, подзапрос запускается заново, потому что результат не кэшируется. Чтобы этого избежать, используйте специальный движок таблицы Join — подготовленный массив для JOIN, который всегда находится в оперативной памяти.
В некоторых случаях эффективнее использовать IN вместо JOIN.
Если JOIN нужен для соединения с таблицами измерений (это относительно небольшие таблицы, содержащие свойства измерений, например названия рекламных кампаний), он может быть не очень удобен, поскольку к правой таблице приходится обращаться заново при каждом запросе. В таких случаях вместо JOIN следует использовать возможность «словари». Подробнее см. в разделе Словари.
Ограничения памяти
По умолчанию ClickHouse использует алгоритм hash join. ClickHouse беретright_table и создает для нее хеш-таблицу в оперативной памяти. Если включен параметр join_algorithm = 'auto', то после достижения определенного порога потребления памяти ClickHouse переключается на алгоритм merge JOIN. Описание алгоритмов JOIN см. в настройке join_algorithm.
Если вам нужно ограничить потребление памяти операцией JOIN, используйте следующие настройки:
- max_rows_in_join — Ограничивает количество строк в хеш-таблице.
- max_bytes_in_join — Ограничивает размер хеш-таблицы.
enable_adaptive_memory_spill_scheduler все же может сбросить на диск тот JOIN, который приведенный ниже порог сделал пригодным для сброса, а legacy_join_size_limits_trigger_spilling снова превращает эти два жестких ограничения в триггеры сброса данных на диск.
Чтобы JOIN продолжал выполняться за счет сброса правой стороны на диск, а не завершался с ошибкой, используйте:
- max_bytes_before_external_join — Абсолютный порог сброса на диск.
- max_bytes_ratio_before_external_join — Порог сброса на диск как доля доступной памяти.
grace_hash; при нехватке памяти enable_adaptive_memory_spill_scheduler может выполнить сброс раньше, чем они требуют, — но только если одна из них ненулевая, поскольку JOIN без какого-либо порога никогда не сбрасывает данные на диск. Выбранный вами join_algorithm определяет, как именно JOIN сбрасывает данные на диск — grace_hash разбивает правую таблицу на партиции начиная с первого блока, а hash и parallel_hash собирают ее в памяти и переключаются при превышении порога, — а не то, применяются ли эти настройки. Единственное исключение — legacy_join_size_limits_trigger_spilling: при ее включении автономный grace_hash игнорирует оба порога и сбрасывает данные на диск по двум жестким ограничениям.
Примеры
Пример:Связанные материалы
- Блог: ClickHouse: молниеносно быстрая СУБД с полной поддержкой SQL JOIN — Часть 1
- Блог: ClickHouse: молниеносно быстрая СУБД с полной поддержкой SQL JOIN — Внутреннее устройство — Часть 2
- Блог: ClickHouse: молниеносно быстрая СУБД с полной поддержкой SQL JOIN — Внутреннее устройство — Часть 3
- Блог: ClickHouse: молниеносно быстрая СУБД с полной поддержкой SQL JOIN — Внутреннее устройство — Часть 4