9 月になり、ClickHouse 26.9 が数多くの新機能とともにリリースされました。
ClickHouse 26.9 リリースには、56 件の新機能 🍁、135 件のパフォーマンス最適化 🍎、464 件のバグ修正 🐿️ が含まれています。
本リリースでは、LIMIT の境界条件指定、追記専用マテリアライズドビューのインクリメンタルリフレッシュ、高カーディナリティの DISTINCT クエリにおけるディスクスピルが導入されました。
また、有効期限付きアクセストークン、min・max・count クエリの高速化、PromQL サポートの拡張、利便性を高めるさまざまな改善についても見ていきます。
新しいコントリビューター
26.9 のすべての新しいコントリビューターの皆さまを心より歓迎します。ClickHouse コミュニティの成長には目を見張るものがあり、ClickHouse をこれほど支持される存在にしてくださった皆さまの貢献にいつも感謝しています。
新しいコントリビューターのお名前は以下のとおりです。
Aaron Harlap, Actuele AI, Alex Francoeur, Alex Prabhat Bara, AlexF, Anand Kumar Shaw, Anton Kovalenko, Aparajita Pandey, Brandon Pereira, Claude, Denys Stetsenko, Dmitrii Bezrukov, Evandro Leopoldino Gonçalves, Friedrich ten Hagen, George Viamontes, Gülçin Yıldırım Jelinek, Hamza Wasim, Hank Hoffmeier, Héctor Pablos, Itamar Tempelhof, Ivan N. Taranov, Ivan Tkachev, Ivan Tkatchev, Jithin Zachariah, Jordan Bertasso, Jords, Joshua, Juanjo, Kelly Toole, Lucas, Luis Neves, Luís Lizardo, Marat Dulin, Mike Shi, Navneet Kumar, Pablo Francisco Pérez Hidalgo, Paul Annesley, Philip Li, Pratham Nayak, Pratheesh, SamWolfberg, Sankalp Thakur, Sebastian Vercruyssse, Serhiy Bzhezytskyy, Takayuki Enomoto, Thien Phan, Vadim Ilves, XanderYoon, Yongqiang Tian, alexprabhat99, anand-tradesea, aparajita, bakhtiiartashbolotov, cuishuang, jithinzac, kasimtj, key-arg, kyungryun, linsen, maederm, mariahlynnenagy, miao tang, mosya415, ngagejason, sakshichitnis27, sleepingeight, statxc, t, tars, zhanglangning
ヒント: このリストをどのように生成しているか興味がある方は、こちらをご覧ください。
また、プレゼンテーションのスライドも確認できます。
DateTime と Time の算術演算
コントリビューター: Yarik Briukhovetskyi
ClickHouse 26.9 以降、DateTime 値の加算・減算において、Time 値をオフセットとして使用できるようになりました。計算結果は元の DateTime 値のタイムゾーンを保持します。
簡単な例を見てみましょう。
SELECT
now() AS now,
now + toTime('02:00:00'),
toTime('04:00:00') + now,
now64() AS now64,
now64 - toTime('02:00:00'),
now64 - toTime64('02:00:00.417', 3)
FORMAT Vertical;Row 1:
──────
now: 2026-09-21 13:49:06
plus(now, to⋯02:00:00')): 2026-09-21 15:49:06
plus(toTime(⋯:00'), now): 2026-09-21 17:49:06
now64: 2026-09-21 13:49:06.440
minus(now64,⋯02:00:00')): 2026-09-21 11:49:06.440
minus(now64,⋯0.417', 3)): 2026-09-21 11:49:06.023計算結果が戻り値の型でサポートされる範囲外になった場合、date_time_overflow_behavior の設定によって、ClickHouse が例外をスローするか、最も近い境界値にクランプするか、オーバーフローをチェックしないかが決まります。
DateTime に格納できる最大値は 2106-02-07 06:28:15 です。この時刻に 1 秒を加算するとどうなるか確認してみましょう。
SELECT
toDateTime('2106-02-07 06:28:15', 'UTC')
+ toTime('00:00:01');┌─plus(toDateT⋯00:00:01'))─┐
│ 1970-01-01 00:00:00 │
└──────────────────────────┘date_time_overflow_behavior のデフォルト値は ignore であるため、予想どおり DateTime の最小値へとオーバーフローしました。しかし、値がオーバーフローした場合には例外を発生させたいこともあります。
SELECT toDateTime('2106-02-07 06:28:15', 'UTC') + toTime('00:00:01')
SETTINGS date_time_overflow_behavior = 'throw';Received exception:
Code: 321. DB::Exception: Value 4294967296 is out of bounds of type DateTime: In scope SELECT toDateTime('2106-02-07 06:28:15', 'UTC') + toTime('00:00:01') SETTINGS date_time_overflow_behavior = 'throw'. (VALUE_IS_OUT_OF_RANGE_OF_DATA_TYPE)あるいは、saturate を使用することもできます。その場合、その型の最大値が返されます。
SELECT toDateTime('2106-02-07 06:28:10', 'UTC') + toTime('00:00:10')
SETTINGS date_time_overflow_behavior = 'saturate';┌─plus(toDateT⋯00:00:10'))─┐
│ 2106-02-07 06:28:15 │
└──────────────────────────┘最後に、初期値と計算後の値のタイムゾーンを確認してみましょう。
SELECT
now() AS now,
timezoneOf(now),
now + toTime('02:00:00') AS future,
timezoneOf(future)
FORMAT Vertical;Row 1:
──────
now: 2026-09-21 14:37:21
timezoneOf(now): Europe/London
future: 2026-09-21 16:37:21
timezoneOf(future): Europe/LondonPromQL のプライベートプレビュー
コントリビューター: Vitaly Baranov、Nikita Mikhaylov、Valery Petrov、Minh Vu
ClickHouse 26.9 では、対応関数の拡充、Prometheus HTTP API エンドポイントの追加、TimeSeries テーブルに対する直接の SELECT クエリのサポートにより、PromQL サポートが拡張されました。
PromQL と TimeSeries テーブルエンジンは ClickHouse Cloud でプライベートプレビューとして利用可能になり、メトリクスを ClickHouse に保存して ClickStack、Grafana、clickhouse-client、SQL から照会できるようになりました。
詳細については、ブログ記事「ClickHouseの新しいTimeSeriesエンジンのご紹介」をご覧ください。
CREATE TOKEN
コントリビューター: Alexey Milovidov
ClickHouse 26.9 では CREATE TOKEN が導入されました。これにより、メインのパスワードを公開したり変更したりすることなく、アプリケーション、スクリプト、CI ジョブ、エージェント向けに有効期限付きの認証情報を作成できます。
トークンはユーザーの既存の権限のサブセットに制限できるため、認証情報が漏洩した際の影響を軽減できます。
動作を見てみましょう。まず、小さなテーブルを作成します。
CREATE TABLE ourTable
(
id UInt8
);
INSERT INTO ourTable VALUES (1);次に、alexey というユーザーを作成します。
CREATE USER alexey IDENTIFIED WITH sha256_password BY 'main-password';
GRANT SELECT, INSERT ON ourTable TO alexey;
GRANT CREATE TOKEN ON *.* TO alexey;alexey はこのテーブルに対する SELECT および INSERT 権限を持っており、自身のトークンを作成することもできます。
続いて、alexey として接続し、有効期間が 30 日間で ourTable に対する SELECT クエリのみを実行できるトークンを作成します。
CREATE TOKEN
VALID FOR INTERVAL 30 DAY
GRANTS (SELECT ON ourTable);この文を実行すると、生成されたトークンとその有効期限が返されます。
┌─token────────────────────────────┬─────────valid_until─┐
│ NXuRnBywn4HcHCIyBWC2WHT2Cfn4xQLb │ 2026-10-21 15:07:57 │
└──────────────────────────────────┴─────────────────────┘トークンは一度しか表示されないため、必ず控えておいてください。
このトークンを使って接続し、SELECT クエリを実行できます。
./clickhouse client \
--user alexey \
--password 'NXuRnBywn4HcHCIyBWC2WHT2Cfn4xQLb' \
--query "SELECT * FROM ourTable"1予想どおり正常に動作しました。では、このトークンを使ってテーブルに挿入しようとするとどうなるでしょうか。
./clickhouse client \
--user alexey \
--password 'NXuRnBywn4HcHCIyBWC2WHT2Cfn4xQLb' \
--query "INSERT INTO ourTable VALUES (2)"Code: 497. DB::Exception: alexey: Not enough privileges. To execute this query, it's necessary to have the grant INSERT(id) ON db.`table`. (ACCESS_DENIED)トークンを使用して認証された場合、alexey は SELECT アクセス権しか持たないため、エラーになります。
トークンによってユーザーがすでに持っている以上の権限が付与されることはなく、有効期限が切れた場合やユーザーが削除された場合には機能しなくなります。VALID UNTIL または VALID FOR を指定しない場合、デフォルトの有効期間は 30 分です。
境界条件付き LIMIT
コントリビューター: Zakhar Kravchuk、Nihal Miaji
ClickHouse 26.9 では、ソートされた結果ストリーム内の値に基づいて出力を開始・終了する境界条件により、LIMIT が拡張されました。私たちの知る限り、この機能は現在他のどのデータベースにも存在しません。
AFTER: 条件に一致する行を含めます。UNTIL: 条件に一致する行の手前で停止します。ALL: 条件が一致するたびに境界を適用します。
以下のさまざまな組み合わせを試して、各クエリがどの行を返すか確認してみてください。
この機能はログデータの分析に特に役立ちます。ブログ記事「列指向ストレージによる Nginx ログの 170 倍圧縮」で使用された Nginx データセットを使って見てみましょう。
まず、テーブルを作成します。
CREATE TABLE nginx_logs
(
timestamp DateTime,
ip String,
method LowCardinality(String),
path String,
status UInt16,
response_bytes UInt64,
referer String,
user_agent String
)
ENGINE = MergeTree
ORDER BY (timestamp, ip, path);次に、データを取り込みます。
INSERT INTO nginx_logs
WITH extractGroups(
line,
'^(\\S+) - \\S+ \\[([^\\]]+)\\] "(\\S+) (.*) [^ ]+" (\\d+) (\\d+) "([^"]*)" "(.*)"$'
) AS fields
SELECT
assumeNotNull(parseDateTimeBestEffortOrNull(fields[2])) AS timestamp,
fields[1] AS ip,
fields[3] AS method,
fields[4] AS path,
toUInt16(fields[5]) AS status,
toUInt64(fields[6]) AS response_bytes,
fields[7] AS referer,
fields[8] AS user_agent
FROM s3(
'https://datasets-documentation.s3.eu-west-3.amazonaws.com/http_logs/nginx-66.log.gz',
LineAsString
)
WHERE length(fields) = 8
AND parseDateTimeBestEffortOrNull(fields[2]) IS NOT NULL;では、データの概要を手短に確認します。
SELECT
count() AS rows,
min(timestamp) AS first_timestamp,
max(timestamp) AS last_timestamp,
countIf(status >= 500) AS server_errors
FROM nginx_logs;Row 1:
──────
rows: 66514081 -- 66.51 million
first_timestamp: 2019-01-24 00:00:00
last_timestamp: 2019-02-24 00:00:00
server_errors: 69857次のクエリは、最初の 5xx レスポンスから開始して 5 つのリクエストを返します。AFTER は該当行を含むため、条件を満たすリクエストが含まれます。
SELECT timestamp, path, status
FROM nginx_logs
WHERE ip = '91.243.160.31'
AND timestamp >= '2019-01-24 00:00:00'
AND timestamp < '2019-02-03 00:00:00'
ORDER BY timestamp, ip, path, method, status, response_bytes
LIMIT 5 AFTER status >= 500;┌───────────timestamp─┬─path─────────────────────────────────────────────────────────┬─status─┐
│ 2019-01-24 06:54:01 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-24 06:54:02 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-24 06:54:06 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-24 06:54:15 │ /product/552/1168/مایکروفر-رومیزی-ال-جی-مدل-MS93SCR │ 200 │
│ 2019-01-24 06:54:16 │ /image/552/product/50x50 │ 200 │
└─────────────────────┴──────────────────────────────────────────────────────────────┴────────┘UNTIL は該当行を含みません。このクエリは、最初のサーバーエラーから開始し、ステータスが 500 未満の最初のレスポンスの手前で停止します。
SELECT timestamp, path, status
FROM nginx_logs
WHERE ip = '91.243.160.31'
AND timestamp >= '2019-01-24 00:00:00'
AND timestamp < '2019-02-03 00:00:00'
ORDER BY timestamp, ip, path, method, status, response_bytes
LIMIT 100
AFTER status >= 500
UNTIL status < 500;┌───────────timestamp─┬─path─────────────────────────────────────────────────────────┬─status─┐
│ 2019-01-24 06:54:01 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-24 06:54:02 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-24 06:54:06 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
└─────────────────────┴──────────────────────────────────────────────────────────────┴────────┘AFTER 境界は 1 回だけ適用されますが、ALL を使用すると、別の行が一致するたびに境界が再適用されます。このクエリは、すべてのサーバーエラーと、その後に続く 2 つのリクエストを返します。
SELECT timestamp, path, status
FROM nginx_logs
WHERE ip = '91.243.160.31'
AND timestamp >= '2019-01-24 00:00:00'
AND timestamp < '2019-02-03 00:00:00'
ORDER BY timestamp, ip, path, method, status, response_bytes
LIMIT 3 AFTER status >= 500 ALL;┌───────────timestamp─┬─path─────────────────────────────────────────────────────────┬─status─┐
│ 2019-01-24 06:54:01 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-24 06:54:02 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-24 06:54:06 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-24 06:54:15 │ /product/552/1168/مایکروفر-رومیزی-ال-جی-مدل-MS93SCR │ 200 │
│ 2019-01-24 06:54:16 │ /image/552/product/50x50 │ 200 │
│ 2019-01-28 23:27:00 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-28 23:27:01 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-28 23:27:05 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-28 23:27:14 │ /product/552/1168/مایکروفر-رومیزی-ال-جی-مدل-MS93SCR │ 200 │
│ 2019-01-28 23:27:15 │ /image/552/product/50x50 │ 200 │
│ 2019-02-02 13:43:29 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-02-02 13:43:30 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-02-02 13:43:34 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-02-02 13:43:43 │ /product/552/1168/مایکروفر-رومیزی-ال-جی-مدل-MS93SCR │ 200 │
│ 2019-02-02 13:43:44 │ /image/552/product/50x50 │ 200 │
└─────────────────────┴──────────────────────────────────────────────────────────────┴────────┘この期間中、行 2、7、12 で 3 つのインシデントが個別に発生しています。各バースト内では、500 レスポンスが発生するたびに 3 行の境界が再適用されます。
そのため、連続した障害によってアクティブなウィンドウが延長され、最後の障害の後に 5xx 以外のリクエストが 2 回連続して発生するまで続きます。この例では、どちらも成功を表す 200 レスポンスです。
また、ALL で開始境界を再適用しつつ、UNTIL で各範囲を終了させることもできます。
SELECT timestamp, path, status
FROM nginx_logs
WHERE ip = '91.243.160.31'
AND timestamp >= '2019-01-24 00:00:00'
AND timestamp < '2019-02-03 00:00:00'
ORDER BY timestamp, ip, path, method, status, response_bytes
LIMIT 100
AFTER status >= 500 ALL
UNTIL status < 500;┌───────────timestamp─┬─path─────────────────────────────────────────────────────────┬─status─┐
│ 2019-01-24 06:54:01 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-24 06:54:02 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-24 06:54:06 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-28 23:27:00 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-28 23:27:01 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-01-28 23:27:05 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-02-02 13:43:29 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-02-02 13:43:30 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
│ 2019-02-02 13:43:34 │ /product/28579/57435/اجاق-گاز-صفحه-ای-داتیس-مدل-DG-503-Ultra │ 500 │
└─────────────────────┴──────────────────────────────────────────────────────────────┴────────┘先ほどのクエリとは異なり、この出力には各障害バーストの後の成功リクエストは含まれません。UNTIL は 5xx 以外の最初のレスポンスで範囲を閉じ、ALL は次のバーストのスキャンを継続します。
インクリメンタル・リフレッシャブルマテリアライズドビュー
コントリビューター: Smita Kulkarni
ClickHouse 26.9 では、リフレッシャブルマテリアライズドビューに APPEND INCREMENTAL が追加されました。リフレッシュのたびにソーステーブル全体をスキャンする代わりに、ClickHouse は前回の更新以降にコミットされた行のみを処理します。
これを使用して、追記専用データを別の ClickHouse テーブルへ増分コピーしたり、MergeTree テーブルから Iceberg データレイクへイベントストリームをレプリケーションしたりできます。
Iceberg でこの機能を使用する方法を見てみましょう。
ClickHouse で注文イベントの追記専用ストリームを作成し、それを定期的に Iceberg テーブルにコピーすることで、更新ごとに新しくコミットされた行のみが処理される様子を示します。
まず、ClickHouse テーブルを作成します。
CREATE TABLE order_events
(
event_id UInt64,
event_time DateTime,
order_id UInt64,
event_type LowCardinality(String),
country LowCardinality(String),
amount Decimal(10, 2)
)
ORDER BY (event_time, order_id, event_id)
SETTINGS
enable_block_number_column = 1,
enable_block_offset_column = 1,
add_minmax_index_for_block_number_column = 1,
add_minmax_index_for_block_offset_column = 1,
part_minmax_index_columns = 'with_block_number_offset';ブロック番号とブロックオフセットのカラムは、前回の更新以降にコミットされた行を識別するために ClickHouse が使用するカーソルを提供します。
次に、Iceberg への挿入を有効にし、ローカルファイルシステム上に Iceberg テーブルを作成します。
SET allow_insert_into_iceberg = 1;
CREATE TABLE lake_order_events
(
event_id UInt64,
event_time DateTime,
order_id UInt64,
event_type String,
country String,
amount Decimal(10, 2)
)
ENGINE = IcebergLocal('lake_order_events', 'Parquet');lake_order_events ディレクトリには、Iceberg のメタデータ、マニフェスト、および Parquet データファイルが含まれます。
最後に、新しくコミットされたイベントを 1 時間ごとに Iceberg にコピーするマテリアライズドビューを作成します。
CREATE MATERIALIZED VIEW order_events_to_iceberg
REFRESH EVERY 1 HOUR APPEND INCREMENTAL
TO lake_order_events
AS
SELECT event_id, event_time, order_id, event_type, country, amount
FROM order_events;データを取り込みます。
INSERT INTO order_events VALUES
(1, '2026-09-22 09:05:00', 1001, 'placed', 'UK', 0),
(2, '2026-09-22 09:17:00', 1002, 'placed', 'Germany', 0),
(3, '2026-09-22 09:20:00', 1001, 'paid', 'UK', 89.99),
(4, '2026-09-22 09:31:00', 1002, 'paid', 'Germany', 129.00);これらのイベントは、マテリアライズドビューが 1 時間ごとにトリガーされた際に Iceberg テーブルにコピーされますが、確認を早めるために手動で更新をトリガーします。
SYSTEM REFRESH VIEW order_events_to_iceberg;
SYSTEM WAIT VIEW order_events_to_iceberg;そして、Iceberg テーブルを照会してみます。
SELECT * FROM lake_order_events;┌─event_id─┬─────────────────event_time─┬─order_id─┬─event_type─┬─country─┬─amount─┐
│ 1 │ 2026-09-22 09:05:00.000000 │ 1001 │ placed │ UK │ 0 │
│ 2 │ 2026-09-22 09:17:00.000000 │ 1002 │ placed │ Germany │ 0 │
│ 3 │ 2026-09-22 09:20:00.000000 │ 1001 │ paid │ UK │ 89.99 │
│ 4 │ 2026-09-22 09:31:00.000000 │ 1002 │ paid │ Germany │ 129 │
└──────────┴────────────────────────────┴──────────┴────────────┴─────────┴────────┘すべてのレコードがコピーされました。次に、注文の変更をシミュレートするために行をいくつか追加してみましょう。
INSERT INTO order_events VALUES
(5, '2026-09-22 10:03:00', 1001, 'shipped', 'UK', 0),
(6, '2026-09-22 10:11:00', 1002, 'refunded', 'Germany', -129.00),
(7, '2026-09-22 10:28:00', 1003, 'placed', 'France', 0);マテリアライズドビューを手動で再度更新し、Iceberg テーブルをもう一度照会します。
┌─event_id─┬─────────────────event_time─┬─order_id─┬─event_type─┬─country─┬─amount─┐
│ 1 │ 2026-09-22 09:05:00.000000 │ 1001 │ placed │ UK │ 0 │
│ 2 │ 2026-09-22 09:17:00.000000 │ 1002 │ placed │ Germany │ 0 │
│ 3 │ 2026-09-22 09:20:00.000000 │ 1001 │ paid │ UK │ 89.99 │
│ 4 │ 2026-09-22 09:31:00.000000 │ 1002 │ paid │ Germany │ 129 │
│ 5 │ 2026-09-22 10:03:00.000000 │ 1001 │ shipped │ UK │ 0 │
│ 6 │ 2026-09-22 10:11:00.000000 │ 1002 │ refunded │ Germany │ -129 │
│ 7 │ 2026-09-22 10:28:00.000000 │ 1003 │ placed │ France │ 0 │
└──────────┴────────────────────────────┴──────────┴────────────┴─────────┴────────┘3 つの新しいイベントが追加されていることが確認できます。
Iceberg ターゲットの場合、ClickHouse はスナップショットのサマリー内にインクリメンタルカーソルを保存します。したがって、カーソルと新しく追記されたデータは、同じ Iceberg スナップショットの一部としてコミットされます。これは system.iceberg_history を照会することで確認できます。
SELECT
made_current_at, operation,
summary['added-records'] AS added_records,
summary['total-records'] AS total_records,
summary['total-data-files'] AS total_data_files,
summary['clickhouse.refresh-cursor'] != '' AS has_refresh_cursor
FROM system.iceberg_history
WHERE table = 'lake_order_events'
ORDER BY made_current_at;┌─────────made_current_at─┬─operation─┬─added_records─┬─total_records─┬─total_data_files─┬─has_refresh_cursor─┐
│ 2026-09-22 11:03:47.549 │ APPEND │ 4 │ 4 │ 1 │ 1 │
│ 2026-09-22 11:04:36.247 │ APPEND │ 3 │ 7 │ 2 │ 1 │
└─────────────────────────┴───────────┴───────────────┴───────────────┴──────────────────┴────────────────────┘1 回目の更新で 4 件のレコードが追加され、2 回目では 3 件の新しいイベントのみが追加されて、合計が 4 件から 7 件になりました。また、両方のスナップショットに更新カーソルが含まれていることもわかります。
ClickHouse は、新しく追記されたデータとともに、このカーソルを Iceberg スナップショット内に保存します。ClickHouse が再起動した場合でも、次の更新は同じイベントを再処理するのではなく、その位置から再開します。カーソルは追記が成功したときにのみ進むため、常にデータと同期した状態が保たれます。
このアプローチは、イベント、ログ、監査レコード、その他の不変のファクトデータなど、追記専用のデータに最適です。
カラム統計からの min、max、count
コントリビューター: Alexey Milovidov
ClickHouse は、各データパート内に数値系カラムの最小値と最大値を保持しています。26.9 からは、基盤となるカラムデータを読み取ることなく、これらの統計情報を使用して min、max、count クエリに応答できるようになりました。
先ほど使用した Nginx ログテーブルで試してみましょう。timestamp ではなく response_bytes を使用します。timestamp はテーブルのソートキーの最初のカラムであるため、ClickHouse はすでに既存のメタデータからそのクエリに応答できます。
クエリプランを確認できるように、クエリの先頭に EXPLAIN を付けます。
EXPLAIN
SELECT
min(response_bytes),
max(response_bytes),
count()
FROM nginx_logs;┌─explain──────────────────────────────────────────────────────────┐
│ Output: min(response_bytes), max(response_bytes), count() │
│ │
│ Aggregating │
│ │ Keys: │
│ │ Aggregates: min(response_bytes), max(response_bytes), count() │
│ │ Skip merging: 0 │
│ └──ReadFromPreparedSource (_statistics_min_max_projection) │
└──────────────────────────────────────────────────────────────────┘最後の行を見ると、ClickHouse が response_bytes カラムを読み取る代わりに、カラム統計から結果を準備していることがわかります。
この最適化は use_statistics_for_min_max_aggregation 設定で無効化できます。無効にすると、次のクエリに示すように、ClickHouse は結果を計算するために response_bytes カラムをスキャンする必要があります。
EXPLAIN
SELECT
min(response_bytes),
max(response_bytes),
count()
FROM nginx_logs
SETTINGS use_statistics_for_min_max_aggregation = 0;┌─explain──────────────────────────────────────────────────────────┐
│ Output: min(response_bytes), max(response_bytes), count() │
│ │
│ Aggregating │
│ │ Keys: │
│ │ Aggregates: min(response_bytes), max(response_bytes), count() │
│ │ Skip merging: 0 │
│ └──ReadFromMergeTree (default.nginx_logs) │
│ Read type: Default │
│ Parts: 5 | Granules: 8122 │
│ Output: response_bytes │
└──────────────────────────────────────────────────────────────────┘system.session_query_ids
コントリビューター: Vladimir Cherkasov
ClickHouse 26.9 では新しいシステムテーブル system.session_query_ids が導入されました。このテーブルは、現在のセッション内のすべてのクエリ ID を実行順に追跡します。
このテーブルは次のように照会できます。
SELECT * FROM system.session_query_ids;┌─sequence_number─┬─query_id─────────────────────────────┐
│ 1 │ 8ebdec91-0636-44e7-9d1a-66974c2d0fe3 │
│ 2 │ 471e8bef-2042-4394-9a08-3538f4ebcf85 │
│ 3 │ f9757bd2-8fe3-4b14-b681-8e938a396880 │
│ 4 │ 36903bb2-9f85-420b-b37b-66f0d23ce41e │
│ 5 │ 4127be18-3e58-45f6-8153-5ff9a0d04675 │
│ 6 │ a81309ab-3498-4ba9-9217-8145406a168d │
└─────────────────┴──────────────────────────────────────┘system.query_log テーブルには各エントリに query_id が含まれているため、そのテーブルを照会する際にクエリ ID を手動で指定することなく、直前に実行したクエリを確認できるようになりました。
SELECT query, query_duration_ms FROM system.query_log
WHERE query_id IN (SELECT query_id FROM system.session_query_ids)
AND type = 'QueryFinish'
ORDER BY event_time_microseconds;┌─query─────────────────────────────────────────────────────────────┬─query_duration_ms─┐
│ SELECT * FROM lake_order_events; │ 10 │
│ system flush logs; │ 0 │
│ SELECT query, query_duration_ms FROM system.query_log ↴│ 3 │
│↳WHERE query_id IN (SELECT query_id FROM system.session_query_ids)↴│ │
│↳ AND type = 'QueryFinish' ORDER BY event_time_microseconds; │ │
│ SELECT * FROM system.session_query_ids; │ 0 │
│ set output_format_pretty_row_numbers=0; │ 0 │
│ SELECT * FROM system.session_query_ids; │ 0 │
└───────────────────────────────────────────────────────────────────┴───────────────────┘テーブルサイズとテーブル数の制限
コントリビューター: Alexey Milovidov
ClickHouse 26.9 では、個々のテーブルが肥大化できる上限サイズや、データベース内に作成できるテーブルの数に制限を設定できるようになりました。
これは、単一のワークロードがすべてのリソースを消費するのを防ぎたいマルチテナント環境、一時的な環境、デモ環境などで役立ちます。
まず、最大 3 行まで格納できるテーブルを作成してみましょう。
CREATE TABLE limited_events
(
id UInt64,
message String
)
ENGINE = MergeTree
ORDER BY id
SETTINGS max_table_size_rows = 3;3 行を挿入します。
INSERT INTO limited_events VALUES
(1, 'started'),
(2, 'processing'),
(3, 'finished');もう 1 行挿入しても、まだ成功します。
INSERT INTO limited_events VALUES
(4, 'one past limit');
SELECT count()
FROM limited_events;┌─count()─┐
│ 4 │
└─────────┘制限のチェックは、INSERT の開始時点におけるテーブルの現在のサイズに対して行われます。つまり、テーブルを 3 行から 4 行に増やす INSERT は完了できます。次の INSERT ではテーブルがすでに上限を超えていることが検出され、拒否されます。
INSERT INTO limited_events VALUES
(5, 'rejected');Code: 1016. DB::Exception: Table size limit exceeded: the total number of rows in active data parts of table default.limited_events is 4, which exceeds the 'max_table_size_rows' setting value (3). (TABLE_SIZE_LIMIT_EXCEEDED)また、max_table_size_bytes_compressed および max_table_size_bytes_uncompressed を使用して、圧縮後または非圧縮時のサイズによってテーブルを制限することもできます。
データベース内のテーブル数にも制限を設定できます。2 つのテーブルを保持できるデータベースを作成してみましょう。
CREATE DATABASE tenant
ENGINE = Atomic
SETTINGS max_tables = 2;最初の 2 つのテーブルは通常どおり作成できます。
CREATE TABLE tenant.events (id UInt64)
ENGINE = MergeTree
ORDER BY id;
CREATE TABLE tenant.users (id UInt64)
ENGINE = MergeTree
ORDER BY id;しかし、3 つ目のテーブルを作成しようとすると、ClickHouse はそれを拒否します。
CREATE TABLE tenant.audit_log (id UInt64)
ENGINE = MergeTree
ORDER BY id;Code: 724. DB::Exception: Too many tables in database `tenant`. The limit (database setting `max_tables`) is set to 2, the current number is 2. (TOO_MANY_TABLES)外部メモリでの DISTINCT
コントリビューター: Nihal Z. Miaji
概念上、DISTINCT はすでに出現した値を追跡し続ける必要があります。ClickHouse にはこの処理を軽減する複数の最適化が備わっていますが、カーディナリティが高いクエリでは、依然としてメモリ上に大規模なセットを保持しなければならない場合があります。
ClickHouse 26.9 では、クエリがメモリ不足になるまでハッシュセットを拡大させ続ける代わりに、GROUP BY や ORDER BY と同様にこのセットをディスクにスピルできるようになりました。
1 億個の数値のシーケンスからすべての重複しない値を返しつつ、max_memory_usage を使用してクエリのメモリを 150 MB に制限した場合の挙動を見てみましょう。
SELECT DISTINCT number
FROM numbers_mt(100_000_000)
FORMAT `NULL`
SETTINGS max_memory_usage = 150_000_000Received exception:
Code: 241. DB::Exception: Query memory limit exceeded: would use 244.33 MiB (attempt to allocate chunk of 127.00 MiB), maximum: 143.05 MiB: While executing ExternalDistinctTransform. (MEMORY_LIMIT_EXCEEDED)メモリが不足しているため、クエリを処理できません。max_bytes_before_external_distinct を設定することで、DISTINCT の中間状態をディスクにスピルさせることができます。
SELECT DISTINCT number
FROM numbers_mt(100_000_000)
FORMAT `NULL`
SETTINGS
max_memory_usage = 150_000_000,
max_bytes_before_external_distinct = 25_000_000;0 rows in set. Elapsed: 1.676 sec. Processed 100.00 million rows, 800.00 MB (59.65 million rows/s., 477.21 MB/s.)
Peak memory usage: 105.63 MiB.今回は、ClickHouse は約 25 MB に達した時点で DISTINCT データのテンポラリファイルへの書き出しを開始します。クエリ全体では最大 150 MB を使用できるため、最終的にそれらのファイルを読み取ってマージするのに十分なメモリが残されています。
この機能を使用する際には、もう 1 点留意すべき重要な点があります。ディスクへのスピルによってメモリ使用量は削減されますが、メモリがまったく不要になるわけではありません。バッファ、クエリパイプライン、ディスクにスピルされたテンポラリファイルのマージ処理には、引き続きメモリが必要です。
ディスクへのスピル閾値を 25 MB に維持したまま、クエリ全体の制限を 100 MB に引き下げるとどうなるか見てみましょう。
SELECT DISTINCT number
FROM numbers_mt(100_000_000)
FORMAT `NULL`
SETTINGS
max_memory_usage = 100_000_000,
max_bytes_before_external_distinct = 25_000_000;Received exception:
Code: 241. DB::Exception: Query memory limit exceeded: would use 98.94 MiB (attempt to allocate chunk of 4.13 MiB), maximum: 95.37 MiB: While executing BufferingFromFileSource. (MEMORY_LIMIT_EXCEEDED)データは正常にスピルされましたが、テンポラリファイルを読み戻す際に ClickHouse がメモリ不足に陥りました。BufferingFromFileSource から、エラーが発生したのは元のインメモリセットの構築中ではなく、スピルされたファイルの処理中であることがわかります。
これを解決するには、max_memory_usage を 150 MB に戻す必要があります。
max_bytes_ratio_before_external_distinctが0.5に設定されている場合、ClickHouse は外部DISTINCTを自動的に有効化します。これは、利用可能なメモリの半分に達した時点でDISTINCTがスピルを開始することを意味します。
JSON サブカラムのブラケット構文
コントリビューター: Pavel Kruglov
ClickHouse 26.9 では、JSON 値内のパスにアクセスするためのブラケット(大括弧)構文が追加されました。これにより、ネストされたパスの記述が容易になり、ドットやスペースなどの文字を含むキーを扱いやすくなります。
インメモリの例を使って、どのように機能するか見てみましょう。
WITH '{
"user": {"name": "Alex"},
"release.version": "26.9",
"first name": "Alexey",
"tags": ["database", "analytics"]
}'::JSON AS json
SELECT
json.user.name AS dot_notation,
json['user']['name'] AS bracket_notation,
json['release.version'] AS release_version,
json['first name'] AS first_name;┌─dot_notation─┬─bracket_notation─┬─release_version─┬─first_name─┐
│ Alex │ Alex │ 26.9 │ Alexey │
└──────────────┴──────────────────┴─────────────────┴────────────┘


