Skip to content

ClickHouse Release 26.6

neutral avatar 400804ae96
ClickHouse
Jul 1, 2026 Β· 11 minutes read

Another month goes by, which means it’s time for another release!

The ClickHouse 26.6 release contains 56 new features 🌞 79 performance optimizations πŸ–οΈ 366 bug fixes 🍦

This release introduces hypothetical skip indexes, cascading refreshable materialized views, experimental support for continuous queries, and more!

New contributors

A special welcome to all the new contributors in 26.6! The growth of ClickHouse's community is humbling, and we are always grateful for the contributions that have made ClickHouse so popular.

Below are the names of the new contributors:

Aditya Chopra, Alasdair Brown, Almaz Kunpeissov, Andriy Yakovlev, Antonio Filipovic, Asya Shneerson, Avenir Voronov, Dmitriy Borisenko, Elian Gidoni, Hanzi Jiang, Harikrishnan Prabakaran, Joe Smith, Joey Yu, Le Zhang, Lefteris Gilmaz, Maksim Dergousov, Maksim Moisiuk, Mathuranath Metivier, Minh Vu, Mohamed Abdelhalim, Mohamed Hussain, MunMunMiao, Patrick Pichler, Ramarajusairajesh, Rory Shanks, SKULLFIRE07, Saarthak Gupta, Sacheendra Talluri, Sergey Kuznetsov, Thomas Cabral, Valerii Mordovskii, Valerii Petrov, Varoon Pazhyanur, Venkata Vineel, Vinayak Joshi, Walt Ribeiro, Youssef Kadry, abdelhalim, abduldjafar, alexbakharew, andyzzhao, bernardlim, daxzel, harikrishnan94, leonard9893, linjiayu, mzitnik, ofeliacode, siwakorn.r, sugaf1204, thewisenerd, uber, uwezkhan, valerypetrov, yousefQadry, zhiqiang-hhhh

Hint: if you’re curious how we generate this list… here.

You can also view the slides from the presentation.

Hypothetical skip indexes

Contributed by Yarik Briukhovetskyi

Starting from ClickHouse 26.6, it’s possible to ask "what if I had this skip index?" without having to build it.

Hypothetical indexes live only in the current session and are invisible to other sessions and discarded when the session ends.

We tried them out on the UK properties dataset, having duplicated the partitions a few times so that we had more rows to work with:

ALTER TABLE uk_price_paid
ATTACH PARTITION ID 'all'
FROM uk_price_paid;

We have almost 500 million rows:

SELECT count() FROM uk_price_paid;
β”Œβ”€β”€β”€count()─┐
β”‚ 487239408 β”‚ -- 487.24 million
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

1 row in set. Elapsed: 0.006 sec.

We run the following query to find the districts in London with the most sales:

SELECT district, count(), round(avg(price)) AS avgPrice
FROM uk_price_paid
WHERE town = 'LONDON'
GROUP BY ALL
ORDER BY count() DESC
LIMIT 10;
β”Œβ”€district────────────┬─count()─┬─avgPrice─┐
β”‚ WANDSWORTH          β”‚ 3258048 β”‚   496367 β”‚
β”‚ LAMBETH             β”‚ 2354352 β”‚   402424 β”‚
β”‚ CITY OF WESTMINSTER β”‚ 2164304 β”‚  1215976 β”‚ -- 1.22 million
β”‚ TOWER HAMLETS       β”‚ 2098864 β”‚   473783 β”‚
β”‚ LEWISHAM            β”‚ 2022208 β”‚   291688 β”‚
β”‚ SOUTHWARK           β”‚ 1998816 β”‚   462604 β”‚
β”‚ BARNET              β”‚ 1942384 β”‚   449124 β”‚
β”‚ GREENWICH           β”‚ 1874464 β”‚   316369 β”‚
β”‚ WALTHAM FOREST      β”‚ 1813360 β”‚   270709 β”‚
β”‚ NEWHAM              β”‚ 1706352 β”‚   284768 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

10 rows in set. Elapsed: 0.317 sec. Processed 487.24 million rows, 1.20 GB (1.54 billion rows/s., 3.80 GB/s.)
Peak memory usage: 894.97 KiB.

We can probably improve the performance of this query by adding a set skip index on the town column.

A set skip index would store the unique values for that column for the provided number of granules. At query time, ClickHouse could refer to this skip-index set to determine whether it needs to scan a particular granule/granules.

Before 26.6, we’d need to create that skip index and test it out, but now we can create a hypothetical index instead. And, in fact, we’re going to create two hypothetical indexes so that we can see the difference between creating a skip index per granule compared to one for every 128 granules:

CREATE HYPOTHETICAL INDEX town_set_10_granularity_1
ON uk_price_paid (town)
TYPE set(10)
GRANULARITY 1;

CREATE HYPOTHETICAL INDEX town_set_10_granularity_128
ON uk_price_paid (town)
TYPE set(10)
GRANULARITY 128;

Once we’ve done that, we can prefix our district query with EXPLAIN WHATIF:

EXPLAIN WHATIF
SELECT district, count(), round(avg(price)) AS avgPrice
FROM uk_price_paid
WHERE town = 'LONDON'
GROUP BY ALL
ORDER BY count() DESC
LIMIT 10;

When we run that query, ClickHouse will read table data to build the candidate index in memory, and scan counts against the session's read limits and quotas. The output of running the query is shown below:

β”Œβ”€explain───────────────────────────────────────────────┐
β”‚ Baseline (after PK + partition + existing indexes):   β”‚
β”‚   table:       default.uk_price_paid                  β”‚
β”‚   parts:       3                                      β”‚
β”‚   marks:       59479                                  β”‚
β”‚   est_bytes:   831.90 MiB                             β”‚
β”‚                                                       β”‚
β”‚ With town_set_10_granularity_128 (set, hypothetical): β”‚
β”‚   status:       applicable                            β”‚
β”‚   marks:        13702                                 β”‚
β”‚   est_bytes:    191.64 MiB                            β”‚
β”‚   skip_ratio:   77.0%                                 β”‚
β”‚                                                       β”‚
β”‚ Estimation:                                           β”‚
β”‚   source:           empirical                         β”‚
β”‚   empirical_status: ok                                β”‚
β”‚   sampled_parts:    3 / 3                             β”‚
β”‚   sampled_marks:    59479 / 118964                    β”‚
β”‚   elapsed_us:       3826267                           β”‚
β”‚                                                       β”‚
β”‚ With town_set_10_granularity_1 (set, hypothetical):   β”‚
β”‚   status:       applicable                            β”‚
β”‚   marks:        4663                                  β”‚
β”‚   est_bytes:    65.22 MiB                             β”‚
β”‚   skip_ratio:   92.2%                                 β”‚
β”‚                                                       β”‚
β”‚ Estimation:                                           β”‚
β”‚   source:           empirical                         β”‚
β”‚   empirical_status: ok                                β”‚
β”‚   sampled_parts:    3 / 3                             β”‚
β”‚   sampled_marks:    59479 / 118964                    β”‚
β”‚   elapsed_us:       4283731                           β”‚
β”‚                                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

32 rows in set. Elapsed: 8.115 sec. Processed 974.48 million rows, 1.95 GB (120.08 million rows/s., 240.11 MB/s.)
Peak memory usage: 269.35 KiB.

If we look at row 12, we can see that the index for 128 granules will skip 77% of granules for this query, whereas on row 25, the index per granule would instead skip 92% of granules.

This gives us some useful information before deciding whether to create a skip index and what settings to use.

Cascading refreshable materialized views

Contributed by Michael Kolupaev

ClickHouse 26.6 introduces an overhaul of how dependencies work for refreshable materialized views.

Before this release, it was possible to create dependencies between refreshable materialized views, but the dependent views still ran on their own independent timers. This meant that latency could build up between stages if the schedules drifted, and views could skip or lag by a full refresh cycle.

Let’s have a look at how to set things up with the following set of tables that represent IMDB data:

CREATE TABLE actor_summary
(
    `id` UInt32,
    `name` String,
    `movies` UInt16,
    `avg_rank` Float32,
    `genres` UInt16,
    `directors` UInt16,
    `updated_at` DateTime
)
ENGINE = MergeTree
ORDER BY movies;
CREATE TABLE actor_rank
(
    `id` UInt32,
    `name` String,
    `movies` UInt16,
    `avg_rank` Float32,
    `genres` UInt16,
    `directors` UInt16,
    `updated_at` DateTime
)
ENGINE = MergeTree
ORDER BY movies;
CREATE TABLE actor_rank_over_time
(
    `id` UInt32,
    `name` String,
    `avg_rank` Float32,
    `as_of` DateTime
)
ENGINE = MergeTree
ORDER BY as_of;

We previously populated these tables like this:

CREATE MATERIALIZED VIEW actor_summary_mv
REFRESH EVERY 2 MINUTES TO actor_summary AS
...
CREATE MATERIALIZED VIEW actor_rank_mv
REFRESH EVERY 1 MINUTE DEPENDS ON actor_summary_mv
TO imdb.actor_rank AS
SELECT *
FROM actor_summary
WHERE movies > 10
ORDER BY avg_rank DESC
LIMIT 5;
CREATE MATERIALIZED VIEW actor_rank_over_time_mv
REFRESH EVERY 1 MINUTE DEPENDS ON actor_rank_mv
APPEND
TO imdb.actor_rank_over_time AS
SELECT id, name, avg_rank, now() AS as_of
FROM actor_rank
ORDER BY avg_rank DESC
LIMIT 1;

Now, only actor_summary_mv has a timer, but we don’t need to specify one for actor_rank_mv or actor_rank_over_time_mv. So, we end up with the following:

CREATE MATERIALIZED VIEW actor_summary_mv
REFRESH EVERY 2 MINUTES TO actor_summary AS
...
CREATE MATERIALIZED VIEW actor_rank_mv
REFRESH DEPENDS ON actor_summary_mv
TO imdb.actor_rank AS
SELECT *
FROM actor_summary
WHERE movies > 10
ORDER BY avg_rank DESC
LIMIT 5;
CREATE MATERIALIZED VIEW actor_rank_over_time_mv
REFRESH DEPENDS ON actor_rank_mv
APPEND
TO actor_rank_over_time AS
SELECT id, name, avg_rank, now() AS as_of
FROM imdb.actor_rank
ORDER BY avg_rank DESC
LIMIT 1;

ALTER TABLE … ADD ENUM VALUES

Contributed by Ilya Golshtein

We can write the following query to find the enum columns in our uk_price_paid table:

SELECT name, type
FROM system.columns
WHERE table = 'uk_price_paid'
  AND database = 'default'
  AND type LIKE 'Enum%'
FORMAT Vertical;
Row 1:
──────
name: type
type: Enum8('other' = 0, 'terraced' = 1, 'semi-detached' = 2, 'detached' = 3, 'flat' = 4)

Row 2:
──────
name: duration
type: Enum8('unknown' = 0, 'freehold' = 1, 'leasehold' = 2)

Prior to ClickHouse 26.6, if we wanted to add a new enum value, we’d need to also provide the existing values when doing so:

ALTER TABLE uk_price_paid
MODIFY COLUMN type Enum8(
 'other' = 0, 
 'terraced' = 1,
 'semi-detached' = 2,
 'detached' = 3,
 'flat' = 4,
 'royal' = 5
);

It’s now possible to append a new value using the ADD ENUM VALUES syntax:

ALTER TABLE uk_price_paid 
MODIFY COLUMN type
ADD ENUM VALUES('royal' = 5);

And if we re-run the query to show our enum columns:

Row 1:
──────
name: type
type: Enum8('other' = 0, 'terraced' = 1, 'semi-detached' = 2, 'detached' = 3, 'flat' = 4, 'royal' = 5)

Row 2:
──────
name: duration
type: Enum8('unknown' = 0, 'freehold' = 1, 'leasehold' = 2)

2 rows in set. Elapsed: 0.006 sec.

help in the CLI

Contributed by Alexey Milovidov

If you’re in the ClickHouse zone and need to look up some documentation, there’s no need to move out of the CLI to search the docs or ask your AI agent for the answer. Instead, you can type help followed by the topic, and you’ll get back in-line documentation.

help Geometry;
GEOMETRY  (Data Type)
─────────────────────────────────────────────────────────────────────────────────────────

Alias of Geometry.

Geometry  (Data Type)
─────────────────────────────────────────────────────────────────────────────────────────

Geometry is a Variant type that can hold any of the geometric data types: Point,
LineString, MultiLineString, Polygon, MultiPolygon, or Ring.

Syntax

    Geometry

Related: Point

This is backed by the new system.documentation table, which you can also query directly:

SELECT type, count()
FROM system.documentation
GROUP BY type
ORDER by count() DESC
LIMIT 10;
β”Œβ”€type───────────────┬─count()─┐
β”‚ Function           β”‚    1593 β”‚
β”‚ Setting            β”‚    1549 β”‚
β”‚ Server Setting     β”‚     412 β”‚
β”‚ MergeTree Setting  β”‚     316 β”‚
β”‚ Aggregate Function β”‚     197 β”‚
β”‚ Data Type          β”‚     140 β”‚
β”‚ Format             β”‚     109 β”‚
β”‚ Table Engine       β”‚      79 β”‚
β”‚ Table Function     β”‚      66 β”‚
β”‚ Dictionary Layout  β”‚      19 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

10 rows in set. Elapsed: 0.019 sec. Processed 4.54 thousand rows, 4.36 MB (240.42 thousand rows/s., 230.69 MB/s.)
Peak memory usage: 6.18 MiB.

Transform clickhouse-local to a server

Contributed by Alexey Milovidov

clickhouse-local is our go-to tool for doing any ad hoc data analysis, but sometimes you want to hook up your ClickHouse instance to external tools, which wasn’t straightforward.

As of 26.6, you can now have clickhouse-local listen for connections on the fly:

SYSTEM START LISTEN TCP;
SYSTEM START LISTEN HTTP;

You can then connect to it as you’re running ClickHouse Server. And once you’re done, it’s easy enough to stop listening on those ports:

SYSTEM STOP LISTEN TCP;
SYSTEM STOP LISTEN HTTP;

Lighter, faster query startup

Contributed by RaΓΊl MarΓ­n, Dmitry Novik, Max Justus Spransy, Azat Khuzhin

There are a series of improvements in 26.6 that significantly reduce per-query overhead for simple queries.

Deeply nested queries, in particular, are now analyzed more efficiently. Let’s run the following (unnecessarily complex) query:

SELECT * FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM (
SELECT * FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM (
SELECT * FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM (
SELECT * FROM (SELECT * FROM (SELECT * FROM (SELECT * FROM (
SELECT * FROM (SELECT * FROM (SELECT * FROM (
    SELECT * FROM uk_price_paid LIMIT 10
)))))))))))))))))));

Against 26.5:

10 rows in set. Elapsed: 0.098 sec.
10 rows in set. Elapsed: 0.102 sec.
10 rows in set. Elapsed: 0.100 sec.

And against 26.6:

10 rows in set. Elapsed: 0.030 sec.
10 rows in set. Elapsed: 0.036 sec.
10 rows in set. Elapsed: 0.050 sec.

The best time on 26.6 was 30 milliseconds, compared to the best of 98 milliseconds on 26.5, a roughly 3 times improvement.

Continuous queries

Contributed by Mikhail Artemenko

We also have the introduction of streaming queries in experimental mode. You can now write a query that never ends by appending STREAM. The query will keep emitting new rows as they are inserted.

To enable this feature, you can use the following setting:

SET enable_streaming_queries = 1;

And then, we could write the following query that blocks and keep streaming new rows:

SELECT id, msg 
FROM live_events STREAM;

As new rows are added to live_events, they would be returned by the above query.

This feature can also be used in a more advanced mode with cursors:

SELECT _block_number AS bn, _block_offset AS bo, id, msg
FROM events STREAM 
CURSOR {'all': {'block_number': 2, 'block_offset': 0}};

As of ClickHouse 26.6, this feature is only available for Linux.

Get started today

Interested in seeing how ClickHouse works on your data? Get started with ClickHouse Cloud in minutes and receive $300 in free credits.

Sign up

Share this post

  • Y Combinator icon
  • X icon
  • Bluesky icon
  • Facebook icon
  • LinkedIn icon

Subscribe to our newsletter

Stay informed on feature releases, product roadmap, support, and cloud offerings!

Recent posts