Skip to main content
The JOIN clause produces a new table by combining columns from one or multiple tables by using values common to each. It is a common operation in databases with SQL support, which corresponds to relational algebra join. The special case of one table join is often referred to as a “self-join”. Syntax
Expressions from the ON clause and columns from the USING clause are called “JOIN keys”. Unless otherwise stated, a JOIN produces a Cartesian product from rows with matching JOIN keys, which might produce results with many more rows than the source tables. This page describes the syntax and semantics of the JOIN clause. To learn about the execution of JOINs in ClickHouse, JOIN types, settings, memory considerations, and optimizations, refer to Related content.

Supported types of JOIN

All standard SQL JOIN types are supported:
  • JOIN without a type specified implies INNER.
  • The keyword OUTER can be omitted.
  • An alternative syntax for CROSS JOIN is specifying multiple tables in the FROM clause separated by commas.
  • If there are no matching columns for a NATURAL JOIN, it functions like a CROSS JOIN.
Additional JOIN types available in ClickHouse are: Diagrams of each JOIN type and strictness are shown in Using JOINs in ClickHouse.
Disabling the semi_join_include_columns_from_both_sides or anti_join_include_columns_from_both_sides setting makes the corresponding join expose only its preserved side to expressions resolved after the join result is formed.
  • LEFT SEMI JOIN and LEFT ANTI JOIN expose only left-side columns.
  • RIGHT SEMI JOIN and RIGHT ANTI JOIN expose only right-side columns.
  • This affects clauses such as SELECT, PREWHERE, WHERE, GROUP BY, HAVING, QUALIFY, ORDER BY, and LIMIT BY, including qualified wildcards like t1.*.
  • The ON expression of the same JOIN can still reference both sides.
When these settings are enabled (the default), ClickHouse keeps the legacy behavior, where both sides remain accessible and SELECT * expands columns from both tables.
Not every JOIN algorithm supports every combination of JOIN type and strictness. The supported combinations are listed in Supported JOIN types per algorithm.

Rewriting CROSS JOIN as INNER JOIN

When the WHERE clause of a query that uses a CROSS JOIN or a comma-separated list of tables contains equality conditions between the tables, ClickHouse can rewrite the JOIN as an INNER JOIN that uses these conditions as JOIN keys. The cross_to_inner_join_rewrite setting controls this rewrite:
  • 0: The rewrite is disabled.
  • 1: A comma-separated list of tables or a CROSS JOIN is rewritten when the WHERE clause contains an equality condition between the tables. Otherwise, the JOIN is executed as a CROSS JOIN.
  • 2: As with 1, but a comma-separated list of tables that cannot be rewritten makes the query fail with the INCORRECT_QUERY error if the query has a WHERE clause. A JOIN written with the CROSS JOIN keywords that cannot be rewritten is executed as a CROSS JOIN.
With the value 0, the query plan optimization controlled by query_plan_merge_filter_into_join_condition can still merge an equality filter into the JOIN and execute it as an INNER JOIN.

ON section conditions

An ON section can contain several conditions combined using the AND and OR operators. Conditions specifying JOIN keys must:
  • reference both left and right tables
  • use the equality operator
Other conditions may use other operators and may reference either the left or the right table of a query, or both. Rows are joined if the whole complex condition is met. If the conditions are not met, rows may still be included in the result depending on the JOIN type. If the same conditions are placed in a WHERE section and they are not met, then rows are always filtered out from the result. JOIN keys combined with OR are supported only by the hash algorithm, which builds a separate hash table for each OR argument that contains JOIN keys.
Only the equality operator (=) makes a condition a JOIN key. Other operators between columns of different tables are supported, but without any equality the JOIN has to examine every pair of rows; see JOIN with an arbitrary ON condition.
Example Consider table_1 and table_2:
Query with one JOIN key condition and an additional condition for table_2:
Query
The result contains the row with the name C and the empty text column. It is included into the result because an OUTER type of a join is used.
Response
Query with INNER type of a join and multiple conditions:
Query
Response
Query with INNER type of a join and condition with OR:
Query
Response
Query with INNER type of a join and conditions with OR and AND:
A non-equal condition that uses columns from a single table, such as t1.a = t2.key AND t1.b > 0 AND t2.b > t2.c, is applied to that table alone: t1.b > 0 uses columns only from t1 and t2.b > t2.c uses columns only from t2. A non-equal condition that compares columns of different tables, such as t1.a = t2.key AND t1.b > t2.key, is also supported; see the sections below.
Query
Response

JOIN with inequality conditions for columns from different tables

In addition to equality conditions, the ON section can contain conditions that compare columns of different tables with other operators. Such conditions are supported with ALL, ANY, SEMI, and ANTI strictness for every JOIN type that the strictness allows. They are not supported with ASOF strictness. When the ON section contains an equality between the two tables next to the inequality, the equality is the JOIN key and the inequality is checked on the rows it matched. Such a mixed condition is executed by the hash, parallel_hash, and grace_hash algorithms. When ie_join is listed ahead of those algorithms in join_algorithm and the ON section contains two inequality conditions, ie_join executes the JOIN instead and applies the equality as a filter. When the ON section contains no equality between the two tables, there is no JOIN key to match on. Such a JOIN is executed by ie_join when the condition is a pair of inequalities, or otherwise as a block nested loop join. A condition evaluated during the join may not contain arrayJoin, because it must preserve the number of rows; use ARRAY JOIN in a subquery instead. Example Table t1:
Table t2

JOIN with an arbitrary ON condition

The ON section may be an arbitrary boolean expression over the columns of both tables, such as a range check, an arithmetic comparison, or a call to a scalar function. When it contains no equality between the two tables, there is no JOIN key to match rows on. A pair of inequality conditions is executed by ie_join whenever that algorithm is listed in join_algorithm, with no requirement that it come first, because no other join_algorithm value can execute such a condition. The default value of join_algorithm includes ie_join since version 26.8. Any other condition is executed as a block nested loop join: the right table is materialized and the condition is evaluated on every pair of rows. An ALL INNER JOIN takes the equivalent form of a CROSS JOIN with the condition as a filter. The block nested loop join supports every JOIN type and strictness except ASOF JOIN, PASTE JOIN and ANY FULL JOIN. It is not one of the join_algorithm values: it is the last resort, used only when no other algorithm can execute the condition. In EXPLAIN output it appears as a BlockNestedLoopJoin step. When allow_block_nested_loop_join is disabled, a query that would need it is rejected with INVALID_JOIN_ON_EXPRESSION. Example
Query
Response
The row that matched nothing is padded according to join_use_nulls, as in any other JOIN: with a default value above, and with NULL when the setting is enabled. Strictness ANY and SEMI normally keep one row per group of rows that share a JOIN key. There is no such group here, so they keep one row per row of the table that drives the JOIN: LEFT ANY and LEFT SEMI emit each left row at most once, RIGHT ANY and RIGHT SEMI each right row at most once. ANY INNER limits both sides at once: each row of either table is used at most once, so the result has at most as many rows as the smaller table. Which pairs make up such a result is arbitrary, exactly as ANY implies, and it may differ between two runs of the same query. For ANY INNER this extends to the number of rows: pairing each row of both sides greedily, in whatever order the rows are examined, may leave a different number of rows unpaired, so the result of count() over an ANY INNER JOIN with no JOIN key is not reproducible and depends on the number of threads and on the physical order of the right table. Performance A block nested loop join examines every pair of rows, so its cost grows with the product of the row counts of both tables, as described in Block nested loop fallback. The materialized right table is subject to the hard limits and spill thresholds described in JOIN memory management and spilling.

NULL and NaN values in JOIN keys

NULL is not equal to any value, including itself. If a JOIN key has a NULL value in one table, it does not match a NULL value in the other table. If the JOIN keys are Nullable columns, a row in which at least one of the keys is NULL is not joined. Example Table A:
Table B:
The row with Charlie from table A and the row with score 88 from table B are not matched because of the NULL value in the JOIN key. The rows without a match get the value 0 in the score column, as described in Processing of empty or NULL cells. To match NULL values, compare the JOIN keys with the isNotDistinctFrom function:
NaN values in float JOIN keys do not follow the NULL rule above. A scalar comparison of two NaN values (NaN = NaN) is 0. However, JOIN keys are not compared using scalar semantics, and NaN keys may match. Whether they match is not specified: it depends on the JOIN algorithm, the key type, and the session settings. To exclude NaN keys from matching, map them to NULL values, for example ON if(isNaN(A.id), NULL, A.id) = B.id. In an ASOF JOIN, the closest-match column is compared by ordering, which is not defined for NaN values.

Implicit type conversion

When the JOIN keys of the left and the right table have different types, ClickHouse converts them to their least common supertype before it compares them. This applies to every JOIN type and strictness:
  • With ON, the conversion applies only to the comparison. The key columns keep their original types in the result.
  • With USING, the merged column has the common type in the result. When semi_join_include_columns_from_both_sides or anti_join_include_columns_from_both_sides is disabled, the merged column of a SEMI JOIN or an ANTI JOIN keeps the type of the preserved table.
If the key types have no common supertype and both are integer types or tuples of integer types, as is the case for UInt64 and Int64, ClickHouse converts both keys to a type that holds the values that both key types have in common. For UInt64 and Int64, this type is UInt64. A key value that this type cannot represent does not match any value. This conversion is used for the equality conditions in ON, except isNotDistinctFrom, and for USING in an INNER JOIN or a SEMI JOIN. It is not used for the closest-match condition of an ASOF JOIN, for the inequality conditions that ie_join executes, or for USING in other JOINs, because their result column must also hold the values without a match. This conversion is available since version 26.9. If the key types have no common supertype and this conversion does not apply, as is the case for String and Int32 or for UInt64 and Float64, the query fails with an exception, such as NO_COMMON_TYPE. Example Consider the table t_1:
and the table t_2:
The query
returns the set:

ASOF JOIN usage

ASOF JOIN joins records that have no exact match. ASOF is a JOIN strictness that is combined with an INNER or LEFT JOIN. It is not a JOIN algorithm. An ASOF JOIN requires a closest-match column in both tables. This column:
  • Must have a type with a defined order.
  • With the hash algorithm, can be of the following types: Int, UInt, Float, Decimal, Date, Date32, DateTime, DateTime64, Time, Enum, Bool, and Nullable of these types. Other types, such as String and Time64, are rejected with the BAD_TYPE_OF_FIELD error.
  • With the full_sorting_merge algorithm, can also be of other comparable types, such as String.
  • With the hash algorithm, cannot be the only column in the JOIN clause. At least one equality condition is required. The full_sorting_merge algorithm does not have this requirement.
Syntax ASOF JOIN ... ON:
You can use any number of equality conditions and exactly one closest match condition. For example, SELECT count() FROM table_1 ASOF LEFT JOIN table_2 ON table_1.a == table_2.b AND table_2.t <= table_1.t. Conditions supported for the closest match: >, >=, <, <=. Syntax ASOF JOIN ... USING:
ASOF JOIN uses equi_columnX for joining on equality and asof_column for joining on the closest match with the table_1.asof_column >= table_2.asof_column condition. The asof_column column is always the last one in the USING clause. For example, consider the following tables:
ASOF JOIN can take the timestamp of a user event from table_1 and find an event in table_2 where the timestamp is closest to the timestamp of the event from table_1 corresponding to the closest match condition. Equal timestamp values are the closest if available. Here, the user_id column can be used for joining on equality and the ev_time column can be used for joining on the closest match. In our example, event_1_1 can be joined with event_2_1 and event_1_2 can be joined with event_2_3, but event_2_2 can’t be joined.
Of the JOIN algorithms, only hash and full_sorting_merge support ASOF strictness. See Supported JOIN types per algorithm. ASOF is not supported in the Join table engine.

PASTE JOIN usage

The result of PASTE JOIN is a table that contains all columns from left subquery followed by all columns from the right subquery. The rows are matched based on their positions in the original tables (the order of rows should be defined). If the subqueries return a different number of rows, extra rows will be cut. Example:
Note: in this case result can be nondeterministic if the reading is parallel. For example:

LATERAL JOIN

JOIN LATERAL lets the subquery on the right side of a join reference columns of the table expressions on its left side. The result is as if the subquery were evaluated for each distinct combination of the left-side column values it references, and its result were joined to every left row with that combination:
The ON true predicate is mandatory, as for any other INNER or LEFT JOIN; omitting it is a syntax error. It is available since version 26.10. It is experimental and disabled by default; enable it with the enable_lateral_join setting. The lateral subquery is not executed once per left row or once per distinct combination. Like other correlated subqueries, it is decorrelated into a single query over all left rows: the left-side columns it references are added to its GROUP BY keys if it aggregates, its LIMIT (if any) becomes LIMIT ... BY these columns, and its result is joined to the left rows on them. When the correlation is a simple equality, such as orders.user_id = u.id, the left-side column can be replaced by the equal column of the subquery (correlated_subqueries_substitute_equivalent_expressions, enabled by default), which can let the subquery avoid reading the left rows. This is not guaranteed: for example, an aggregate without GROUP BY (such as count) still reads the distinct left values to produce its result for left values with no matching rows. Whenever the distinct left values are passed to the subquery, with the default settings this goes through an in-memory buffer, and only when such a buffer is actually used the join is always a hash join. In all other cases, such as when the equality substitution avoids reading the left rows or the buffer is disabled (correlated_subqueries_use_in_memory_buffer = 0 or correlated_subqueries_default_join_kind = 'left'), the algorithm is chosen by the join_algorithm setting. For example, EXPLAIN of a lateral subquery with LIMIT shows a Join step over a LimitBy (LIMIT BY for decorrelated correlated subquery) step; a subquery without LIMIT has no such step. Only the following subset is supported so far; anything else is rejected with an error:
  • INNER JOIN LATERAL and LEFT JOIN LATERAL only; RIGHT, FULL, PASTE and NATURAL joins are not supported, and LATERAL cannot be combined with a CROSS or comma join at all.
  • The default ALL strictness only; ANY, SEMI, ANTI and ASOF are not supported.
  • No join predicate other than ON true (ON 1 is also accepted); USING is not supported, and the predicate cannot be omitted. Put the filters that relate the two sides into the WHERE clause of the lateral subquery.
  • The GLOBAL and LOCAL join modifiers are not supported.
  • The lateral subquery must reference at least one column of the left side. Use a regular join for a non-correlated subquery.
  • Because the lateral subquery is decorrelated rather than evaluated separately for each left value, it must not contain functions that are non-deterministic within a query, such as rand or generateUUIDv4, or table functions that generate random rows, such as generateRandom. Functions that are constant within a query, such as now, are allowed.
  • Only a subquery is supported as the lateral table expression. The PostgreSQL table-source forms LATERAL unnest(...) and CROSS JOIN UNNEST(...) are not supported. Use the ARRAY JOIN clause instead.
  • The GROUP BY and ORDER BY of the lateral subquery run once for all left values together, so the max_rows_to_group_by, max_rows_to_sort and max_bytes_to_sort limits count the rows for all left values, not for one. They are only supported with the throw overflow mode; any and break are rejected.
  • The result of the decorrelated subquery is matched to the left rows by a single join. As for any hash join, max_rows_in_join and max_bytes_in_join limit the side of this join that is kept in memory, and the planner chooses that side: when the in-memory buffer described above is actually used, it is the left side of JOIN LATERAL, because the left rows must be fully read before the lateral subquery is evaluated; otherwise it can be the result of the decorrelated subquery. The limits are always enforced as if join_overflow_mode were throw: with break, the join would silently drop unrelated left rows.
Example

Distributed JOIN

There are two ways to execute a JOIN involving distributed tables:
  • When using a normal JOIN, the query is sent to remote servers. Subqueries are run on each of them in order to make the right table, and the join is performed with this table. In other words, the right table is formed on each server separately.
  • When using GLOBAL ... JOIN, first the requestor server runs a subquery to calculate one side of the join and collects the result into a temporary table. This temporary table is then passed to each remote server, and queries are run on them using the temporary data that was transmitted. For LEFT and INNER joins, the right table is calculated as the subquery.
For a GLOBAL RIGHT JOIN with ALL, ANY, SEMI, or ANTI strictness whose left table is a Distributed table with more than one shard, ClickHouse swaps the two sides and executes the JOIN as a LEFT JOIN, so that the rows of the preserved right table are returned once. The left table is then calculated as the subquery, and the right table is read from its shards if it is a Distributed table with several shards, or on the requestor server otherwise. A GLOBAL RIGHT JOIN or GLOBAL FULL JOIN that cannot be swapped this way is executed on the requestor server. This behavior applies since version 26.9. Be careful when using GLOBAL. For more information, see the Distributed subqueries section.

Processing of empty or NULL cells

In the result of an outer JOIN, the columns of the other table are empty for a row without a match. The join_use_nulls setting defines how ClickHouse fills these cells:
  • 0: The cells are filled with the default value of the column type.
  • 1: The cells are filled with NULL, and the types of these columns are converted to Nullable. A column of a type that cannot be inside Nullable, such as Array, keeps its type and is filled with the default value. In a FULL JOIN, the merged USING column is also converted to Nullable.
A row in which at least one of the JOIN keys is NULL is not joined, as described in NULL and NaN values in JOIN keys.

USING clause

The USING clause specifies one or more columns to join on, and establishes the equality of these columns:
  • Each column must exist under the same name in both the left and the right table expression. Aliases in subqueries can give columns the same name.
  • The USING clause accepts only column names. Expressions are not supported.
  • The column list can be written with or without parentheses.
  • Each column of the USING clause appears once in the result. Its type is described in Implicit type conversion.
  • Other columns can have the same name in both tables. SELECT * qualifies the column of the right table with the table name or alias, and an unqualified name resolves to the column of the left table when single_join_prefer_left_table is enabled.
  • When analyzer_compatibility_join_using_top_level_identifier is enabled, a column name in USING is resolved from an alias of the same name in the query, such as an alias in the SELECT list.

Examples

The following query joins the number of hits per counter with the number of visits per counter. The ANY LEFT JOIN returns each row of the left subquery once, and a counter without visits gets the default value 0:
Query
Response

Settings

The session settings that affect the semantics, planning, execution, and memory use of JOINs are listed in JOIN Settings.