またひと月が経ち、新しいリリースの時期がやってきました!
ClickHouse 26.4 リリースには、39 件の新機能 🌷 45 件のパフォーマンス最適化 🐇 238 件のバグ修正 🐝 が含まれています。
今回のリリースでは、さらに多くの機能が標準 SQL と互換になり、COUNT DISTINCT が高速化され、EXPLAIN の表示がさらに見やすくなるなど、盛りだくさんの内容となっています!
新しいコントリビューターの皆さん
26.4 のすべての新しいコントリビューターを心から歓迎します! ClickHouse コミュニティの広がりにはいつも圧倒されます。ClickHouse をここまで成長させてくれた皆さんの貢献に、心より感謝いたします。
新しいコントリビューターのお名前は以下のとおりです。
Alexander Kuleshov, Alsu, Anton Frost, Aruj Bansal, Asya, ClickGap AI Bot, Denys Melnyk, Diego Gomes Tome, Dustin Healy, Evgeny Kuzin, Farid Adam, Francisco Garcia Florez, Gagan Dhakrey, Gleb Popov, Groene AI, Ivan Mantova, Jaap Elst, Jack Knudson, James Cunningham, JingYanchao, K, Kc Balusu, Matheus Nerone, Michael Russell, MukundaKatta, Nikita Semenov, Pavel Kravtsov, Peng, RenzoMXD, Sergey Veletskiy, Takumi Hara, Timothy Kurniawan, Wenyu Chen, XiaoBinMu, Yuri Fedoseev, ashrithb, asyablue22, dwagner-decix, egor romanov, groeneai, liuguangliang, manerone, nerve-bot, simonhammes, sourcelliu, xiaobin
ヒント: このリストをどのように生成しているか興味がある方は、こちらをご覧ください。
プレゼンテーションのスライドもご覧いただけます。
SQL 互換性: テーブル式としての VALUES、EXTRACT、SET TIME ZONE
26.4 リリースでは、さらに多くの機能が標準 SQL 構文と互換になりました。ここではその一部を紹介しますが、詳細はプレゼンテーションスライドをご覧ください。
テーブル式としての VALUES
コントリビューター: Desel72
まずは VALUES です。このリリースより前は、次のように呼び出すことができました。
SELECT *
FROM VALUES((1, 'a'), (2, 'b'), (3, 'c'));┌─c1─┬─c2─┐
1. │ 1 │ a │
2. │ 2 │ b │
3. │ 3 │ c │
└────┴────┘これに対し、今回から以下のようにテーブル式としても呼び出せるようになりました。
SELECT *
FROM (VALUES (1, 'a'), (2, 'b'), (3, 'c'));┌─c1─┬─c2─┐
1. │ 1 │ a │
2. │ 2 │ b │
3. │ 3 │ c │
└────┴────┘また、カラムにエイリアスを付けられるようにもなりました。これは結合クエリで VALUES を使用する際に便利です。たとえば、これまでは次のように書いていたところを、
SELECT
c.c2,
o.c2
FROM VALUES((1, 'Alice'), (2, 'Bob')) AS c
INNER JOIN VALUES((1, 250), (2, 100), (1, 75)) AS o
ON c.c1 = o.c1;┌─c2────┬─o.c2─┐
1. │ Alice │ 250 │
2. │ Alice │ 75 │
3. │ Bob │ 100 │
└───────┴──────┘カラムに名前を付けることができ、クエリが理解しやすくなります。
SELECT c.name, o.amount
FROM (VALUES (1, 'Alice'), (2, 'Bob')) AS c(id, name)
JOIN (VALUES (1, 250), (2, 100), (1, 75)) AS o(customer_id, amount)
ON c.id = o.customer_id;┌─name──┬─amount─┐
1. │ Alice │ 250 │
2. │ Alice │ 75 │
3. │ Bob │ 100 │
└───────┴────────┘EXTRACT
コントリビューター: Alexey Milovidov
日付の処理で使用する EXTRACT 演算子が PostgreSQL スタイルの単位に対応しました。以下のクエリのように使用できます。
SELECT
EXTRACT(EPOCH FROM now()) AS epoch,
EXTRACT(DOW FROM today()) AS dayOfWeek,
EXTRACT(DOY FROM today()) AS dayOfYear,
EXTRACT(ISODOW FROM today()) AS isoDOW,
EXTRACT(ISOYEAR FROM today()) AS isoYear,
EXTRACT(WEEK FROM today()) AS isoWeek,
EXTRACT(CENTURY FROM today()) AS century,
EXTRACT(DECADE FROM today()) AS decade,
EXTRACT(MILLENNIUM FROM today()) AS millennium;Row 1:
──────
epoch: 1777992683
dayOfWeek: 2
dayOfYear: 125
isoDOW: 2
isoYear: 2026
isoWeek: 19
century: 21
decade: 202
millennium: 3SET TIME ZONE
コントリビューター: phulv94
タイムゾーンを設定するための新しい標準 SQL エイリアスも追加されました。まず、現在のタイムゾーンを確認してみます。
SELECT timezone(), formatDateTime(now(), '%Y-%m-%d %H:%M:%S %z');┌─timezone()────┬─formatDateTim⋯H:%M:%S %z')─┐
1. │ Europe/London │ 2026-05-05 15:May:47 +0100 │
└───────────────┴────────────────────────────┘次に、アムステルダムに設定します。
SET TIME ZONE 'Europe/Amsterdam';上記のクエリを再実行すると、以下のようになります。
┌─timezone()───────┬─formatDateTim⋯H:%M:%S %z')─┐
1. │ Europe/Amsterdam │ 2026-05-05 16:May:59 +0200 │
└──────────────────┴────────────────────────────┘その他の互換性の向上
これだけではありません。NATURAL JOIN のサポート、そのまま使える OVERLAY の互換性、複合 INTERVAL リテラルのサポートなども追加されています!
LIKE でのテキストインデックスの利用
コントリビューター: Elmi Ahmadov
ClickHouse 25.4 から、LIKE/ILIKE のクエリパターンが %<スペースを含まない英数字>% であり、テキストインデックスのトークナイザーが splitByNonAlpha の場合、ClickHouse は転置インデックスを利用してこれらのクエリを高速化します。一致するパターンを見つけるためにテーブル全体をフルスキャンするのではなく、転置インデックスの辞書をスキャンすることでこれを実現しています。
おなじみの HackerNews データセットを使って、この仕組みを確認してみましょう。まず clickhousectl を使い、ノート PC 上で ClickHouse 26.4 を起動します。
chctl local install 26.4次にサーバーを起動します。
chctl local server start --version 26.4そして ClickHouse クライアントを使用して接続します。
chctl local client --name default -mn続いて、HackerNews テーブルを作成します。
CREATE TABLE hackernews
(
`id` Int64,
`deleted` Int64,
`type` String,
`by` String,
`time` DateTime64(9),
`text` String,
`dead` Int64,
`parent` Int64,
`poll` Int64,
`kids` Array(Int64),
`url` String,
`score` Int64,
`title` String,
`parts` Array(Int64),
`descendants` Int64
)
ORDER BY time;データを挿入します。
INSERT INTO hackernews
SELECT *
FROM url('https://datasets-documentation.s3.eu-west-3.amazonaws.com/hackernews/hacknernews.csv.gz', 'CSVWithNames')次に、splitByNonAlpha トークナイザーを使用して text カラムにテキストインデックスを追加します。
ALTER TABLE hackernews
ADD INDEX text_tokens_idx text
TYPE text(tokenizer='splitByNonAlpha')
GRANULARITY 1;そのインデックスをマテリアライズします。
ALTER TABLE hackernews
(MATERIALIZE INDEX text_tokens_idx)
SETTINGS mutations_sync = 1;この最適化は 26.4 で既に有効になっていますが、use_text_index_like_evaluation_by_dictionary_scan 設定を使って制御できます。次のクエリは、Kubernetes について言及している Hacker News の投稿数をカウントします。
SELECT count()
FROM hackernews
WHERE text LIKE '%Kubernetes%'
SETTINGS use_text_index_like_evaluation_by_dictionary_scan=0;┌─count()─┐
1. │ 20070 │
└─────────┘
1 row in set. Elapsed: 0.832 sec. Processed 18.25 million rows, 6.29 GB (21.93 million rows/s., 7.56 GB/s.)
Peak memory usage: 88.18 MiB.
1 row in set. Elapsed: 0.624 sec. Processed 18.25 million rows, 6.29 GB (29.23 million rows/s., 10.08 GB/s.)
Peak memory usage: 87.93 MiB.
1 row in set. Elapsed: 0.638 sec. Processed 18.25 million rows, 6.29 GB (28.60 million rows/s., 9.86 GB/s.)
Peak memory usage: 86.01 MiB.次に、最適化を使用して実行します。
SELECT count()
FROM hackernews
WHERE text LIKE '%Kubernetes%'
SETTINGS use_text_index_like_evaluation_by_dictionary_scan=1;┌─count()─┐
1. │ 20070 │
└─────────┘
1 row in set. Elapsed: 0.208 sec. Processed 18.25 million rows, 18.25 MB (87.53 million rows/s., 87.53 MB/s.)
Peak memory usage: 2.07 MiB.
1 row in set. Elapsed: 0.225 sec. Processed 18.25 million rows, 18.25 MB (80.98 million rows/s., 80.98 MB/s.)
Peak memory usage: 2.07 MiB.
1 row in set. Elapsed: 0.234 sec. Processed 18.25 million rows, 18.25 MB (77.83 million rows/s., 77.83 MB/s.)
Peak memory usage: 2.07 MiB.処理された行数は同じですが、最適化を使用したクエリの最短実行時間は 208 ミリ秒で、624 ミリ秒と比べて 3 倍強高速になっています。
クエリプランを比較すると、最適化を使用した方がスキャンするグラニュールが 1,000 個以上少ないことがわかります。
転置インデックスを使用しない場合:
┌─explain─────────────────────────────────────────────────────────┐
1. │ Output: count() │
2. │ │
3. │ Aggregating │
4. │ └──Filter ((WHERE + Change column names to column identifiers)) │
5. │ └──ReadFromMergeTree (default.hackernews) │
6. │ Indexes: │
7. │ PrimaryKey │
8. │ Condition: true │
9. │ Parts: 6/6 │
10. │ Granules: 3533/3533 │
11. │ Skip │
12. │ Name: text_tokens_idx │
13. │ Description: text GRANULARITY 100000000 │
14. │ Condition: (mode: All; tokens: []) │
15. │ Parts: 6/6 │
16. │ Granules: 3533/3533 │
17. │ Ranges: 6 │
└─────────────────────────────────────────────────────────────────┘転置インデックスを使用する場合:
┌─explain──────────────────────────────────────────────┐
1. │ Output: count() │
2. │ │
3. │ Aggregating │
4. │ └──Filter │
5. │ └──ReadFromMergeTree (default.hackernews) │
6. │ Indexes: │
7. │ PrimaryKey │
8. │ Condition: true │
9. │ Parts: 6/6 │
10. │ Granules: 3533/3533 │
11. │ Skip │
12. │ Name: text_tokens_idx │
13. │ Description: text GRANULARITY 100000000 │
14. │ Condition: (mode: All; tokens: []) │
15. │ Parts: 6/6 │
16. │ Granules: 2247/3533 │
17. │ Ranges: 190 │
└──────────────────────────────────────────────────────┘COUNT DISTINCT の高速化
コントリビューター: Jiebin Sun
コア数の多いマシンにおける uniqExact(COUNT(DISTINCT ...) で使用)に対して、いくつかの改善が行われました。
- マージフェーズ中に ClickHouse が冗長なスレッドを生成しなくなりました。uniqExact は 256 個のバケットを持つ 2 レベルのハッシュテーブルを使用しますが、これまでは状況に関係なく最大
max_threads個のスレッドを生成していたため、その多くは何の処理も行わずに即座に終了していました。 - N 個の中間ハッシュテーブル(集計スレッドごとに 1 個)をマージする際、スレッドプールが N 回初期化されていたため、合計
O(N × threads)回のスレッド生成と深刻なロック競合が発生していました。今回から、N 個のハッシュテーブルすべてが 1 回のパスでマージされるようになりました。各スレッドがすべてのハッシュテーブルにわたる 1 つのバケットを一度に処理するため、スレッドプールの初期化がO(N)からO(1)に削減されます。
ベンチマークの一部では、288 コアマシンで 3 〜 15 倍の高速化が確認されました。
ただし、これはコア数の多いマシン向けの最適化です。手元の Mac M2 Max(12 コア)で HackerNews データセットを使って試してみたところ、改善は見られませんでした。
EXPLAIN のさらなる美化
コントリビューター: Kirill Kopnev
EXPLAIN PLAN pretty=1 が式を人間が読みやすい形式で出力するようになり、トップレベルの出力カラムとステップごとの出力カラムを表示し、JOIN に推定行数と局所性(locality)のラベルを付けるようになりました。
以下のクエリでどのように動作するか見てみましょう。
EXPLAIN pretty = 1
SELECT by, count()
FROM hackernews
WHERE (text LIKE '%OpenAI%') AND (text LIKE '%Google%')
GROUP BY ALL
ORDER BY count() DESC, by
LIMIT 10;26.3
┌─explain────────────────────────────────────────────────────────────────────────────┐
1. │ Expression (Project names) │
2. │ └──Limit (preliminary LIMIT) │
3. │ └──Sorting (Sorting for ORDER BY) │
4. │ └──Expression ((Before ORDER BY + Projection)) │
5. │ └──Aggregating │
6. │ └──Expression (Before GROUP BY) │
7. │ └──Expression ((WHERE + Change column names to column identifiers)) │
8. │ └──ReadFromMergeTree (default.hackernews) │
└────────────────────────────────────────────────────────────────────────────────────┘26.4
┌─explain─────────────────────────────────────────────────────┐
1. │ Output: by, count() │
2. │ │
3. │ Expression (Project names) │
4. │ └──Limit (preliminary LIMIT) │
5. │ └──Sorting (Sorting for ORDER BY) │
6. │ └──Expression ((Before ORDER BY + Projection)) │
7. │ └──Aggregating │
8. │ └──Expression (Before GROUP BY) │
9. │ └──Expression │
10. │ └──ReadFromMergeTree (default.hackernews) │
└─────────────────────────────────────────────────────────────┘JSONAllValues + テキストインデックス
コントリビューター: Anton Popov
ClickHouse 26.4 では JSONAllValues が追加され、JSON カラムのすべてのリーフ値を Array(String) として返すようになりました。この上にテキストインデックスを作成することで、JSON サブカラムに対するより効率的なフィルタリングが可能になります。
StatsBomb データセットを使って、この仕組みを確認してみましょう。以下を実行して、データの一部を手元のマシンに取得します。
git clone --filter=blob:none --sparse https://github.com/statsbomb/open-data.git
cd open-data
git sparse-checkout set data/eventsclickhouse-local を使用して以下のテーブルを作成します。
CREATE TABLE events (
match_id UInt32,
json JSON(id String, index UInt32),
INDEX vals JSONAllValues(json) TYPE text(tokenizer = 'ngrams') GRANULARITY 1
)
ENGINE = MergeTree
ORDER BY (match_id, json.index);続いてデータを挿入します。
INSERT INTO events
SELECT
toUInt32(replaceRegexpOne(_file, '\\.json$', '')) AS match_id,
json
FROM file('open-data/data/events/*.json', JSONAsObject);12188949 rows in set. Elapsed: 1275.404 sec. Processed 12.19 million rows, 10.48 GB (9.56 thousand rows/s., 8.22 MB/s.)
Peak memory usage: 1.87 GiB.インデックスの仕組みを理解するために、JSONAllValues 関数が何を返すのか確認してみましょう。
SELECT JSONAllValues(json) FROM events LIMIT 1
FORMAT Vertical;JSONAllValues(json): ['[36.4,21.7]','1.013174','000000b5-8156-429d-9088-e62a6ac2ea0d','2529','[36.8,20]','60','2','4','From Throw In','10958','Chris Smalling','5','Left Center Back','123','39','Manchester United','[\'5fbbde9b-74ab-48e9-9873-ef956db384de\',\'fd43cc18-c37b-438a-8a40-a8bb50e59469\']','18','39','Manchester United','00:15:18.727','43','Carry']このデータセットのレコード数は 1,200 万件強にとどまり、インデックスの効果を確認するにはやや不十分なため、データを何回か複製します。
ALTER TABLE events
ATTACH PARTITION ID 'all'
FROM events;0 rows in set. Elapsed: 3.892 sec.
0 rows in set. Elapsed: 7.957 sec.
0 rows in set. Elapsed: 15.894 sec.
0 rows in set. Elapsed: 33.655 sec.
0 rows in set. Elapsed: 68.870 sec.これでレコード数が大幅に増えました。
SELECT count()
FROM events;┌───count()─┐
1. │ 390046368 │ -- 390.05 million
└───────────┘次のクエリは、リオネル・メッシに関連する行数を返します。
SELECT count()
FROM events
WHERE json.player.name = 'Lionel Andrés Messi Cuccittini'
SETTINGS use_skip_indexes = 1;use_skip_indexes = 0 を設定することでテキストインデックスを無効にできます。このクエリを実行すると、以下の結果が得られます。
┌─count()─┐
1. │ 4268960 │ -- 4.27 million
└─────────┘インデックスなしで 3 回実行してみます。
1 row in set. Elapsed: 1.505 sec. Processed 390.05 million rows, 13.87 GB (259.20 million rows/s., 9.22 GB/s.)
Peak memory usage: 48.23 MiB.
1 row in set. Elapsed: 1.666 sec. Processed 390.05 million rows, 13.87 GB (234.12 million rows/s., 8.32 GB/s.)
Peak memory usage: 48.23 MiB.
1 row in set. Elapsed: 1.668 sec. Processed 390.05 million rows, 13.87 GB (233.88 million rows/s., 8.32 GB/s.)
Peak memory usage: 48.23 MiB.次にインデックスありで 3 回実行します。
1 row in set. Elapsed: 1.139 sec. Processed 80.64 million rows, 3.23 GB (70.80 million rows/s., 2.84 GB/s.)
Peak memory usage: 69.25 MiB.
1 row in set. Elapsed: 1.096 sec. Processed 80.64 million rows, 3.23 GB (73.61 million rows/s., 2.95 GB/s.)
Peak memory usage: 68.93 MiB.
1 row in set. Elapsed: 1.087 sec. Processed 80.64 million rows, 3.23 GB (74.21 million rows/s., 2.97 GB/s.)
Peak memory usage: 74.13 MiB.処理された行数を見ると、インデックスによってスキャンするデータ量が 5 分の 1 近くに削減されていることがわかります。最短実行時間は、インデックスなしの 1,505 ミリ秒に対してインデックスありでは 1,087 ミリ秒となっており、約 50% の向上が見られます。



