ClickHouse: My Favorite Features

Author: Alexey Milovidov, ClickHouse User Council, Amsterdam, 2026.

My Favorite ClickHouse Features

Photo by Eduard Gordeev, https://www.instagram.com/eduard_gordeev_

My favorite features

One year of ClickHouse

From 25.8 to 26.8 — thirteen releases:

512 new features 🍇

703 performance optimizations 🌠

2825 bug fixes 🐝

Which one is your favorite?

Query Engine

DateTime64 and Date32: years 0 to 9999

The supported range grew from [1900, 2299]
to [0000-01-01, 9999-12-31]:

SELECT toDateTime64('9999-12-31 23:59:59.999', 3); -- 9999-12-31 23:59:59.999 (was clamped to 2299-12-31) SELECT age('year', toDateTime64('0079-08-24 13:00:00', 0), now()); -- 1946 years since the eruption of Vesuvius SELECT toDateTime64('1789-07-14 12:00:00', 0) + INTERVAL 237 YEAR; -- 2026-07-14 12:00:00

In 26.8, Date32 got the same range — parsing and conversions
accept it instead of silently clamping.

Uses the Proleptic Gregorian Calendar.

Developer: Alexey Milovidov.

Correlated Subqueries

— are subqueries that depend on the columns from the outer scope.

SELECT * FROM users AS u1 WHERE EXISTS ( SELECT * FROM users2 AS u2 WHERE u1.age = u2.age)

They can appear in many different contexts: EXISTS, IN, scalar...

In 25.4 we supported correlated subqueries inside the WHERE clause with the EXISTS operator.

In 25.5 we support scalar correlated subqueries inside the WHERE clause
and correlated subqueries inside SELECT!

Now it is enough to cover the TPC-H test suite without modifying queries.

Developer: Dmitry Novik.

Materialized CTE

CTEs (subqueries in the WITH clause) can now be evaluated only once
and stored in temporary tables:

SET enable_materialized_cte = 1; WITH top_users AS MATERIALIZED ( SELECT user_id, count() AS cnt FROM events GROUP BY user_id ORDER BY cnt DESC LIMIT 1000) SELECT * FROM top_users INNER JOIN (SELECT user_id FROM top_users WHERE ...) ...

Developer: Dmitry Novik.

JOIN Reordering

Reordering of the JOIN graph, based on the amount of data to read
and on the column-level statistics.

:) SET query_plan_optimize_join_order_limit = 10; :) SET allow_statistics_optimize = 1;

Developer: Vladimir Cherkasov.

Column statistics by default

29% improvement across all the benchmarks.

4.5 times improvement on TPC-H
(an old synthetic benchmark for JOINs).

JOIN Reordering, by default

Since 25.12, JOIN reordering is enabled by default:

query_plan_optimize_join_order_limit = 10 -- was 1 allow_statistics_optimize = 1 -- was 0

— The optimizer reorders up to 10 tables using column-level statistics.

— In 26.4, statistics are collected by default on merges;
  in 26.8, also on INSERT — so freshly loaded dimension tables
  already have good estimates.

Developers: Vladimir Cherkasov, Alexander Gololobov, Nikita Taranov.

DPsub JOIN reordering

A new JOIN-order optimization algorithm —
dynamic programming over subsets:

SET query_plan_optimize_join_order_algorithm = 'dpsub';

— Considers all JOIN orders — finds the optimal plan.
— Lower optimization overhead than dpsize.
— Supports non-inner joins, unlike dpsize and dphyp.
— Algorithms can be chained: 'dpsub,greedy' — tried in order,
  with fallback for queries an algorithm cannot handle.

Developer: Fisnik Kastrati.

Query cache for subqueries

You can control query caching on a per-subquery basis:

-- Enable query cache only for outmost query, as usual: SELECT * FROM (SELECT * FROM table) SETTINGS use_query_cache = 1; -- Enable query cache for subquery (new in 26.5): SELECT * FROM (SELECT * FROM table SETTINGS use_query_cache = 1); -- Enable propagation of query cache into all subqueries (new in 26.5): SELECT * FROM (SELECT * FROM table) SETTINGS use_query_cache_for_subqueries = 1; -- Enable propagation of query cache into all subqueries but disable in one: SELECT * FROM (SELECT * FROM table1) t1 NATURAL JOIN (SELECT * FROM table2 SETTINGS use_query_cache = 0) t2 SETTINGS use_query_cache_for_subqueries = 1;

Developer: Nikita Barannik, Vincent Voyer.

Distributed Processing For Large Files

ClickHouse runs distributed queries:
— with Distributed tables;
— with Replicated tables, using "parallel replicas";
— on top of a bunch of external files, e.g., Parquet files on S3,
  using the s3Cluster table function.

But what if there is only a single (or a few) large files?

Since 25.11, ClickHouse splits the work using ranges
(row groups) inside single files!

Many servers of the cluster will "eat" different portions of a file.

Developer: Konstantin Vedernikov.

Automatic Parallel Replicas

25.12 introduces automatic_parallel_replicas_mode,
which collects statistics in the runtime and decides when
— a query is heavy enough and has to be run distributed;
— a query is quick or has a lot of network transfer;

Developer: Nikita Taranov.

Max-Min Scheduler

The server can be configured with concurrent_threads_scheduler,
which gives better latency distribution on highly concurrent queries:

$ cat config.d/scheduler.yaml concurrent_threads_scheduler: max_min_fair

Developer: Sergei Trifonov.

Async Insert by default

Starting from 26.3 LTS, async inserts are enabled by default.

— ClickHouse will batch all small inserts automatically.
— No configuration changes needed for most users.
— Reduces the number of parts created by frequent small inserts.

New: consistent mechanism of deduplication of both regular and asynchronous inserts, including inserts with materialized views.

Developer: Sema Checherinda.

Data Lakes

Data Lakes Performance

Reading from data lakes is now tens of times faster on multi-core machines, when reading from a small number of files.

Applies to Iceberg, Delta Lake, Hudi, and all object storage reads.

Developer: Alexey Milovidov.

Dictionary-based filtering in Parquet

A new way to skip whole row groups, for equality and IN conditions:

SELECT count() FROM s3('s3://clickhouse-public-datasets/hits_compatible/hits.parquet') WHERE MobilePhoneModel = 'GT-C3262'; -- 200 rows out of 100 million; the value is read from the dictionary page: -- if it is not in the dictionary, the whole row group is skipped

Example: row groups read: 22612 — only the ones with this value.

— Works where min/max statistics cannot help (every row group
  spans the value) and bloom filters are absent — most Parquet
  writers, including the one that wrote hits.parquet, do not write them.
— Applies when a column chunk is fully dictionary-encoded;
  controlled by input_format_parquet_dictionary_filter_push_down.

Developer: Alexey Milovidov.

Lazy reading in Parquet

Read the ORDER BY columns first,
then read only records that survive the LIMIT for the rest:

SELECT URL, Title FROM s3('s3://clickhouse-public-datasets/hits_compatible/hits.parquet') ORDER BY EventTime DESC LIMIT 10; -- 100 million rows, 14 GB

26.7: 4.9 sec, 5.85 GB read.
26.8: 1.7 sec, 0.53 GB read (11 times less).

Developer: Alexey Milovidov.

Parquet Metadata Cache

A new SLRU cache for Parquet metadata (footer):

— Up to 2x reduction of reads on regular queries.
— Especially beneficial for repeated queries over the same Parquet files.

SET use_parquet_metadata_cache;

Enabled by default.

Consistency is guaranteed by tracking file modifications by etag.

Developer: Grant Holly.

Metadata prefetching for Iceberg

Iceberg tables now support asynchronous metadata prefetching:

CREATE TABLE my_iceberg (...) ENGINE = IcebergS3(...) SETTINGS iceberg_metadata_async_prefetch_period_ms = 60000; SELECT ... FROM my_iceberg SETTINGS iceberg_metadata_staleness_ms = 30000;

— Periodically pre-populates the metadata cache.
— SELECT queries use cached metadata if fresh enough,
 eliminating calls to the Iceberg catalog.

Developer: Arsen Muk.

Query condition cache for Iceberg

The query condition cache — the ephemeral index that remembers which
granules don't satisfy a WHERE clause — now works for Iceberg tables too:

SET use_query_condition_cache = 1; SELECT count() FROM iceberg('s3://bucket/events/') WHERE event_date = today() AND user_id = 42; -- Subsequent queries with the same predicate skip irrelevant data files.

— Especially useful for repeated dashboard queries.

Developer: Konstantin Vedernikov.

PREWHERE and more for Iceberg

SELECTs from Iceberg now use the PREWHERE optimization.

Iceberg tables support ALTER TABLE RENAME COLUMN.

INSERTs into Iceberg are production ready!

Developers: Konstantin Vedernikov, murphy-4o.

Every Data Lake Catalog

ClickHouse has database engines for:

— REST, Polaris catalog (since 24.12);
— Unity catalog (since 25.3);
— AWS Glue catalog (since 25.3);
— Hive Metastore (since 25.5);
— Microsoft Fabric OneLake (since 25.11);
— Google BigLake (since 26.2);
— AWS S3 Tables (since 26.8);
Snowflake Horizon (since 26.8);

And table engines for Iceberg, Delta Lake, Apache Paimon, Hudi.

Developers: Konstantin Vedernikov, Melvyn Peignon, Daniil Ivanik, Arsen Muk, and others.

Fabric OneLake

OneLake - Microsoft Fabric's unified data lake,
powered by the OneLake Tables APIs and Apache Iceberg.

Compatible with the Iceberg REST Catalog.

CREATE DATABASE onelake ENGINE = DataLakeCatalog('https://onelake.table.fabric.microsoft.com/iceberg') SETTINGS catalog_type = 'onelake', warehouse = 'warehouse_id/data_item_id', onelake_tenant_id = '<tenant_id>', oauth_server_uri = 'https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token', auth_scope = 'https://storage.azure.com/.default', onelake_client_id = '<client_id>', onelake_client_secret = '<client_secret>'

Developers: Konstantin Vedernikov.

Google BigLake

CREATE DATABASE biglake ENGINE = DataLakeCatalog( 'https://biglake.googleapis.com/iceberg/v1/restcatalog') SETTINGS catalog_type = 'biglake', google_adc_credentials_file = '/home/ubuntu/.config/gcloud/application_default_credentials.json', warehouse = 'gs://biglake-public-nyc-taxi-iceberg'

Demo

Developer: Konstantin Vedernikov.

S3 Tables

A catalog for AWS S3 Tables — Amazon's managed Iceberg buckets:

CREATE DATABASE tables ENGINE = DataLakeCatalog( 'https://s3tables.us-east-1.amazonaws.com/iceberg') SETTINGS catalog_type = 's3tables', region = 'us-east-1', warehouse = 'arn:aws:s3tables:us-east-1:1234567890:bucket/analytics'; SHOW TABLES FROM tables; SELECT … FROM tables.`ns.events`; INSERT INTO tables.`ns.events` VALUES (…); -- writes work now

— Reading arrived earlier; 26.8 brings a working INSERT
  and you can create your own tables.
— Requests are SigV4-signed; IAM role support included.

Developer: Konstantin Vedernikov.

Snowflake Horizon

Query — and write — Iceberg tables behind the
Snowflake Horizon catalog:

CREATE DATABASE horizon ENGINE = DataLakeCatalog( 'https://<org>-<account>.snowflakecomputing.com/polaris/api/catalog') SETTINGS catalog_type = 'horizon', warehouse = 'ICEBERG_DB', catalog_credential = '<PAT>', vended_credentials = 1; SELECT … FROM horizon.`schema.trades`; -- reads Iceberg directly INSERT INTO horizon.`schema.trades` …; -- commits via the catalog

— ClickHouse speaks to the Snowflake-hosted Iceberg REST endpoint —
  the data files are read from object storage directly,
  without spending Snowflake compute credits.

Developer: Melvyn Peignon.

Text Index

Text Index is production-ready!

A full-text search index in ClickHouse.

— In development since 2022
— first prototype in 2023 (by Harry Lee and Larry Luo)
— experimental in 25.9
— beta in 25.12
production in 26.2

Developers: Anton Popov, Elmi Ahmadov, Jimmy Aguilar Mena.

Text Index

CREATE TABLE text_log ( message String, ... INDEX inv_idx(message) TYPE text(tokenizer = 'splitByNonAlpha') GRANULARITY 128 ) ENGINE = SharedMergeTree ORDER BY id; SELECT ... WHERE hasToken(message, 'DDLWorker'); SELECT ... WHERE hasAllTokens(message, ['peak', 'memory']); SELECT ... WHERE hasAnyTokens(message, tokens('01442_merge_detach_attach'));

Developers: Anton Popov, Elmi Ahmadov, Jimmy Aguilar Mena.

Text Index (50 TB logs demo)

Developers: Anton Popov, Elmi Ahmadov, Jimmy Aguilar Mena.

Text Index: a new storage format

A new storage format, optimized for object storage.

Developers: Anton Popov.

Text indexes: faster answers

count() is answered directly from the index cardinality metadata:

SELECT count() FROM docs WHERE hasAnyTokens(body, ['fox']); -- 1 million rows matched; rows actually read: 1

— Posting lists are now decoded lazily, at packed-block granularity,
  instead of being eagerly materialized into bitmaps.

— Optional cache for tokens missing from the index.

— The v2 on-disk format (with positions for phrase search) is the default;
text_index_version controls it for rolling upgrades.

Developers: Elmi Ahmadov, Anton Popov, Rory Shanks.

Tokenization: new languages

Four new tokenizers for the tokens function and text indexes:

SELECT tokens('ClickHouse是一个快速的开源数据库', 'chinese'); -- ['ClickHouse','是','一个','快速','的','开源','数据库'] — dictionary + HMM SELECT tokens('日本語の形態素解析エンジン', 'japanese'); -- ['日本語','の','形態','素','解析','エンジン'] — the MeCab morphological analyzer SELECT tokens('こんにちは世界。データベースです。', 'icu', 'ja'); -- ['こんにちは','世界','データベース','です'] — locale-aware Unicode segmentation SELECT tokens('C++ and C# are languages', 'splitByRegexp', '[^\p{L}\p{N}#+]+'); -- ['C++','and','C#','are','languages'] — tokens other tokenizers would break

Developers: Robert Schulze, Amos Bird, Jimmy Aguilar Mena.

Positions for phrase matching

Text indexes can now store token positions — for exact phrase search:

CREATE TABLE docs (…, INDEX idx body TYPE text( tokenizer = 'splitByNonAlpha', support_phrase_search = 1)) … SETTINGS allow_experimental_text_index_phrase_search = 1; SELECT count() FROM docs WHERE hasPhrase(body, 'quick brown fox'); -- 1 — but hasPhrase(body, 'brown quick fox') = 0: order matters

— The phrase test is answered directly from the index:
  in our test, 124 granules → 1, and no column data was read at all.

Developer: Elmi Ahmadov.

Geospatial

Geometry Data Type

We already had all the data types for GIS:

Point, LineString, MultiLineString, Ring, Polygon, MultiPolygon

Since 25.11, we also have one unified data type: Geometry

— it can contain any geometry!

New functions, readWkt, readWkb,
which read any type of geometry as a Geometry value.

ClickHouse also supports H3 and S2 indexes, Geohashes, optimized spherical and geo distances,
Polygonal dictionaries for reverse geocoding, SVG rendering...

Developer: Konstantin Vedernikov.

Spatial pruning for GeoParquet

Geospatial queries over GeoParquet files skip the data
that cannot match:

Row-group pruning: row groups whose bounding box does not
  overlap the query geometry are skipped entirely.
Page-level pruning: the covering.bbox column index
  skips irrelevant pages within the surviving row groups.
— Spatial predicate push-down also applies during row reading.

Load city-scale slices out of planet-scale files —
without reading the planet.

Developer: Vasily Chekalkin.

GeoJSON input format

Read a GeoJSON FeatureCollection — one row per feature:

SELECT id, properties.name, toTypeName(geometry) FROM file('places.geojson', GeoJSON);

idproperties.namegeometry
1LondonGeometry (Point)
2squareGeometry (Polygon)

— Columns: id String, geometry Geometry, properties Nullable(JSON).
— Point, LineString, MultiLineString, Polygon, MultiPolygon supported natively.

Developer: Mark Needham.

GeoJSON output

26.6 added GeoJSON input. 26.7 writes it, too:

SELECT id, (13.4050, 52.5200)::Point AS geometry, 'Berlin' AS name FORMAT GeoJSON

{"type":"FeatureCollection","features":[ {"type":"Feature","id":1, "geometry":{"type":"Point","coordinates":[13.405,52.52]}, "properties":{"name":"Berlin"}}, …]}

— The geo column → geometry, the id column → feature id,
  everything else → properties.
— Feed query results straight to Leaflet, Mapbox, deck.gl, QGIS.

Developers: Mark Needham, Nihal Z. Miaji.

Polygons and bounding boxes as an index

Geospatial predicates can now be answered by the primary key index:

CREATE TABLE points (p Point, ...) ENGINE = MergeTree ORDER BY p; -- or: ORDER BY (lon, lat) -- a tuple of coordinates works too SELECT count() FROM points WHERE pointInPolygon(p, [(2.2, 48.8), (2.4, 48.8), (2.4, 49.0), (2.2, 49.0)]);

— The bounding box of the polygon is derived at index-analysis time
  and turned into a range condition on the key — granules outside it
  are never read.
— The same works for pointInEllipses, and for the first elements
  of a tuple key.

Developer: Alexey Milovidov.

Universal Transverse Mercator

Convert between WGS84 coordinates and the UTM / MGRS systems
used in surveying, mapping, and military grids:

UTM zones: 60 longitude zones and latitude bands C–X on a world map

Map: NASA Visible Earth / Wikimedia Commons, public domain.

SELECT geoToUTM(4.89, 52.36) AS amsterdam; -- (628700.24, 5802760.82, 31, 'U') -- easting, northing, zone, band SELECT UTMToGeo(628700.24, 5802760.82, 31, true); -- (4.89, 52.36) — and back SELECT geoToMGRS(4.89, 52.36); -- '31UFU2870002760' SELECT MGRSToGeo('31UFU2870002760'); -- (4.89, 52.36)

Developer: Alexey Milovidov.

Mapbox Vector Tiles from SQL

Serve map tiles straight out of ClickHouse with the new MVT functions:

-- Project lon/lat into a tile's pixel space: SELECT MVTEncodeGeom((13.37, 52.52)::Point, 10, 550, 335); -- (124, 3384) -- Aggregate a group's geometries into one binary tile: SELECT MVTEncode(geom_in_tile_space) FROM ...; -- Tile bounding box for the WHERE clause: SELECT MVTBoundingBox(12, 1205, 2557);

— Point, line and polygon geometry.
— Also available as PostGIS aliases ST_AsMVTGeom and ST_AsMVT.

Developer: Saarthak Gupta.

More Indices

Projections As Secondary Indices

You can already use projections to have different sort orders of a table:

CREATE TABLE pageviews ( CounterID UInt32, UserID UInt64, EventTime DateTime, ... PROJECTION by_time ( SELECT * ORDER BY EventTime -- projection's order ) ) ORDER BY (CounterID, UserID, EventTime) -- table's main order

And the projection with accelerate range queries or queries with ORDER BY.
However, it requires duplicating the storage for the columns (or a subset).

Can we make it work without duplication?

Projections As Secondary Indices

Yes: since 25.11, a projection can store a single column, _part_offset
— so it acts as an index into the main table instead of a copy of the data.

Since 26.1, there is a dedicated syntax for exactly this:

CREATE TABLE pageviews ( CounterID UInt32, UserID UInt64, EventTime DateTime, ... PROJECTION by_time INDEX EventTime TYPE basic ) ORDER BY (CounterID, UserID, EventTime) ALTER TABLE pageviews ADD PROJECTION by_url INDEX URL TYPE basic;

Accelerates queries using much less extra storage, at the cost of worse
locality of reads: useful for point queries. Demo.

Developer: Amos Bird.

Hypothetical indexes + EXPLAIN WHATIF

Ask "what if I had this skip index?" — without building it:

CREATE HYPOTHETICAL INDEX idx_level ON logs (level) TYPE set(2) GRANULARITY 4; EXPLAIN WHATIF SELECT count() FROM logs WHERE level = 'error';

Baseline (PK + partition + existing indexes): marks: 612, 19.56 MiB With idx_level (set, hypothetical): status: applicable marks: 200 (was 612) est_bytes: 6.39 MiB skip_ratio: 67.3% Estimation: empirical, sampled 5/5 parts

— Session-scoped, visible in system.hypothetical_indexes.
Tune indexes data-driven.

Developer: Yarik Briukhovetskyi.

Streaming For Secondary Indices

In 25.8: reading and analyzing the index happens before the query starts reading the data, which introduces a noticable delay when using large indices.

In 25.9: reading the indices happens together when scanning the data. Queries start faster and can finish earlier, e.g., when LIMIT is reached.

:) SET use_skip_indexes_on_data_read;

Demo

Developer: Amos Bird.

The right table is an index

A JOIN can use the values of the right-hand side to prune granules
of the left table via its primary key or skip indexes:

SET enable_join_runtime_filters_index_analysis = 1; SELECT … FROM facts JOIN lookup ON facts.id = lookup.id; -- the ids from `lookup` become an index condition on `facts`

Measured — 100M-row table joined with a 100-row table:
  granules read: 12216 → 100 (122x),
  time: 0.177 s → 0.022 s (8x).

Developer: Shankar Iyer.

More Interesting

Continuous queries 🧪

A SELECT that never ends — the first step toward streaming queries. Append STREAM and it keeps emitting new rows as they are inserted:

SET enable_streaming_queries = 1; SELECT id, msg FROM live_events STREAM; -- blocks, keeps streaming

Applications: filtering and real-time alerting on a stream of events.

Advanced usage with cursors:

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

Developer: Mikhail Artemenko.

AI functions 🧪

Call LLM endpoints (OpenAI, Anthropic) directly from SQL:

SELECT aiGenerate('Summarize this in one line: ' || review) FROM product_reviews LIMIT 10;

Higher-level helpers built on top:

SELECT aiClassify(text, ['bug', 'feature', 'question']) FROM tickets; SELECT aiExtract(article, 'people, organizations, dates') FROM news; SELECT aiTranslate(message, 'English') FROM logs;

— Provider configured server-side; secrets never leak to clients.

Developer: George Larionov.

Sharded Map

Bucketed serialization for Map columns:

CREATE TABLE tab (id UInt64, m Map(String, UInt64)) ENGINE = MergeTree ORDER BY id SETTINGS map_serialization_version = 'with_buckets', max_buckets_in_map = 32;

2–49x faster single-key lookups depending on map size.
— Uses map_serialization_version_for_zero_level_parts = 'basic'
  to keep insert speed close to baseline.

Developer: Pavel Kruglov.

Indices on JSON columns

MergeTree skip indices now work on the set of JSON paths
using JSONAllPaths:

ALTER TABLE events ADD INDEX paths_idx JSONAllPaths(data) TYPE bloom_filter GRANULARITY 1;

— Supported types: bloom_filter, tokenbf_v1, ngrambf_v1, and text (inverted).
— Skip granules where the JSON paths a query is filtering by are absent.

Developer: Pavel Kruglov.

JSONAllValues + text index

A new function JSONAllValues returns every leaf value
of a JSON column as Array(String):

SELECT JSONAllValues(data) FROM events -- ['42', 'click', '2026-04-30', ...]

Build a text index on it — and it kicks in automatically
for filters on JSON subcolumns:

ALTER TABLE events ADD INDEX vals JSONAllValues(data) TYPE text(tokenizer='ngrams') GRANULARITY 1; SELECT * FROM events WHERE data.event_type = 'click'; -- The text index is used to skip granules.

Developer: Anton Popov.

Lazy type hints for JSON 🧪

Add or modify JSON type hints instantly,
without rewriting data:

SET allow_experimental_json_lazy_type_hints = 1; ALTER TABLE events MODIFY COLUMN data JSON(metrics.count UInt64);

— Metadata-only operation — completes instantly.
— Type hints are applied at query time for old parts.
— Materialized during INSERTs and background merges.

Developer: tanner-bruce.

Faster JSON parsing

For the JSON data type:

Developer: Pavel Kruglov.

Interfaces

PromQL

ClickHouse speaks PromQL — over a TimeSeries table:

CREATE TABLE metrics ENGINE = TimeSeries; SELECT * FROM prometheusQuery('metrics', 'sum by (job) (rate(http_requests_total[5m]))'); -- or switch the whole session to the PromQL dialect: SET dialect = 'promql', promql_table = 'metrics'; sum by (job) (rate(http_requests_total[5m]))

— The Prometheus remote-write and remote-read protocols,
  and the Prometheus HTTP API (/api/v1/query, /api/v1/query_range)
  — so Grafana can point at ClickHouse as if it were Prometheus.
— PromQL is transpiled to SQL and runs on the ClickHouse engine.

Developers: Vitaly Baranov, Nikita Mikhaylov, Valery Petrov, Minh Vu.

PromQL: a real observability backend

A year of work on making it fast and correct:

— Selectors compile into a continuous primary-key range on the samples
  table instead of a huge id IN (...) set — up to 45% lower cold latency
  on dashboard and alerting-rule queries over a 62-billion-sample table.
topk / bottomk / limitk run streaming and parallel, in bounded memory.
— Shared subexpressions are evaluated once (~2x on topk over rate).
— A "recent samples" table: a TTL'd copy that short-range dashboards
  and alerting rules read automatically — several times faster.
— A PromQL compliance suite runs against Prometheus itself in CI.

Developers: Nikita Mikhaylov, Vitaly Baranov, Valery Petrov, Minh Vu.

Arrow Flight SQL

ClickHouse can now serve queries over Arrow Flight SQL
— a high-throughput, columnar gRPC protocol from Apache Arrow.
— Any ADBC client can connect — the standard Arrow database API.

Configured as a separate listener:

$ cat config.d/arrow.yaml arrowflight_port: 9005

import pyarrow.flight as flight client = flight.FlightClient("grpc://localhost:9005") ticket = flight.Ticket(b"SELECT * FROM system.numbers LIMIT 10") reader = client.do_get(ticket) print(reader.read_all())

Developer: Yakov Olkhovskiy.

Web Terminal

An in-browser clickhouse-client:

Point your browser at http://localhost:8123/webterminal
— an interactive client session over a WebSocket, no installation.

Experimental in 26.5; since 26.6 it is a production feature,
enabled by default:

$ cat config.d/webterminal.yaml enable_webterminal: true -- the default; set to false to disable

Demo

Developer: Alexey Milovidov.

CREATE HANDLER

Define custom HTTP endpoints from SQL:

CREATE HANDLER top_pages URL '/top_pages' AS SELECT path, sum(hits) AS total FROM hits GROUP BY path ORDER BY total DESC LIMIT 5;

$ curl http://server:8123/top_pages /page19 254500 /page18 254000

— Parameterized: {p:String} is filled from URL params, form fields,
  headers, or named regexp capture groups on paths.
— INSERT handlers read the POST body.
— ALTER/DROP HANDLER, grants, system.handlers.

Developer: Alexey Milovidov.

API for query construction

New HTTP settings compose a query for you — and turn the HTTP
interface into a REST-style API over tables:

-- enable `http_allow_path_requests` in the server configuration -- and `http_allow_table_as_file` in settings profile GET /hits.tsv.gz -- requests a table, format, and compression GET /hits?format=RowBinary -- the format setting also works GET /hits.tsv?select=a,b GET /hits.tsv?query=SELECT+a,b -- enable `http_allow_database_as_path` GET /db/hits.parquet GET /db/hits.parquet?filter=x>1&order=c+DESC&limit=100&page=2 -- enable `http_allow_filters_as_unrecognized_url_parameters` GET /db/hits.parquet?x=1&y>2&sort=-c -- http_allow_filters_as_path GET /x=1/db/hits.parquet

Developer: Alexey Milovidov.

API for result modification

— new query settings: filter, select, sort, order, page, compression,
  and already existing limit, offset - for out-of-band result modification;

— and new settings format, input_format, output_format
  allow to explicitly override the input/output data format.

These settings wrap the query to alter its presentation for various tools.
They are safe and available in the readonly mode and can be restricted if needed.

database and default_format became normal settings
  rather than being limited to the HTTP interface;

Developer: Alexey Milovidov.

Framing formats

One HTTP response stream can now carry everything: data, progress,
totals, profile events, server logs, and exceptions — multiplexed:

$ curl 'http://server:8123/?query=...&framing_output_format=EventStream' event: data data: {"number":"0"} event: progress data: {"read_rows":"3","elapsed_ns":"7496990",...}

EventStream - HTTP Server-Sent Events, consumable
  by the browser's built-in EventSource API.
JSONEachPacketString / JSONEachPacketBase64 - a JSON per packet.

Live progress bars and streaming results wrapping any data format.

Developer: Alexey Milovidov.

Pipelined SQL

Write queries top-down - each step follows the previous:

FROM hits |> WHERE status = 404 |> AGGREGATE sum(hits) AS total GROUP BY path |> ORDER BY total DESC |> LIMIT 3;

Pipes can apply to any normal query:

SELECT region_id, user_id, sum(session_length) AS total_duration FROM hits GROUP BY user_id |> AGGREGATE avg(total_duration) GROUP BY region_id |> ORDER BY 1 DESC |> LIMIT 100;

Developer: Alexey Milovidov.

Vector Search

Vector Search: the road to GA

22.9 — experimental version

Introduced by Arthur Filatenkov, Vladimir Makarov, Danila Mishin, Nikita Vasilenko, Alexander Piachonkin, Nikita Evsiukov, Hakob Sagatelyan.

23.8 — integration with the USearch library

Introduced by Davit Vardanyan.

25.1 — faster vector indices

By Robert Schulze, Michael Kolupaev.

25.5 — beta (prefiltering, postfiltering, rescoring)

Developer: Shankar Iyer, Robert Schulze.

25.8 — GA (index-only reading, fetch multiplier, binary quantization)

Developer: Shankar Iyer.

Vector Index

In development since 2021. Production-ready since 25.8.

ALTER TABLE dbpedia ADD INDEX vector_index(vector) TYPE vector_similarity( 'hnsw', 'cosineDistance', 1536, 'bf16', 64, 512); ALTER TABLE dbpedia MATERIALIZE INDEX vector_index; WITH ... AS reference_vector SELECT ... FROM table WHERE ... ORDER BY cosineDistance(vector, reference_vector) LIMIT 10;

Developer: Shankar Iyer.

Vector Index: how it works

HNSW algorithm with quantization options (bf16, i8, b1).

Supports different filtering modes:
post-filtering (run ANN search, then apply other filters)
or pre-filtering (apply regular filters, then run ANN across filtered results).

Supports filtering multiplier
(e.g., find 100 nearest candidates to filter and return 10).

Avoids reading the data column if not necessarily.

Developer: Shankar Iyer.

QBit Data Type

A data type for vector embeddings,
that allows tuning the search precision at runtime.

CREATE TABLE vectors ( id UInt64, name String, ... vec QBit(BFloat16, 1536) ) ORDER BY ();

SELECT id, name FROM vectors ORDER BY L2DistanceTransposed(vector, target, 10) LIMIT 10;

Developer: Raufs Dunamalijevs.

QBit: bit-sliced layout

It uses a bit-sliced data layout:
every number is sliced by bits,
e.g., for 1536-dim vector of BFloat16,
we store 16 subcolumns with Nth (1..16th) bits from all dimensions.

At the query time, we specify, how many (most significant) bits to take.

For example, we can ask to read
only 10 out of 16 bits.

QBit: Int8 quantization

QBit now supports Int8, which allows anything from binary (1 bit) to full 8 bit quantization, chosen at runtime per query.

It can store any Int8 codes, and we also provide quantizeBFloat16ToInt8 and dequantizeInt8ToBFloat16 functions for optimal conversion from Float to Int8, minimizing the error on gaussian-distributed vector coordinates.

CREATE TABLE images (id UInt64, emb QBit(Int8, 2048)) …; INSERT INTO images SELECT id, quantizeBFloat16ToInt8(embedding * sqrt(2048)) FROM src; -- scale unit-normalized embeddings by sqrt(dim) and convert to Int8 SELECT id FROM images ORDER BY cosineDistanceTransposedQuantized(emb, target, 4) LIMIT 10; -- reads only the top 4 bit-planes — half the I/O of 8-bit

Developer: Alexey Milovidov.

QBit: strided storage

The optional stride parameter slices dimensions into groups — each bit-plane of each group is its own stream on disk: QBit(Int8, 1024, 256):

dims 1–256 257–512 513–768 769–1024
bit 1 (sign)
bit 2
bit 3
bit 4
bit 5 … 8

… ORDER BY cosineDistanceTransposedQuantized(emb, target, 4, 256) … -- 4 bits × first 256 dims: reads the 4 green streams of 32 — 3% of a Float32 column

You can request reading only first N dimensions - good for MRL (Matryoshka Representation Learning) embeddings to tune between the speed and recall.

Developer: Alexey Milovidov.

Quantization functions

A scalar codec that compresses embedding components to 8bit and below:

SELECT quantizeBFloat16ToInt8(1.5::BFloat16); -- 107 SELECT dequantizeInt8ToBFloat16(quantizeBFloat16ToInt8(1.5::BFloat16)); -- 1.5 (near-lossless round-trip)

— 256-level Gaussian Lloyd–Max quantizer; one byte per component.
— Int4 / Int2 / binary codes fall out by bit-truncation — trade size for recall.
— Shrinks vector indexes 4x (or more) for cheaper similarity search.

Developer: Alexey Milovidov.

Randomized Hadamard rotations

To improve the quality of vector search, it makes sense to apply random rotation of N-dimensional space before quantization of vectors.

Rotation is a multiplication to an orthogonal matrix. There are many orthogonal matrices, and matrix multiplication is computationally expensive.

Thankfully, there is a family of orthogonal matrices, that are faster to multiply with - Hadamard matrices. And it is broad enough to represent pseudorandom rotations.

Now ClickHouse has a function for pseudorandom rotations:

SELECT randomHadamardTransform(vec);

Developer: Alexey Milovidov.

Randomized Hadamard rotations

A building block for vector quantization pipelines:

SELECT randomHadamardTransform([1., 2., 3., 4.]::Array(Float32)); -- [0, 2, 1, -5] — an orthogonal, norm-preserving rotation SELECT randomHadamardTransform(v, seed); -- different rotation SELECT randomHadamardTransform(v, seed, 256); -- truncate to 256 dims

— Spreads the information evenly across dimensions,
  so scalar quantization loses less — rotate, then quantize.
— Truncated form = Johnson–Lindenstrauss random projection.
— Deterministic: the same seed gives the same rotation.

Developer: Alexey Milovidov.

Quantization codecs 🧪

Store a compact quantized companion next to full-precision vectors:

CREATE TABLE vecs (id UInt32, v Array(BFloat16) CODEC(Quantized('rabitq', 384))) …; -- methods: int8, rabitq (1-bit), turboquant (2-bit), mrl, pq SET vector_search_use_quantized_codes = 1; SELECT id FROM vecs ORDER BY L2Distance(v, […]) LIMIT 10;

— Two-stage brute-force search without an index: scan the small
  quantized stream, then rescore the candidates at full precision.
— Up to 8x less I/O in the first stage (1-bit codes: 16x smaller).

Developer: Shankar Iyer.

Embeddings.info

Contains 100 million photos, 50 million comments, and the Internet
— to explore as a vector embeddings in ClickHouse!

Demo.

Developer: Alexey Milovidov.

chDB

chDB in WASM

The clickhouse binary now builds for WebAssembly
(wasm64, via Emscripten) — and chDB brings it to your browser:

https://wasm.chdb.io -- a full SQL shell, no server, no cloud chdb :) CREATE TABLE t (x UInt64) ENGINE = MergeTree ORDER BY x; chdb :) INSERT INTO t SELECT number FROM numbers(1000000); chdb :) SELECT sum(x) FROM t; -- everything runs in the browser tab

clickhouse local runs under Node.js ≥ 24 and in the browser,
  including real MergeTree tables; a CI job pins the build.
— The SQL parser also builds standalone (utils/wasm-parser) —
  it powers the syntax highlighting of the Web UI and the new Fiddle.

Developers: Auxten Wang (chDB), Alexey Milovidov (parser).

Pandas API in chDB

In addition to SQL, chDB now implements Pandas API.

It builds a lazy pipeline from Pandas transformations,
and runs it with the ClickHouse speed!

import datastore as pd # That's it! Use pandas API as usual

Developer: Auxten.

User Defined Functions

WebAssembly UDFs 🧪

Create user-defined functions in WebAssembly:

INSERT INTO system.webassembly_modules (name, code) SELECT 'collatz', '...'; CREATE FUNCTION collatz_steps LANGUAGE WASM ARGUMENTS (n UInt32) RETURNS UInt32 FROM 'collatz' :: 'steps'; SELECT groupArray(collatz_steps(number::UInt32)) FROM numbers(1, 100);

— Write UDFs in any language that compiles to WASM:
  Rust, C, C++, Go, Zig, ...
— Sandboxed execution with Wasmtime.
Experimental.

Developers: Vladimir Cherkasov, Alexey Smirnov, Vasily Chekalkin.

WebAssembly UDFs 🧪

Rust crate: clickhouse-wasm-udf:

use clickhouse_wasm_udf_bindgen::clickhouse_udf; #[clickhouse_udf] pub fn some_udf(data: String) -> HashMap<String, String> { // Your implementation here }

Control over runtime complexity:
— webassembly_udf_max_fuel
— webassembly_udf_max_memory
— webassembly_udf_max_input_block_size
— webassembly_udf_max_instances

Developers: Vladimir Cherkasov, Alexey Smirnov, Vasily Chekalkin.

WebAssembly UDFs 🧪

A showcase:

chgeos: PostGIS-compatible spatial functions for ClickHouse,
delivered as a WebAssembly UDF module powered by GEOS 3.12+.

https://github.com/bacek/chgeos

Developer: Vasily Chekalkin.

Drivers for UDFs 🧪

Write a UDF in any programming language — right in SQL.
A "driver" compiles it at CREATE time into an executable UDF:

CREATE FUNCTION collatz_steps ARGUMENTS (n UInt64) RETURNS UInt64 ENGINE = GVisorC AS $$ uint64_t steps = 0; while (n > 1) { n = (n % 2 == 0) ? n / 2 : 3 * n + 1; ++steps; } return steps; $$; SELECT number, collatz_steps(number) FROM numbers(1, 10);

— The C body is compiled and runs in sandboxed Docker containers
  the GVisorC, DockerC, and UnsafeC drivers are provided as examples.
— Drivers are declared by the operator; functions survive restarts.

Developers: Daniil Timižev, Alexey Milovidov.

Bonus

Open-source Kubernetes operator

The official ClickHouse Kubernetes operator:

https://github.com/ClickHouse/clickhouse-operator

Automated Cluster Provisioning: multi-node with sharding and replication.

ClickHouse Keeper Support: deploy and manage ClickHouse Keeper.

Vertical & Horizontal Scaling: adjust CPU / Memory resources or shards.

Configuration Management: in a single manifest change.

Seamless Upgrades: rolling updates without dropping queries.

Developer: Grigory Pervakov.

Q&A

Photo by Kell Kell, 2013. CC-BY-SA-3.0.