Skip to content

ClickHouse リリース 26.10

neutral avatar 400804ae96
2026年11月6日 · 35 minutes read

またひと月が過ぎ、新たなリリースの時期がやってきました!

ClickHouse 26.10 リリースには、104 件の新機能 🎃、225 件のパフォーマンス最適化 🍂、739 件のバグ修正 🦉 が含まれています。

本リリースでは、SQL の可読性を高める関数合成、より柔軟な JOIN 条件、テキストおよび SQLite ファイルを扱う新しい手段が導入されています。

さらに、GROUP BY クエリの高速化、IN サブクエリのディスクスピル、同じデータの解凍やデシリアライズを繰り返すのを防ぐ実験的なカラムキャッシュも追加されました。

新規コントリビューター

26.10 の新規コントリビューターの皆さまを心より歓迎します!ClickHouse コミュニティの成長にはいつも身の引き締まる思いであり、ClickHouse の普及を支えてくださる数多くの貢献に深く感謝いたします。

新規コントリビューターの皆さまのお名前は以下のとおりです。

ARON, Aarav, Abdullah Kaya, Akanshaak, Aleh Strakachuk, Aleksandr Efimov, Alexey Sitnikov, Andrea Debernardi, Andrew Wilkins, Angelica Ismailos, Armin Braun, Arvind Kumar, Ashish Choubey, Azarudeen Shariff A, Azhar Izzannada E, Bentsi Leviav, Chester, Craig Ringer, David Rolfe, Denis Voytyuk, Dimitri König, Frank Denis, Fábio Neves, GON, Gheorghe Pucea, Ilias, Ilias Aberkane, Javier G. Sogo, Jay Pokale, Jaysurya Ray, Jonathan Ackerman, Knut-Aage-Hofseth, Krithika Balagurunathan, LeeTeng2001, LyJacky, Maksim Andreev, Mike Lewis, Mishalski, Mohammed Sohail, Nick Akey, Oleg Milanov, Prince Maurya, RJ, Ray Winkelman, Rion Bora, Sam Wolfberg, Shahzeb Ali, Thomas Sileghem, Tom Alard, Unamed, Uwez Khan, Vadim Lopatin, Vector, Vitaly Pryakhin, Vlad, VrtxOmega, Zach Rippas, ZainnQureshii, ajay tomar, amogiska, antonioscaramuzzino, arc1999, artemkulyk, awss, azharizz, bentsileviav, bisht2050, clickgapai, comino, dhruvtaneja-ch, dosedaf, erezrokah, gonappuccino, jacky, linjiayu1025-collab, namtran1812, seva-potapov, sunliqiang, superdiaodiao, tianhui-fu, tridao, turbolytics, turbolytics.io, ultrabig, vector524

ヒント: このリストの作成方法に興味がある方は…こちらをご覧ください。

プレゼンテーションのスライドもご覧いただけます。

関数合成演算子

コントリビューター: Ivan Esipov、Alexey Milovidov

arrayMap や arrayFilter といった ClickHouse の高階関数を使ったことがある方なら、渡すラムダ式がすぐに関数呼び出しの多重ネストになってしまうのをご存じでしょう。ClickHouse 26.10 では関数合成演算子が追加され、| 演算子で関数をつなぐパイプラインを作成できるようになりました。これにより、ラムダ式の可読性が大幅に向上します。

URL からサイト名を抽出する処理で、2 つのスタイルを比較してみましょう。 URL を小文字化し、www プレフィックスを取り除き、最初の主要なサブドメインを取得してから大文字化します。

WITH [
    'https://WWW.ClickHouse.com/docs',
    'https://GitHub.com/ClickHouse/ClickHouse',
    'https://www.anthropic.com/news'
] AS urls
SELECT
    arrayMap(x -> upper(firstSignificantSubdomain(domainWithoutWWW(lower(x)))), urls) AS with_lambda,
    arrayMap(lower | domainWithoutWWW | firstSignificantSubdomain | upper, urls) AS with_composition
FORMAT Vertical;
Row 1:
──────
with_lambda:      ['CLICKHOUSE','GITHUB','ANTHROPIC']
with_composition: ['CLICKHOUSE','GITHUB','ANTHROPIC']

どちらのカラムも同じ結果を返しますが、合成演算子を使ったバージョンのほうが一連の手順のように読み取れます。この例の各関数は渡された値だけで呼び出せるため、値の配置場所を指定する必要はありませんでした。

関数に追加の引数を渡す場合は、パイプラインからの値が入る位置を _ で指定します。たとえば substring(_, 1, 3) は、渡された文字列の先頭 3 文字を取得します。

WITH ['ClickHouse', 'Anthropic', 'OpenAI'] AS companies
SELECT
    arrayMap(x -> substring(upper(x), 1, 3), companies) AS with_lambda,
    arrayMap(upper | substring(_, 1, 3), companies) AS with_composition
FORMAT Vertical;
Row 1:
──────
with_lambda:      ['CLI','ANT','OPE']
with_composition: ['CLI','ANT','OPE']

高階関数には同じ長さの配列を複数渡すこともでき、位置ごとに並行して処理されます。その場合は、1 つ目の配列の要素に _1、2 つ目の配列の対応する要素に _2 のように指定します。注文の明細合計を計算するために、各価格に数量を掛けて、その結果を価格形式に整形してみましょう。

WITH
    [49.99, 189.00, 19.50] AS prices,
    [2, 1, 4] AS quantities
SELECT
    arrayMap(multiply(_1, _2) | toString | concat('$', _), prices, quantities) AS line_totals;
┌─line_totals─────────────┐
│ ['$99.98','$189','$78'] │
└─────────────────────────┘

プレースホルダーは、| を使わずに単独でラムダ式の短縮記法としても機能します。たとえば、以下の例に示すように、_1 * _2 は (price, quantity) -> price * quantity と同じ意味になります。

WITH
    ['Keyboard', 'Monitor', 'Mouse'] AS products,
    [49.99, 189.00, 19.50] AS prices,
    [2, 1, 4] AS quantities
SELECT
    arrayMap(_1 * _2, prices, quantities) AS line_totals,
    arrayMap(concat(_1, ' x', toString(_2)), products, quantities) AS order_lines
FORMAT Vertical;
Row 1:
──────
line_totals: [99.98,189,78]
order_lines: ['Keyboard x2','Monitor x1','Mouse x4']

内部的には、ClickHouse はクエリ解析中にパイプラインを通常のラムダ式へ変換します。これは、run_query_tree_passes = 1 を指定して EXPLAIN SYNTAX を実行すると確認できます。アナライザーによる書き換え後のクエリが表示されます。

EXPLAIN SYNTAX run_query_tree_passes = 1
SELECT arrayMap(
    lower | domainWithoutWWW | firstSignificantSubdomain | upper,
    ['https://WWW.ClickHouse.com/docs']
) AS sites;
SELECT arrayMap(lambda(tuple(__composed_arg_1), upper(firstSignificantSubdomain(domainWithoutWWW(lower(__composed_arg_1))))), _CAST(['https://WWW.ClickHouse.com/docs'], 'Array(String)')) AS sites
FROM system.one AS __table1

| のチェーンは、最初にお見せしたのと同じネストされた式を持つラムダ式へ変換されているため、どちらのスタイルもまったく同一に実行されます。お好みの書き方を選択できます。

挿入行数に対するクォータ

コントリビューター: Alexey Milovidov

クォータを使うと、一定の時間間隔内でユーザーが実行できる処理量を制限できます。ClickHouse 26.10 では written_rows カウンターが追加され、ユーザーが書き込む行数によってデータ取り込みを制限できるようになりました。

テーブルと、1 分あたり最大 250 回の挿入、合計最大 10 万行まで書き込める ingest ユーザーを作成してみましょう。

CREATE TABLE events (id UInt64) 
ORDER BY id;
CREATE USER ingest IDENTIFIED WITH no_password;
GRANT INSERT, SELECT ON default.events TO ingest;
CREATE QUOTA ingest_limit
FOR INTERVAL 1 minute MAX written_rows = 100000, query_inserts = 250
TO ingest;

最初の 6 万行の挿入は何の問題もなく完了します。

INSERT INTO events 
SELECT number 
FROM numbers(60000);

しかし、同じ 1 分以内にもう一度同じ挿入を実行すると、上限を超えてしまいます。

Received exception:
Code: 201. DB::Exception: Quota for user `ingest` for 60s has been exceeded: written_rows = 120000/100000. Interval will end at 2026-10-23 14:00:00. Name of quota template: `ingest_limit`. (QUOTA_EXCEEDED)

拒否された挿入ではデータが書き込まれないため、テーブルには 6 万行が保持されたままになります。他のクォータと同様に、各ユーザーが上限にどれだけ近づいているかは system.quotas_usage で確認できます。

SELECT quota_name, written_rows, max_written_rows, query_inserts, max_query_inserts
FROM system.quotas_usage
WHERE quota_name = 'ingest_limit'
FORMAT Vertical;
Row 1:
──────
quota_name:        ingest_limit
written_rows:      120000
max_written_rows:  100000
query_inserts:     2
max_query_inserts: 250

新しい Array および Map 関数

コントリビューター: Minh Vu、Jimmy Aguilar Mena、UnamedRus、Alexey Milovidov

この 26.10 リリースでは、配列やマップを操作するための新しい関数もいくつか追加されています。

arrayMinIndex と arrayMaxIndex は、各要素にラムダ式を適用した上で(省略可能)、最小要素および最大要素の位置を返します。arrayRemoveAt は指定された位置の要素を削除し、負の位置を指定した場合は末尾から逆算します。

いくつかの小さな配列で、それぞれが何を返すか確認してみましょう。

WITH [2,5,10,9,7] AS values

SELECT
    arrayMinIndex(values) AS min_index,
    arrayMaxIndex(values) AS max_index,
    arrayMaxIndex(x -> length(x), ['a', 'abc', 'ab', 'abcd']) AS longest_index,
    arrayRemoveAt(values, 2) AS remove_second,
    arrayRemoveAt(values, -1) AS remove_last
FORMAT Vertical;
Row 1:
──────
min_index:     1
max_index:     3
longest_index: 4
remove_second: [2,10,9,7]
remove_last:   [2,5,10,9]

ClickHouse の配列インデックスは 1 から始まるため、配列の先頭位置にある最小値 2 のインデックスは 1 になります。

この結果を別の配列にそのまま渡して、対応する値を参照することもできます。以下の例では、1 週間の中で最も寒い日と最も暖かい日を特定し、その後に金曜日をリストから除外しています。

WITH
    ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'] AS days,
    [18.5, 21.2, 16.9, 24.3, 22.0] AS temps
SELECT
    days[arrayMinIndex(temps)] AS coldest,
    days[arrayMaxIndex(temps)] AS warmest,
    arrayRemoveAt(days, -1) AS without_friday;
┌─coldest─┬─warmest─┬─without_friday────────────┐
│ Wed     │ Thu     │ ['Mon','Tue','Wed','Thu'] │
└─────────┴─────────┴───────────────────────────┘

マップ向けには、キーを削除する mapRemove、キーと値のペアをタプルの配列として返す mapEntries、マップに特定のキーと値のペアが含まれているかを判定する mapContainsKeyValue が追加されました。

WITH map('env', 'prod', 'region', 'eu-west-1', 'team', 'payments') AS labels
SELECT
    mapRemove(labels, 'team') AS without_team,
    mapEntries(labels) AS entries,
    mapContainsKeyValue(labels, 'env', 'prod') AS is_prod
FORMAT Vertical;
Row 1:
──────
without_team: {'env':'prod','region':'eu-west-1'}
entries:      [('env','prod'),('region','eu-west-1'),('team','payments')]
is_prod:      1

mapContainsKeyValue はテキストインデックスも利用できるため、行数が膨大な場合でもラベルによるフィルタリングが格段に高速化されます。

最後に、-Map コンビネーターで Map カラムが不要になりました。代わりに最後の引数としてキーを渡せるようになり、行から直接キーごとの集約を簡単に行えるようになりました。

SELECT
    sumMap(views, country) AS views_by_country,
    argMaxMap(page, views, country) AS top_page_by_country
FROM VALUES('country String, page String, views UInt32',
    ('UK', '/docs', 120), ('UK', '/blog', 340),
    ('US', '/docs', 910), ('US', '/pricing', 450),
    ('DE', '/blog', 75));
┌─views_by_country─────────────┬─top_page_by_country──────────────────────┐
│ {'DE':75,'UK':460,'US':1360} │ {'DE':'/blog','UK':'/blog','US':'/docs'} │
└──────────────────────────────┴──────────────────────────────────────────┘

SQL 互換の整数除算

コントリビューター: Ilya Yatsishin

ClickHouse では、整数同士の除算は常に浮動小数点数を返してきたため、7 / 2 の結果は 3.5 になります。一方、PostgreSQL、SQL Server、SQLite ではいずれも小数点以下が切り捨てられて 3 になります。

リリースコールでの Alexey の見解は「ClickHouse が正しく、他がすべて間違っている」というものでしたが、他のデータベースからクエリを移行する際に同じように切り捨てる必要がある場合に備えて設定が追加されました。デフォルトの挙動は以下のとおりです。

SELECT 7 / 2, -7 / 2, 7 / 2.0, toTypeName(7 / 2);
┌─divide(7, 2)─┬─divide(-7, 2)─┬─divide(7, 2.)─┬─toTypeName(divide(7, 2))─┐
│          3.5 │          -3.5 │           3.5 │ Float64                  │
└──────────────┴───────────────┴───────────────┴──────────────────────────┘

PostgreSQL 方式を好む場合は、sql_compatible_division_for_integers を有効化できます。

SET sql_compatible_division_for_integers = 1;

SELECT 7 / 2, -7 / 2, 7 / 2.0, toTypeName(7 / 2);
┌─divide(7, 2)─┬─divide(-7, 2)─┬─divide(7, 2.)─┬─toTypeName(divide(7, 2))─┐
│            3 │            -3 │           3.5 │ UInt8                    │
└──────────────┴───────────────┴───────────────┴──────────────────────────┘

結果はゼロ方向に切り捨てられて整数型が維持されますが、浮動小数点数による除算は引き続き浮動小数点数を返します。この設定を有効にすると、ゼロ除算時は inf を返すのではなくエラーが発生します。

SET sql_compatible_division_for_integers = 1;

SELECT 1 / 0;
Received exception:
Code: 153. DB::Exception: Division by zero: In scope SELECT divideExact(1, 0) AS `divide(1, 0)`. (ILLEGAL_DIVISION)

この設定はデフォルトで無効になっています。ビューなどの保存された定義は作成時の除算挙動を維持するため、有効にしても既存のビューの結果が変わることはありません。

IN サブクエリのディスクスピル

コントリビューター: Nihal Z. Miaji

サブクエリで IN を使用すると、ClickHouse はそのサブクエリが返した値のセットを構築し、外側のクエリの行をそのセットと照合します。セットが大きいと大量のメモリを消費する可能性があります。ClickHouse 26.10 では、このセットをディスクへスピル(退避)する機能が追加され、より小さなメモリ制限でもクエリを完了できるようになりました。

1995 年以降のイングランドおよびウェールズの不動産売買データを含む 英国の不動産価格データセット で試してみましょう。2024 年以前にも売買記録がある住所について、2024 年の売買件数を集計します。建物や(該当する場合は)部屋を特定する郵便番号と 2 つの住所フィールド addr1、addr2 で照合します。これは恒久的な不動産 ID ではなく住所ベースの照合であり、ユニークな物件数ではなく売買件数をカウントしています。

まず、クエリのメモリ制限を 4 GiB に設定し、スピルを無効のまま実行してみます。

SELECT count() AS repeat_address_sales_2024
FROM uk_price_paid
WHERE date >= '2024-01-01'
  AND date < '2025-01-01'
  AND (postcode1, postcode2, addr1, addr2) IN
  (
      SELECT postcode1, postcode2, addr1, addr2
      FROM uk_price_paid
      WHERE date < '2024-01-01'
  )
SETTINGS max_threads = 2,
         max_memory_usage = '4Gi',
         max_bytes_before_external_set = 0;
┌─repeat_address_sales_2024─┐
│                    558458 │
└───────────────────────────┘

1 row in set. Elapsed: 5.777 sec. Processed 60.90 million rows, 966.34 MB (10.54 million rows/s., 167.27 MB/s.)
Peak memory usage: 1.96 GiB.

サブクエリは住所のセットを構築するために、過去の 2,930 万件の売買を読み取ります。ClickHouse 26.10 では、このクエリのピークメモリ使用量は 1.96 GiB でした。メモリ制限を 512 MiB に下げるとどうなるか確認してみましょう。

SELECT count() AS repeat_address_sales_2024
FROM uk_price_paid
WHERE date >= '2024-01-01'
  AND date < '2025-01-01'
  AND (postcode1, postcode2, addr1, addr2) IN
  (
      SELECT postcode1, postcode2, addr1, addr2
      FROM uk_price_paid
      WHERE date < '2024-01-01'
  )
SETTINGS max_threads = 2,
         max_memory_usage = '512Mi',
         max_bytes_before_external_set = 0;
Elapsed: 0.978 sec. Processed 7.93 million rows, 122.04 MB (8.10 million rows/s., 124.75 MB/s.)

Received exception:
Code: 241. DB::Exception: Query memory limit exceeded: would use 537.51 MiB (attempt to allocate chunk of 63.96 MiB), maximum: 512.00 MiB: While executing CreatingSetsTransform. (MEMORY_LIMIT_EXCEEDED)

ClickHouse はメモリ不足(OOM)になります。ここで max_bytes_before_external_set を設定してスピルを有効にできます。クエリ全体のメモリ制限を 512 MiB に保ったまま、しきい値を 128 MiB に設定してみます。

SELECT count() AS repeat_address_sales_2024
FROM uk_price_paid
WHERE date >= '2024-01-01'
  AND date < '2025-01-01'
  AND (postcode1, postcode2, addr1, addr2) IN
  (
      SELECT postcode1, postcode2, addr1, addr2
      FROM uk_price_paid
      WHERE date < '2024-01-01'
  )
SETTINGS max_threads = 2,
         max_memory_usage = '512Mi',
         max_bytes_before_external_set = '128Mi';
┌─repeat_address_sales_2024─┐
│                    558458 │
└───────────────────────────┘

1 row in set. Elapsed: 3.185 sec. Processed 60.90 million rows, 966.48 MB (19.12 million rows/s., 303.44 MB/s.)
Peak memory usage: 203.11 MiB.

今回はピークメモリ使用量が 203 MiB になりました。これはスピルのしきい値である 128 MiB を上回っていますが、この設定は厳密なメモリ制限を課すものではなく、スピルを開始するタイミングを ClickHouse に指示するものです。クエリにはデータの読み取りと処理のためのメモリも必要なため、スピルしきい値は全体の制限である 512 MiB よりも低く設定します。

クエリの実行中にスペースキーを押してプロファイルイベントを確認すると、ClickHouse が 1 つのセットをディスクにスピルし、4 つのパートを書き込んだことがわかります。

SetsSpilledToDisk:          1
ExternalSetWritePart:       4
ExternalSetCompressedBytes: 470342352

使用したさまざまな構成のまとめは以下の表のとおりです。

メモリ制限スピル結果ピークメモリ
4 GiB無効558,4581.96 GiB
512 MiB無効メモリ制限超過—
512 MiB128 MiB558,458203 MiB

LowCardinality カラムによる GROUP BY の高速化

コントリビューター: artemkulyk、Samuel Krempaský、Alexey Milovidov

ClickHouse 26.10 では、複数の LowCardinality カラムによるグループ化が改善されました。辞書から直接グループ化キーをシリアライズし、キーの重複する組み合わせに対する集約状態のルックアップをキャッシュできるようになりました。

先ほどの uk_price_paid テーブルを再利用して、郡(county)、地区(district)、町(town)、郵便番号外側コード(outward postcode)の組み合わせごとに売買件数をカウントし、平均価格を計算してみましょう。4 つのグループ化カラムはすべて LowCardinality(String) 型です。

SELECT
    county,
    district,
    town,
    postcode1,
    count() AS sales,
    avg(price) AS average_price
FROM uk_price_paid
GROUP BY county, district, town, postcode1
SETTINGS use_query_cache = 0
FORMAT Null;

ターミナル出力が比較に影響しないよう、FORMAT Null を使用して 15,409 行の結果を出力せずにクエリを実行します。

ClickHouse 26.9 でクエリを 3 回実行してみます。

0 rows in set. Elapsed: 0.289 sec. Processed 30.45 million rows, 331.39 MB (105.20 million rows/s., 1.14 GB/s.)
Peak memory usage: 170.67 MiB.

0 rows in set. Elapsed: 0.269 sec. Processed 30.45 million rows, 331.39 MB (113.29 million rows/s., 1.23 GB/s.)
Peak memory usage: 171.81 MiB.

0 rows in set. Elapsed: 0.255 sec. Processed 30.45 million rows, 331.39 MB (119.19 million rows/s., 1.30 GB/s.)
Peak memory usage: 177.98 MiB.

次に、同じデータとテーブルレイアウトを使用して ClickHouse 26.10 で 3 回実行します。どちらのバージョンでも、手元のマシンでデフォルトの 12 スレッドを使用しています。

0 rows in set. Elapsed: 0.242 sec. Processed 30.45 million rows, 331.39 MB (125.90 million rows/s., 1.37 GB/s.)
Peak memory usage: 128.49 MiB.

0 rows in set. Elapsed: 0.181 sec. Processed 30.45 million rows, 331.39 MB (168.49 million rows/s., 1.83 GB/s.)
Peak memory usage: 124.16 MiB.

0 rows in set. Elapsed: 0.171 sec. Processed 30.45 million rows, 331.39 MB (178.37 million rows/s., 1.94 GB/s.)
Peak memory usage: 127.35 MiB.

各バージョンの最速タイムを比較すると、26.9 では 255 ミリ秒、26.10 では 171 ミリ秒となり、26.10 のほうが 30% 強高速化されています。

GROUP BY 向けの Top-K 動的フィルタリング

コントリビューター: Alexey Milovidov、Konstantin Bogdanov

ClickHouse 25.12 では、現在の Top-K 境界を利用して不要なカラム読み取りを回避し、minmax インデックスによってグラニュールをスキップする ORDER BY … LIMIT クエリ向けの動的フィルタリングが導入されました。ClickHouse 26.10 では、このアプローチが GROUP BY クエリにも拡張され、プライマリインデックスによるグラニュールスキップが追加されました。

英国の不動産データを再利用して、売買が記録された直近 10 日間を、売買件数および平均価格とともに取得してみましょう。クエリで使用する 2 つのカラムを含み、date でソートされた別のテーブルを作成します。このレイアウトにより、ClickHouse は両方のメカニズムを活用できます。価格を読み取る前に過去の売買をフィルタリングし、日付がすべてカットオフ値より古いグラニュールをプライマリインデックスでスキップします。

CREATE TABLE uk_price_paid_by_date
ENGINE = MergeTree
ORDER BY date
AS SELECT date, price FROM uk_price_paid;

OPTIMIZE TABLE uk_price_paid_by_date FINAL;

これにより、3,000 万件の売買データがすべてコピーされ、新しいテーブルが単一のパートにマージされます。出力結果を確認するために ClickHouse 26.10 でクエリを実行します。

SELECT
    date,
    count() AS sales,
    round(avg(price)) AS average_price
FROM uk_price_paid_by_date
GROUP BY date
ORDER BY date DESC
LIMIT 10
SETTINGS
    enable_group_by_top_k_dynamic_filtering = 0,
    use_query_condition_cache = 0,
    use_query_cache = 0;

クエリは次の 10 件の日付を返します。

┌───────date─┬─sales─┬─average_price─┐
│ 2025-07-31 │    26 │        300021 │
│ 2025-07-30 │    39 │        316864 │
│ 2025-07-29 │    92 │        344986 │
│ 2025-07-28 │   220 │        317645 │
│ 2025-07-27 │     4 │        242998 │
│ 2025-07-25 │   926 │        316243 │
│ 2025-07-24 │   388 │        358814 │
│ 2025-07-23 │   437 │        347647 │
│ 2025-07-22 │   373 │        332035 │
│ 2025-07-21 │   662 │        337260 │
└────────────┴───────┴───────────────┘

clickhouse-local を使用して、最適化の有無によるテストを実施します。処理時間は CLI の --time の出力、読み取り行数とピークメモリは --print-profile-events の出力に基づいています。

実行回クエリ実行時間読み取り行数処理データ量ピークメモリ
145 ms30,452,463182.71 MB7.18 MiB
227 ms30,452,463182.71 MB7.26 MiB
331 ms30,452,463182.71 MB7.26 MiB

enable_group_by_top_k_dynamic_filtering を 1 に変更すると、以下のようになります。

実行回クエリ実行時間読み取り行数処理データ量ピークメモリ
19 ms1,728,26310.37 MB7.30 MiB
28 ms1,793,67210.50 MB7.30 MiB
310 ms1,728,26310.11 MB7.24 MiB

結果の表から、フィルタリングを有効にすると ClickHouse の読み取り行数が約 94% 削減されることがわかります。最適化を有効にした実行では、それぞれ 3,529、3,521、3,529 個のグラニュールがスキップされましたが、無効な場合はまったくスキップされませんでした。並列集約ストリームがカットオフ値を検出して公開するタイミングによって、実際の処理量には多少のばらつきが生じます。

グラニュール全体がスキップされたことは、TopKGranulesSkippedByPrimaryKey プロファイルイベントで確認できます。--print-profile-events --profile-events-delay-ms=-1 を指定すると、CLI は最終的なイベント合計を出力します。有効時の最初の実行における関連する値は以下のとおりです。

SelectedRows:                     1728263
SelectedBytes:                    10369578
TopKGranulesSkippedByPrimaryKey:  3529

カラムキャッシュ

コントリビューター: Alexey Milovidov

ClickHouse 26.10 では実験的なカラムキャッシュが導入され、デシリアライズ済みのカラムデータをメモリ内に保持することで、以降のクエリで同じデータを再度解凍およびデシリアライズする手間を省けます。

英国の不動産データセットを再利用して、最初の住所フィールドに HOUSE が含まれる売買件数をカウントしてみましょう。元のソート順を維持したまま、価格と住所のカラムを含むコピーを作成します。

CREATE TABLE uk_price_paid_columns_cache
ENGINE = MergeTree
ORDER BY (postcode1, postcode2, addr1, addr2)
SETTINGS min_bytes_for_wide_part = 0,
         min_rows_for_wide_part = 0
AS SELECT postcode1, postcode2, addr1, addr2, price FROM uk_price_paid;

このキャッシュは現在、ワイドな MergeTree パートをサポートしています。両方のサイズしきい値を 0 に設定することで、この形式を強制します。

まず、カラムキャッシュを無効にした状態でクエリを実行してみましょう。

SELECT count() AS house_sales
FROM uk_price_paid_columns_cache
WHERE addr1 LIKE '%HOUSE%'
SETTINGS use_columns_cache = 0,
         use_query_cache = 0,
         use_query_condition_cache = 0,
         log_queries = 1,
         log_profile_events = 1,
         log_comment = 'sorted-columns-cache:off-1';
┌─house_sales─┐
│      904871 │
└─────────────┘

ファイルシステムキャッシュをウォームアップするために測定外で 1 回実行した後、ログコメントに sorted-columns-cache:off-1、sorted-columns-cache:off-2、sorted-columns-cache:off-3 を指定してこのクエリを 3 回実行します。

続いて use_columns_cache を 1 に変更し、コメントに sorted-columns-cache:on-1、sorted-columns-cache:on-2、sorted-columns-cache:on-3 を指定してさらに 3 回実行します。有効時の最初の実行でカラムキャッシュにデータが読み込まれ、その後の 2 回でキャッシュが再利用されます。

キャッシュを有効にすると、クエリは addr1 をキャッシュします。たとえば price は読み取られないためキャッシュされません。ただし、price に条件を追加した場合は、そのカラムから読み取られたデータもキャッシュされます。

クエリログを有効にしていれば、以下のクエリを使用して system.query_log から実行時間とキャッシュカウンターを取得できます。

SYSTEM FLUSH LOGS;

SELECT
    log_comment AS run,
    query_duration_ms AS queryTime,
    ProfileEvents['ColumnsCacheHits'] AS cacheHits,
    ProfileEvents['ColumnsCacheMisses'] AS cacheMisses,
    ProfileEvents['SelectedRows'] AS selectedRows
FROM
(
    SELECT *
    FROM system.query_log
    WHERE type = 'QueryFinish'
      AND startsWith(log_comment, 'sorted-columns-cache:')
      AND log_comment != 'sorted-columns-cache:warmup'
    ORDER BY query_start_time_microseconds DESC
    LIMIT 6
)
ORDER BY query_start_time_microseconds
FORMAT Markdown;
runqueryTimecacheHitscacheMissesselectedRows
sorted-columns-cache:off-1340030452463
sorted-columns-cache:off-2320030452463
sorted-columns-cache:off-3330030452463
sorted-columns-cache:on-1370371830452463
sorted-columns-cache:on-2123718030452463
sorted-columns-cache:on-3123718030452463

カラムキャッシュが無効の場合、クエリには 32〜34 ミリ秒かかります。ClickHouse がキャッシュを参照しないため、cacheHits は 0 です。

キャッシュにデータが取り込まれる実行には 37 ミリ秒かかり、キャッシュを利用するその後の 2 回の実行はそれぞれ 12 ミリ秒で完了し、addr1 の全 3,718 グラニュールがカラムキャッシュから読み取られました。

SelectedRows は変わりません。ClickHouse は依然として 3,045 万行すべてに対してフィルターを評価しますが、デコード済みの住所データを再利用できます。キャッシュはクエリ結果ではなくカラムデータを保存するため、同じデータを読み取る他のクエリもその恩恵を受けられます。

LATERAL JOIN

コントリビューター: Dimitri König、Alexey Milovidov

ClickHouse 26.10 では LATERAL JOIN の実験的サポートが追加され、サブクエリが先行するテーブルのカラムを参照できるようになりました。allow_experimental_lateral_join = 1 を設定することで有効化できます。

結合のサンプルで使用している IMDb データセットを使って、3 人の俳優それぞれについて評価の高い映画上位 3 本を検索してみましょう。actors テーブルには俳優の名前、movies には映画の詳細と評価、roles には俳優が出演した映画との関連付けが格納されています。

SELECT
    concat(a.first_name, ' ', a.last_name) AS actor,
    top_titles.name AS title,
    top_titles.year AS year,
    round(top_titles.rank, 1) AS rating
FROM actors AS a
JOIN LATERAL
(
    SELECT m.id, m.name, m.year, m.rank
    FROM roles AS r
    INNER JOIN movies AS m ON r.movie_id = m.id
    WHERE r.actor_id = a.id
      AND m.rank IS NOT NULL
    GROUP BY m.id, m.name, m.year, m.rank
    ORDER BY m.rank DESC, m.year DESC, m.id
    LIMIT 3
) AS top_titles ON true
WHERE (a.first_name, a.last_name) IN
    (('Tom', 'Hanks'), ('Meryl', 'Streep'), ('Denzel', 'Washington'))
ORDER BY actor, rating DESC, year DESC, top_titles.id
SETTINGS allow_experimental_lateral_join = 1;

12 行目が重要な部分です: WHERE r.actor_id = a.id。a.id の値は、外側のクエリの actors AS a から渡されます。LATERAL によって内側のクエリがその値を使用できるようになり、この俳優のトップ 3 作品を検索できるようになります。LIMIT 3 はデータセット全体から 3 作品を選択するのではなく、俳優ごとに適用されます。

選択された各俳優について、内側のクエリはその役割を検索し、映画の詳細と結合して、評価のない映画を除外します。GROUP BY は、1 人の俳優が複数の役柄レコードを持つ場合の映画の重複を排除します。その後、評価順にソートし(同点の場合は公開年と映画 ID で順位を決定)、上位 3 件を返します。

┌─actor─────────────┬─title──────────────────┬─year─┬─rating─┐
│ Denzel Washington │ Glory                  │ 1989 │    8.1 │
│ Denzel Washington │ Much Ado About Nothing │ 1993 │    7.5 │
│ Denzel Washington │ Antwone Fisher         │ 2002 │    7.4 │
│ Meryl Streep      │ Manhattan              │ 1979 │    8.2 │
│ Meryl Streep      │ Deer Hunter, The       │ 1978 │    8.1 │
│ Meryl Streep      │ Adaptation.            │ 2002 │    7.9 │
│ Tom Hanks         │ Saving Private Ryan    │ 1998 │    8.3 │
│ Tom Hanks         │ Green Mile, The        │ 1999 │    8.1 │
│ Tom Hanks         │ Toy Story 2            │ 1999 │    8.1 │
└───────────────────┴────────────────────────┴──────┴────────┘

各俳優につき 3 作品ずつ、計 9 行が取得されます。

任意の条件による JOIN

コントリビューター: Vladimir Cherkasov、Alexey Milovidov

ClickHouse 26.10 では、テーブル間に等値条件がないものを含め、任意の ON 条件による JOIN もサポートされました。これを使って、URL パターンに基づき Web サイトのトラフィックを分類してみましょう。

まず、ページビュー数を含むテーブルを作成します。

CREATE TABLE page_views
(
    path String,
    views UInt64
);

INSERT INTO page_views VALUES
    ('/docs/getting-started', 1200),
    ('/docs/sql-reference/functions', 850),
    ('/blog/clickhouse-release-26-10', 2400),
    ('/pricing', 600),
    ('/about', 300);

分類ルールは別のテーブルに保存します。ワイルドカード % は任意の文字列にマッチします。

CREATE TABLE page_categories
(
    pattern String,
    category String
);

INSERT INTO page_categories VALUES
    ('/docs/%', 'Documentation'),
    ('/blog/%', 'Blog'),
    ('/pricing', 'Pricing');

これで、LIKE を使って各パスをカテゴリにマッチさせ、テーブル同士を結合できます。

SELECT p.path, p.views, c.category
FROM page_views AS p
LEFT JOIN page_categories AS c ON p.path LIKE c.pattern
ORDER BY p.views DESC;
┌─path───────────────────────────┬─views─┬─category──────┐
│ /blog/clickhouse-release-26-10 │  2400 │ Blog          │
│ /docs/getting-started          │  1200 │ Documentation │
│ /docs/sql-reference/functions  │   850 │ Documentation │
│ /pricing                       │   600 │ Pricing       │
│ /about                         │   300 │               │
└────────────────────────────────┴───────┴───────────────┘

任意の結合による柔軟性にはコストが伴う点にご注意ください。ブロックネストループ結合はすべての行のペアをチェックするため、クロス結合並みに遅くなる可能性があり、クエリプランナーでは最後の手段として使用されます。

Freeform 入力フォーマット

コントリビューター: Tri Dao、Alexey Milovidov

ClickHouse 26.10 では、一貫した構造を持つテキストからカラムとその型を推論する Freeform 入力フォーマットも追加されました。

ls -l の出力を clickhouse-local にパイプして、どのように動作するか見てみましょう。パイプされた入力は table として利用可能になり、他のテーブルと同様にクエリを実行できます。次のクエリは ls -l の出力を記述します:

ls -l **/*.parquet | 
./clickhouse --input-format Freeform --query "DESCRIBE TABLE table"
c0	String
c1	Int64
c2	String
c3	String
c4	Int64
c5	Int64
c6	String
c7	Int64
c8	Int64
c9	String

この出力から、ls -l が 10 個のカラムを出力しており、それらに c0 から c9 という名前が付けられていることがわかります。次のクエリを使って table をクエリし、すべてのカラムを返すことができます。

ls -l **/*.parquet | 
./clickhouse --input-format Freeform --query "FROM table FORMAT Vertical"
Row 1:
──────
c0: -rw-r--r--@
c1: 1
c2: markhneedham
c3: staff
c4: 2048074919 -- 2.05 billion
c5: 30
c6: Oct
c7: 16
c8: 23
c9: benchmarks/amazon-columns-cache/data/amazon_reviews_2010.snappy.parquet

Row 2:
──────
c0: -rw-r--r--@
c1: 1
c2: markhneedham
c3: staff
c4: 3013406813 -- 3.01 billion
c5: 30
c6: Oct
c7: 16
c8: 27
c9: benchmarks/amazon-columns-cache/data/amazon_reviews_2011.snappy.parquet

カラムにわかりやすい名前を付け、ファイルサイズをフォーマットし、日付フィールドをタイムスタンプに結合してみましょう。この一覧には年が含まれていないため、これらの最近のファイルには現在の年を使用します:

ls -l **/*.parquet |
./clickhouse --input-format Freeform --query "
    SELECT
        c9 AS file,
        formatReadableSize(c4) AS size,
        parseDateTimeBestEffort(
            format('{} {} {} {}:{}', c5, c6, toYear(today()), c7, c8)
        ) AS modified
    FROM table
    ORDER BY c4 DESC
    FORMAT Vertical"
Row 1:
──────
file:     benchmarks/amazon-columns-cache/data/amazon_reviews_2011.snappy.parquet
size:     2.81 GiB
modified: 2026-10-30 16:27:00

Row 2:
──────
file:     benchmarks/amazon-columns-cache/data/amazon_reviews_2010.snappy.parquet
size:     1.91 GiB
modified: 2026-10-30 16:23:00

SQLite のインポートとエクスポート

コントリビューター: Yue Ni、Alexey Milovidov

ClickHouse はすでに、データベースエンジン, テーブルエンジン, テーブル関数を介した SQLite データベースへのクエリ実行をサポートしています。バージョン 26.10 では入力および出力フォーマットとして SQLite が追加され、クエリ結果を SQLite データベースファイルに直接エクスポートできるようになりました。これにより、SQLite ツールですぐに開いたり、他の人と共有したりできます。

IMDb の movies テーブルを再利用して、1990 年代の各年の映画数をカウントし、平均評価を計算してみましょう:

SELECT
    year,
    toInt64(count()) AS films,
    round(avg(rank), 2) AS average_rating
FROM movies
WHERE year BETWEEN 1990 AND 1999
GROUP BY year
ORDER BY year
INTO OUTFILE 'films.sqlite'
SETTINGS output_format_sqlite_table_name = 'films_by_year';

出力ファイルの拡張子が .sqlite であるため、ClickHouse は SQLite フォーマットを使用しますが、ファイル名の後に FORMAT SQLite を指定して明示的に出力を指定することもできます。films_by_year というテーブルに 10 件の結果を書き込みます。

sqlite3 コマンドラインツールでファイルを開き、クエリを実行できます:

sqlite3 -header -column films.sqlite \
  'SELECT * FROM films_by_year ORDER BY year LIMIT 5;'
year  films  average_rating
----  -----  --------------
1990  6098   5.33
1991  6068   5.69
1992  6580   5.53
1993  6900   5.55
1994  7502   5.43

ClickHouse はカラム名と型を推論して、ファイルを読み戻すこともできます:

SELECT *
FROM file('films.sqlite')
ORDER BY year
LIMIT 5;
┌─year─┬─films─┬─average_rating─┐
│ 1990 │  6098 │           5.33 │
│ 1991 │  6068 │           5.69 │
│ 1992 │  6580 │           5.53 │
│ 1993 │  6900 │           5.55 │
│ 1994 │  7502 │           5.43 │
└──────┴───────┴────────────────┘

スキャン中の文字列フィルタリング

コントリビューター: Alexey Milovidov

ClickHouse 26.10 では、カラムのデシリアライズ中に LIKE、startsWith、endsWith などの文字列フィルターをチェックできるようになり、一致しない文字列のメモリへのコピーを回避できます。この最適化は、apply_string_filters_during_scan によってデフォルトで有効になっています。

Amazon レビューデータセットで試してみましょう。Freeform の例にある 2010 年と 2011 年の Parquet ファイルを MergeTree テーブルにロードすると、998 万件のレビューが得られます:

CREATE TABLE amazon_reviews
ENGINE = MergeTree
ORDER BY tuple()
AS SELECT *
FROM file('benchmarks/amazon-columns-cache/data/amazon_reviews_{2010,2011}.snappy.parquet');

disappointed という単語を含むレビューをカウントしてみます。まず、カラムキャッシュ、クエリキャッシュ、クエリ条件キャッシュとともに、この最適化を無効化します:

SELECT count() AS matching_reviews
FROM amazon_reviews
WHERE review_body LIKE '%disappointed%'
SETTINGS apply_string_filters_during_scan = 0,
         use_columns_cache = 0,
         use_query_cache = 0,
         use_query_condition_cache = 0;
┌─matching_reviews─┐
│           293115 │
└──────────────────┘

このクエリを、テーブルを 1 つの Wide パートにマージした後、デフォルトの 12 スレッドを設定した ClickHouse 26.10 で実行しました。各設定で 1 回ウォームアップを行った後、最適化を無効にした状態と有効にした状態で測定を 3 回ずつ交互に実施しました。以下の所要時間とピークメモリの数値は system.query_log によるものです。

apply_string_filters_during_scan = 0 の場合:

実行回クエリ時間ピークメモリ
11.204 s128.66 MiB
21.303 s127.93 MiB
31.251 s129.05 MiB

apply_string_filters_during_scan を 1 に変更した場合:

実行回クエリ時間ピークメモリ
11.091 s2.54 MiB
21.333 s2.54 MiB
31.232 s2.54 MiB

どの実行でも同じ 293,115 件の一致が返されます。実行時間はほぼ同じですが、ピークメモリは約 129 MiB から 2.54 MiB へと約 98% 削減されています。

今すぐ始める

ご自身のデータで ClickHouse の動作を確認してみませんか?ClickHouse Cloud は数分で使い始めることができ、300 ドル分の無料クレジットも進呈されます。

サインアップ

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

Follow us

XBlueskySlackGithubTelegramMeetupRSS