Skip to main content
This page provides prescriptive guidance for avoiding JOINs when possible and reducing their cost when they are required. For syntax and semantics, see the JOIN clause reference. For execution algorithms and algorithm selection, see Joining tables.

Prefer avoiding JOINs

ClickHouse supports a wide variety of JOIN types and algorithms. However, JOINs are inherently more expensive than querying from a single, denormalized table. Denormalization shifts computational work from query time to insert or preprocessing time, which often results in significantly lower latency at runtime. For real-time or latency-sensitive analytical queries, denormalization is strongly recommended. In general, denormalize when:
  • Tables change infrequently or when batch refreshes are acceptable.
  • Relationships are not many-to-many and do not have excessively high cardinality.
  • Only a limited subset of the columns will be queried, i.e. certain columns can be excluded from denormalization.
  • You have the capability to shift processing out of ClickHouse into upstream systems like Flink, where real-time enrichment or flattening can be managed.
Not all data needs to be denormalized - focus on the attributes that are frequently queried. Also consider materialized views to incrementally compute aggregates instead of duplicating entire sub-tables. When schema updates are rare and latency is critical, denormalization offers the best performance trade-off. For a full guide on denormalizing data in ClickHouse see denormalization.

When JOINs are required

Use ClickHouse 26.8 or later.Multi-table JOIN reordering and optimizer use of available statistics are enabled by default since 25.12. Column statistics became generally available and enabled by default in 26.3. Since 26.8, eligible tables materialize column statistics on insert by default, improving estimates for cost-based JOIN optimization.
Follow these best practices to improve JOIN performance:
  • Use ANY JOINs when only one match is needed. A regular JOIN returns every matching row and can produce a Cartesian product for duplicate keys. Use LEFT ANY JOIN or ANY INNER JOIN when one match is sufficient to reduce result size and memory use.
  • Reduce both sides before the JOIN. Apply selective conditions in WHERE and JOIN ON clauses. ClickHouse pushes filters down when possible, but explicit subqueries can ensure that each input is filtered before the JOIN.
  • Consider IN instead of INNER JOIN. When a query only needs to test whether a key exists in the other table, an IN subquery expresses that requirement directly and can use less memory than producing an INNER JOIN result.
  • Prefer the Join table engine for reusable lookups. The Join table engine stores a precomputed in-memory structure for a declared JOIN type, strictness, and key. In ClickHouse Cloud, it is implemented as SharedJoin with a MergeTree backing table, and Join(ANY, LEFT, key) provides an updatable per-key lookup. See Fast, updatable lookups with the Join table engine.
  • Join on table sorting keys when possible. When both tables are physically ordered by the JOIN key, full_sorting_merge can skip sorting and reduce memory use. Confirm the execution plan and benchmark this algorithm against the default on representative data.
  • Avoid disk-spilling JOINs. Spilling intermediate JOIN state to disk can keep a query within its memory limit, but disk I/O usually increases latency. Hash joins can switch to grace hash at configured spill thresholds, while merge joins can use external sorting. Reduce input rows and columns or change the data model before relying on spilling. See Choosing the optimal JOIN algorithm for details.
  • Use default values as no-match markers when appropriate. The default join_use_nulls = 0 fills unmatched outer JOIN columns with their data type’s default value and avoids Nullable wrappers. Enable join_use_nulls only when the query must distinguish an unmatched row from a matched row containing a default value.

Choosing the optimal JOIN algorithm

ClickHouse supports several JOIN algorithms that trade off between speed and memory. The default value of join_algorithm is direct,parallel_hash,hash,ie_join, and ClickHouse selects the first applicable algorithm for the JOIN type, strictness, condition, and table engine. Consider changing it only after reducing both inputs and when measurements on representative data show that a different memory or execution strategy is needed. For algorithm execution details and supported JOIN types, see Choosing a JOIN algorithm. If your key optimization metric is performance and you’re looking to execute the join as fast as possible, then you can use the following decision tree for choosing the right join algorithm:

  • (1) If the data from the right-hand side table can be pre-loaded into an in-memory low-latency key-value data structure, e.g. a dictionary, and if the join key matches the key attribute of the underlying key-value storage, and if LEFT ANY JOIN semantics are adequate - then the direct join is applicable and is often the fastest approach.
  • (2) If your table’s physical row order matches the join key sort order, then it depends. In this case, the full sorting merge join skips the sorting phase resulting in significantly reduced memory usage plus, depending on data size and join key value distribution, faster execution times than some of the hash join algorithms.
  • (3) If the right table fits into memory, even with the additional memory usage overhead of the parallel hash join, then this algorithm or the hash join can be faster. This depends on data size, data types, and value distribution of the join key columns.
  • (4) If the right table does not fit into memory, hash and parallel hash joins can spill by switching to grace hash when max_bytes_before_external_join or max_bytes_ratio_before_external_join is reached. Full sorting merge and partial merge joins can also use external storage. Which approach is fastest depends on the data volume, data types, and value distribution of the join key columns.
Partial merge join is optimized for minimizing memory usage when large tables are joined, at the expense of join speed which is quite slow. This is especially the case when the physical row order of the left table doesn’t match the join key sorting order. Grace hash join is the most flexible of the three non-memory-bound join algorithms and offers good control of settings. The grace_hash_join_initial_buckets setting controls the initial number of buckets and provides a tradeoff between memory use and join speed. Which one of the three disk based algorithms is the fastest depends on the volume of data, the data types, and the value distribution of the join key columns. It is always best to run some benchmarks with realistic data volumes of your data in order to determine which algorithm is the fastest.

Optimizing for memory

If you want to optimize a join for the lowest memory usage instead of the fastest execution time, then you can use this decision tree instead:

  • (1) If your table’s physical row order matches the join key sort order, then the memory usage of the full sorting merge join is as low as it gets with the additional benefit of good join speed because the sorting phase is disabled.
  • (2) The grace hash join can be tuned for very low memory usage by increasing grace_hash_join_initial_buckets at the expense of join speed. The partial merge join intentionally uses a low amount of main memory. The full sorting merge join with external sorting enabled generally uses more memory than the partial merge join when the row order does not match the key sort order, with the benefit of significantly better join execution time. Hash and parallel hash joins can also reduce memory pressure by switching to grace hash at the configured spill threshold.

JOIN optimization examples

The following examples use the Stack Overflow dataset and apply the recommendations progressively. The measurements below are medians from five runs on a ClickHouse Cloud service running ClickHouse 26.8.1 with max_threads = 4. Each query was warmed once, the query order was alternated, and use_query_cache and use_query_condition_cache were disabled.

Replace ANY INNER JOIN with IN

In this test, the IN query was 2.7 times faster and used 83% less peak memory. This query counts posts that do not mention ClickHouse but have at least one comment that does:
This query returns 86 as a result. Its median elapsed time was 27.101 seconds, with 970.01 MiB peak memory. It read 150.20 million rows and 51.77 GiB of data. The query needs only the existence of a matching comment. An IN subquery avoids producing a JOIN result:
The IN query also returns 86. Its median elapsed time was 10.206 seconds, with 162.81 MiB peak memory. It read 174.20 million rows and 19.02 GiB of data.

Filter both sides before replacing the JOIN

In this test, the rewrite reduced median elapsed time by 18% and peak memory by 14%. This query counts up-votes for Java-related posts created since 2020. The date condition is applied to both inputs before the JOIN:
This query returns 261915 as a result. Its median elapsed time was 0.850 seconds, with 59.96 MiB peak memory. It read 80.94 million rows and 1.13 GiB of data. Only the existence of a matching post is required for each vote, so the JOIN can then be replaced with IN while retaining filters on both inputs:
The IN query also returns 261915. Its median elapsed time was 0.696 seconds, with 51.70 MiB peak memory. It read 46.20 million rows and 817.49 MiB of data.

Further reading