title_wip

### Load balancing across processing lanes[​](#load-balancing-across-processing-lanes "Direct link to Load balancing across processing lanes")

Note that the `Resize` operators in the physical plan above [repartition and redistribute](/docs/academic_overview#4-2-multi-core-parallelization) data block streams across processing lanes to keep them evenly utilized. This rebalancing is especially important when data ranges vary in how many rows match the query predicates, otherwise, some lanes may become overloaded while others sit idle. By redistributing the work, faster lanes effectively help out slower ones, optimizing overall query runtime.

## Why max\_threads isn't always respected[​](#why-max-threads-isnt-always-respected "Direct link to Why max_threads isn't always respected")

As mentioned above, the number of `n` parallel processing lanes is controlled by the `max_threads` setting, which by default matches the number of CPU cores available to ClickHouse on the server:

```
SELECT getSetting('max_threads');
```

```
   ┌─getSetting('max_threads')─┐
1. │                        59 │
   └───────────────────────────┘
```

However, the `max_threads` value may be ignored depending on the amount of data selected for processing:

```
EXPLAIN PIPELINE
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
WHERE town = 'LONDON';
```

```
...   
(ReadFromMergeTree)
MergeTreeSelect(pool: PrefetchedReadPool, algorithm: Thread) × 30
```

As shown in the operator plan extract above, even though `max_threads` is set to `59`, ClickHouse uses only **30** concurrent streams to scan the data.

Now let's run the query:

```
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
WHERE town = 'LONDON';
```

```
   ┌─max(price)─┐
1. │  594300000 │ -- 594.30 million
   └────────────┘
   
1 row in set. Elapsed: 0.013 sec. Processed 2.31 million rows, 13.66 MB (173.12 million rows/s., 1.02 GB/s.)
Peak memory usage: 27.24 MiB.
```

As shown in the output above, the query processed 2.31 million rows and read 13.66MB of data. This is because, during the index analysis phase, ClickHouse selected **282 granules** for processing, each containing 8,192 rows, totaling approximately 2.31 million rows:

```
EXPLAIN indexes = 1
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
WHERE town = 'LONDON';
```

```
    ┌─explain───────────────────────────────────────────────┐
 1. │ Expression ((Project names + Projection))             │
 2. │   Aggregating                                         │
 3. │     Expression (Before GROUP BY)                      │
 4. │       Expression                                      │
 5. │         ReadFromMergeTree (uk.uk_price_paid_simple)   │
 6. │         Indexes:                                      │
 7. │           PrimaryKey                                  │
 8. │             Keys:                                     │
 9. │               town                                    │
10. │             Condition: (town in ['LONDON', 'LONDON']) │
11. │             Parts: 3/3                                │
12. │             Granules: 282/3609                        │
    └───────────────────────────────────────────────────────┘
```

Regardless of the configured `max_threads` value, ClickHouse only allocates additional parallel processing lanes when there's enough data to justify them. The "max" in `max_threads` refers to an upper limit, not a guaranteed number of threads used.

What "enough data" means is primarily determined by two settings, which define the minimum number of rows (163,840 by default) and the minimum number of bytes (2,097,152 by default) that each processing lane should handle:

For shared-nothing clusters:

- [merge\_tree\_min\_rows\_for\_concurrent\_read](https://clickhouse.com/docs/operations/settings/settings#merge_tree_min_rows_for_concurrent_read)
- [merge\_tree\_min\_bytes\_for\_concurrent\_read](https://clickhouse.com/docs/operations/settings/settings#merge_tree_min_bytes_for_concurrent_read)

For clusters with shared storage (e.g. ClickHouse Cloud):

- [merge\_tree\_min\_rows\_for\_concurrent\_read\_for\_remote\_filesystem](https://clickhouse.com/docs/operations/settings/settings#merge_tree_min_rows_for_concurrent_read_for_remote_filesystem)
- [merge\_tree\_min\_bytes\_for\_concurrent\_read\_for\_remote\_filesystem](https://clickhouse.com/docs/operations/settings/settings#merge_tree_min_bytes_for_concurrent_read_for_remote_filesystem)

Additionally, there's a hard lower limit for read task size, controlled by:

- [Merge\_tree\_min\_read\_task\_size](https://clickhouse.com/docs/operations/settings/settings#merge_tree_min_read_task_size) + [merge\_tree\_min\_bytes\_per\_task\_for\_remote\_reading](https://clickhouse.com/docs/operations/settings/settings#merge_tree_min_bytes_per_task_for_remote_reading)

Don't modify these settings

We don't recommend modifying these settings in production. They're shown here solely to illustrate why `max_threads` doesn't always determine the actual level of parallelism.

For demonstration purposes, let's inspect the physical plan with these settings overridden to force maximum concurrency:

```
EXPLAIN PIPELINE
SELECT
   max(price)
FROM
   uk.uk_price_paid_simple
WHERE town = 'LONDON'
SETTINGS
  max_threads = 59,
  merge_tree_min_read_task_size = 0,
  merge_tree_min_rows_for_concurrent_read_for_remote_filesystem = 0, 
  merge_tree_min_bytes_for_concurrent_read_for_remote_filesystem = 0;
```

```
...   
(ReadFromMergeTree)
MergeTreeSelect(pool: PrefetchedReadPool, algorithm: Thread) × 59
```

Now ClickHouse uses 59 concurrent streams to scan the data, fully respecting the configured `max_threads`.

This demonstrates that for queries on small datasets, ClickHouse will intentionally limit concurrency. Use setting overrides only for testing—not in production—as they can lead to inefficient execution or resource contention.

## Key takeaways[​](#key-takeaways "Direct link to Key takeaways")

- ClickHouse parallelizes queries using processing lanes tied to `max_threads`.
- The actual number of lanes depends on the size of data selected for processing.
- Use `EXPLAIN PIPELINE` and trace logs to analyze lane usage.

## Where to find more information[​](#where-to-find-more-information "Direct link to Where to find more information")

If you'd like to dive deeper into how ClickHouse executes queries in parallel and how it achieves high performance at scale, explore the following resources:

- [Query Processing Layer – VLDB 2024 Paper (Web Edition)](/docs/academic_overview#4-query-processing-layer) - A detailed breakdown of ClickHouse's internal execution model, including scheduling, pipelining, and operator design.
- [Partial aggregation states explained](https://clickhouse.com/blog/clickhouse_vs_elasticsearch_mechanics_of_count_aggregations#-multi-core-parallelization) - A technical deep dive into how partial aggregation states enable efficient parallel execution across processing lanes.
- A video tutorial walking in detail through all ClickHouse query processing steps:

- [Step-by-step: How ClickHouse parallelizes an aggregation query](#step-by-step-how-clickHouse-parallelizes-an-aggregation-query)
  - [Distributing work across processing lanes](#distributing-work-across-processing-lanes)- [Processing queries on sharded tables](#processing-queries-on-sharded-tables)- [Monitoring query parallelism](#monitoring-query-parallelism)
    - [Load balancing across processing lanes](#load-balancing-across-processing-lanes)- [Why max\_threads isn't always respected](#why-max-threads-isnt-always-respected)- [Key takeaways](#key-takeaways)- [Where to find more information](#where-to-find-more-information)

Was this page helpful?