ClickHouse 主キーインデックスのベストプラクティス

この記事では、ClickHouse のインデックス作成について詳しく説明します。これについて詳しく説明し、説明します。

  • ClickHouse のインデックスは従来のリレーショナル データベースとどう違うのですか
  • ClickHouse が主キー スパース インデックスを構築して使用する方法
  • ClickHouse インデックス作成のベスト プラクティス

この記事で説明されているすべての Clickhouse SQL ステートメントとクエリを自分のマシンで実行することを選択できます。ClickHouseのインストールと構築方法については、クイックスタートを参照してください。

ノート

この投稿では、スパース インデックスに焦点を当てます。

セカンダリ ホップ インデックスについて詳しく知りたい場合は、チュートリアルを参照してください

データセット

この記事では、匿名化された Web トラフィック データセットを使用します。

  • サンプル データセット内の 887 万行 (イベント) のサブセットを使用します。
  • 非圧縮データサイズは887万イベント、約700mbです。ClickHouse に保存すると 200mb 圧縮されます。
  • このサブセットでは、各行に、特定の時間 (EventTime 列) に URL (URL 列) をクリックしたインターネット ユーザー (UserID 列) を表す 3 つの列が含まれています。

これら 3 つの列を使用すると、次のようないくつかの典型的な Web 分析クエリをすでに定式化できます。

  • 特定のユーザーが最も多くクリックした上位 10 の URL は何ですか?
  • 特定の URL を最も多くクリックした上位 10 人のユーザーは誰ですか?
  • ユーザーが特定の URL をクリックする最も頻繁な時間帯 (曜日など) は何ですか?

テスト環境

このドキュメントに記載されているすべてのランタイム データは、Apple M1 Pro チップと 16 GB の RAM を搭載した MacBook Pro 上で ClickHouse 22.2.1 をローカルで実行したものです。

フルテーブルスキャン

主キーなしでデータセットに対してクエリを実行する方法を確認するために、次の SQL DDL ステートメントを実行してテーブルを作成しました (MergeTree テーブル エンジンを使用)。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">CREATE</span> <span style="color:#569cd6">TABLE</span> hits_NoPrimaryKey
</span><span style="color:#9cdcfe"><span style="color:#d4d4d4">(</span>
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">`</span>UserID<span style="color:#d4d4d4">`</span> UInt32<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">`</span>URL<span style="color:#d4d4d4">`</span> String<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">`</span>EventTime<span style="color:#d4d4d4">`</span> <span style="color:#569cd6">DateTime</span>
</span><span style="color:#9cdcfe"><span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ENGINE</span> <span style="color:#d4d4d4">=</span> MergeTree
</span><span style="color:#9cdcfe"><span style="color:#569cd6">PRIMARY</span> <span style="color:#569cd6">KEY</span> tuple<span style="color:#d4d4d4">(</span><span style="color:#d4d4d4">)</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

次に、次の挿入 SQL を使用して、ヒット データセットのサブセットをテーブルに挿入します。この SQL は、 URL テーブル関数型推論を使用して、clickhouse.com からデータセットの一部を読み込みます。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">INSERT</span> <span style="color:#569cd6">INTO</span> hits_NoPrimaryKey <span style="color:#569cd6">SELECT</span>
</span><span style="color:#9cdcfe">   intHash32<span style="color:#d4d4d4">(</span>c11::UInt64<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> UserID<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">   c15 <span style="color:#569cd6">AS</span> URL<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">   c5 <span style="color:#569cd6">AS</span> EventTime
</span><span style="color:#9cdcfe"><span style="color:#569cd6">FROM</span> url<span style="color:#d4d4d4">(</span><span style="color:#ce9178">'https://datasets.clickhouse.com/hits/tsv/hits_v1.tsv.xz'</span><span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">WHERE</span> URL <span style="color:#d4d4d4">!=</span> <span style="color:#ce9178">''</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">Ok.
</span>
<span style="color:#9cdcfe">0 rows in set. Elapsed: 145.993 sec. Processed 8.87 million rows, 18.40 GB (60.78 thousand rows/s., 126.06 MB/s.)
</span></code></span></span></span>

ClickHouse クライアントは実行結果を出力し、887 万行のデータを挿入します。

最後に、この記事で後述する説明を簡素化し、グラフと結果を再現可能にするために、FINAL キーワードを使用してテーブルを最適化します。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">OPTIMIZE</span> <span style="color:#569cd6">TABLE</span> hits_NoPrimaryKey FINAL<span style="color:#d4d4d4">;</span>
</span></code></span></span></span>
ノート

一般に、データをロードした直後に最適化を実行する必要はなく、推奨されません。この例では、これが必要な理由は明らかです。

ここで、最初の Web 分析クエリを実行します。ユーザー ID 749927693 のインターネット ユーザーによって最もクリックされた URL のトップ 10 は次のとおりです。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">SELECT</span> URL<span style="color:#d4d4d4">,</span> <span style="color:#dcdcaa">count</span><span style="color:#d4d4d4">(</span>URL<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">as</span> Count
</span><span style="color:#9cdcfe"><span style="color:#569cd6">FROM</span> hits_NoPrimaryKey
</span><span style="color:#9cdcfe"><span style="color:#569cd6">WHERE</span> UserID <span style="color:#d4d4d4">=</span> <span style="color:#b5cea8">749927693</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">GROUP</span> <span style="color:#569cd6">BY</span> URL
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> Count <span style="color:#569cd6">DESC</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">LIMIT</span> <span style="color:#b5cea8">10</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">┌─URL────────────────────────────┬─Count─┐
</span><span style="color:#9cdcfe">│ http://auto.ru/chatay-barana.. │   170 │
</span><span style="color:#9cdcfe">│ http://auto.ru/chatay-id=371...│    52 │
</span><span style="color:#9cdcfe">│ http://public_search           │    45 │
</span><span style="color:#9cdcfe">│ http://kovrik-medvedevushku-...│    36 │
</span><span style="color:#9cdcfe">│ http://forumal                 │    33 │
</span><span style="color:#9cdcfe">│ http://korablitz.ru/L_1OFFER...│    14 │
</span><span style="color:#9cdcfe">│ http://auto.ru/chatay-id=371...│    14 │
</span><span style="color:#9cdcfe">│ http://auto.ru/chatay-john-D...│    13 │
</span><span style="color:#9cdcfe">│ http://auto.ru/chatay-john-D...│    10 │
</span><span style="color:#9cdcfe">│ http://wot/html?page/23600_m...│     9 │
</span><span style="color:#9cdcfe">└────────────────────────────────┴───────┘
</span>
<span style="color:#9cdcfe">10 rows in set. Elapsed: 0.022 sec.
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">Processed 8.87 million rows,
</span></span><span style="color:#9cdcfe">70.45 MB (398.53 million rows/s., 3.17 GB/s.)
</span></code></span></span></span>

ClickHouse クライアントの出力には、ClickHouse がテーブル全体のスキャンを実行したことが示されています。テーブルの 887 万行はそれぞれ ClickHouse にロードされますが、これはスケーラブルではありません。

これをより効率的かつ高速にするには、適切な主キーを持つテーブルを使用する必要があります。これにより、ClickHouse は (主キーの列に基づいて) スパース主インデックスを自動的に作成できるようになり、これを使用してサンプル クエリの実行を大幅に高速化できます。

主キーを持つテーブル

結合主キーの UserID 列と URL 列を含むテーブルを作成します。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">CREATE</span> <span style="color:#569cd6">TABLE</span> hits_UserID_URL
</span><span style="color:#9cdcfe"><span style="color:#d4d4d4">(</span>
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">`</span>UserID<span style="color:#d4d4d4">`</span> UInt32<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">`</span>URL<span style="color:#d4d4d4">`</span> String<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">`</span>EventTime<span style="color:#d4d4d4">`</span> <span style="color:#569cd6">DateTime</span>
</span><span style="color:#9cdcfe"><span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ENGINE</span> <span style="color:#d4d4d4">=</span> MergeTree
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe"><span style="color:#569cd6">PRIMARY</span> <span style="color:#569cd6">KEY</span> <span style="color:#d4d4d4">(</span>UserID<span style="color:#d4d4d4">,</span> URL<span style="color:#d4d4d4">)</span>
</span></span><span style="color:#9cdcfe"><span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> <span style="color:#d4d4d4">(</span>UserID<span style="color:#d4d4d4">,</span> URL<span style="color:#d4d4d4">,</span> EventTime<span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe">SETTINGS index_granularity <span style="color:#d4d4d4">=</span> <span style="color:#b5cea8">8192</span><span style="color:#d4d4d4">,</span> index_granularity_bytes <span style="color:#d4d4d4">=</span> <span style="color:#b5cea8">0</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

DDLの詳細

上記の DDL ステートメントの主キーは、指定された 2 つのキー列に基づいて主インデックスを作成します。


データを挿入します:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">INSERT</span> <span style="color:#569cd6">INTO</span> hits_UserID_URL <span style="color:#569cd6">SELECT</span>
</span><span style="color:#9cdcfe">   intHash32<span style="color:#d4d4d4">(</span>c11::UInt64<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> UserID<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">   c15 <span style="color:#569cd6">AS</span> URL<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">   c5 <span style="color:#569cd6">AS</span> EventTime
</span><span style="color:#9cdcfe"><span style="color:#569cd6">FROM</span> url<span style="color:#d4d4d4">(</span><span style="color:#ce9178">'https://datasets.clickhouse.com/hits/tsv/hits_v1.tsv.xz'</span><span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">WHERE</span> URL <span style="color:#d4d4d4">!=</span> <span style="color:#ce9178">''</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">0 rows in set. Elapsed: 149.432 sec. Processed 8.87 million rows, 18.40 GB (59.38 thousand rows/s., 123.16 MB/s.)
</span></code></span></span></span>


optimize表:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">OPTIMIZE</span> <span style="color:#569cd6">TABLE</span> hits_UserID_URL FINAL<span style="color:#d4d4d4">;</span>
</span></code></span></span></span>


次のクエリを使用して、テーブルに関するメタデータを取得できます。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">SELECT</span>
</span><span style="color:#9cdcfe">    part_type<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    path<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    formatReadableQuantity<span style="color:#d4d4d4">(</span><span style="color:#569cd6">rows</span><span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> <span style="color:#569cd6">rows</span><span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    formatReadableSize<span style="color:#d4d4d4">(</span>data_uncompressed_bytes<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> data_uncompressed_bytes<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    formatReadableSize<span style="color:#d4d4d4">(</span>data_compressed_bytes<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> data_compressed_bytes<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    formatReadableSize<span style="color:#d4d4d4">(</span>primary_key_bytes_in_memory<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> primary_key_bytes_in_memory<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    marks<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    formatReadableSize<span style="color:#d4d4d4">(</span>bytes_on_disk<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> bytes_on_disk
</span><span style="color:#9cdcfe"><span style="color:#569cd6">FROM</span> system<span style="color:#d4d4d4">.</span>parts
</span><span style="color:#9cdcfe"><span style="color:#569cd6">WHERE</span> <span style="color:#d4d4d4">(</span><span style="color:#569cd6">table</span> <span style="color:#d4d4d4">=</span> <span style="color:#ce9178">'hits_UserID_URL'</span><span style="color:#d4d4d4">)</span> <span style="color:#d4d4d4">AND</span> <span style="color:#d4d4d4">(</span>active <span style="color:#d4d4d4">=</span> <span style="color:#b5cea8">1</span><span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe">FORMAT Vertical<span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">part_type:                   Wide
</span><span style="color:#9cdcfe">path:                        ./store/d9f/d9f36a1a-d2e6-46d4-8fb5-ffe9ad0d5aed/all_1_9_2/
</span><span style="color:#9cdcfe">rows:                        8.87 million
</span><span style="color:#9cdcfe">data_uncompressed_bytes:     733.28 MiB
</span><span style="color:#9cdcfe">data_compressed_bytes:       206.94 MiB
</span><span style="color:#9cdcfe">primary_key_bytes_in_memory: 96.93 KiB
</span><span style="color:#9cdcfe">marks:                       1083
</span><span style="color:#9cdcfe">bytes_on_disk:               207.07 MiB
</span>

<span style="color:#9cdcfe">1 rows in set. Elapsed: 0.003 sec.
</span></code></span></span></span>

クライアントの出力には次のものが表示されます。

  • テーブル データは特定のディレクトリにワイド形式で保存され、各列にはデータ ファイルとマーク ファイルがあります。
  • テーブルには 887 万行のデータがあります。
  • 非圧縮データは 733.28 MB です。
  • 圧縮データは 206.94 MB です。
  • 主キー インデックス エントリは 1083 個あり、サイズは 96.93 KB です。
  • ディスク上では、テーブルのデータ、タグ ファイル、メイン インデックス ファイルが合計 207.07 MB を占めます。

大規模なデータ規模に対応したインデックス設計

従来のリレーショナル データベース管理システムでは、テーブルの各行にプライマリ インデックスが含まれています。私たちのデータセットの場合、メイン インデックス (通常はB(+) ツリーデータ構造) に 887 万のエントリが含まれることになります。

このようなインデックスを使用すると、特定の行をすばやく見つけることができるため、検索と更新の効率が向上します。B(+) ツリー データ構造内のエントリを検索する平均時間複雑さは O(log2n) です。887 万行のテーブルの場合、インデックス エントリを見つけるには 23 の手順が必要であることを意味します。

この機能には代償が伴います。ディスクとメモリのオーバーヘッドが追加され、新しい行がテーブルに追加され、エントリがインデックスに追加されるときの挿入コストが高くなります (B ツリーの再バランスが必要になる場合もあります)。

ClickHouse のテーブル エンジンは、B-Tee インデックスに関連する課題を考慮して、異なるアプローチを使用します。ClickHouse MergeTree Engineファミリのエンジンは、大量のデータを処理できるように設計および最適化されています。

これらのテーブルは、1 秒あたり数百万の行挿入を受け取り、非常に大容量 (100 ペタバイト) のデータを保存するように設計されています。

データはバッチごとにテーブルに迅速に書き込まれ、マージ ルールがバックグラウンドで適用されます。

ClickHouse では、各データ部分 (データ部分) に独自のメイン インデックスがあります。マージすると、マージされた部分のマスターインデックスもマージされます。

大規模な場合、ディスクとメモリの効率が非常に重要です。したがって、行ごとにインデックスを作成する代わりに、データ行のグループ (グラニュールと呼ばれます) に対してインデックス エントリが構築されます。

ClickHouse が主キー列の順序で一連の行をディスクに保存するため、このスパース インデックスが可能になります。

個々の行を直接検索する (B ツリー ベースのインデックスなど) とは異なり、スパース プライマリ インデックスを使用すると、クエリに一致する可能性が高い行のグループを (インデックス エントリに対して二分検索を実行することで) 迅速に特定できます。

次に、一致する行を見つけるために、一致する可能性のある行のグループ (顆粒) が ClickHouse エンジンに並行してロードされます。

このインデックス設計により、メイン インデックスを小さくすることができます (メイン メモリに完全に収まる可能性があり、またそうする必要があります) と同時に、特にデータ分析のユースケースで一般的な範囲クエリの場合に、クエリの実行時間が大幅に高速化されます。

以下に、ClickHouse がスパース マスター インデックスを構築して使用する方法を詳しく説明します。この記事の後半では、インデックスの作成に使用されるテーブル列 (主キー列) を選択、削除、並べ替える方法に関するいくつかのベスト プラクティスについて説明します。

データは主キーによってソートされてディスク

上記で作成されたテーブルは次のとおりです。

ノート
  • ソートキーのみを指定した場合、主キーが暗黙的にソートキーとして定義されます。

  • メモリ効率を向上させるために、クエリによってフィルターされた列のみを含む主キーを明示的に指定します。主キーに基づく主インデックスは、メイン メモリに完全にロードされます。

  • コンテキストの一貫性と最大圧縮率を確保するために、現在のテーブルのすべての列を含むソート キーを個別に定義します (圧縮アルゴリズムに関連しており、通常はソート後の方が圧縮率が高くなります)。

  • 主キーとソートキーの両方を指定する場合、主キーはソートキーのプレフィックスである必要があります。

挿入された行は、主キー列 (およびソート キーの追加の EventTime 列) ごとに辞書編集順 (最小値から最大値) でディスクに保存されます。

ノート

ClickHouse では、同じ主キー列を持つ複数行のデータを挿入できます。この場合 (下図の行 1 と行 2 を参照)、最終的な順序は指定されたソート キー (ここでは EventTime 列の値) によって決まります。

以下の図に示すように: ClickHouse はカラム ストレージ データベースです。

  • ディスク上には、各テーブルにデータ ファイル (*.bin) があり、その列のすべての値が圧縮形式で保存されます。
  • この例では、887 万行が主キー列 (および追加のソート キー列) の辞書編集上の昇順でディスクに保存されます。
    • まずユーザーID、
    • そしてURL、
    • そして最後に EventTime です。

UserID.bin、URL.bin、および EventTime.bin は、 UserID URL 、およびEventTime列を持つデータ ファイルです。
 

ノート
  • 主キーはディスク上の行の辞書編集上の順序を定義するため、テーブルには主キーを 1 つだけ持つことができます。

  • メッセージのログ記録にも使用される ClickHouse の内部行番号付けスキームに合わせて、0 から始まる行番号を付けます。

データは並列データ処理

データ処理の目的で、テーブルの列値は論理的に複数の粒度に分割されます。グラニュールは、データ処理のために ClickHouse に流入する最小の分割できないデータセットです。つまり、ClickHouse は、個々の行を読み取るのではなく、常に行のグループ (グラニュル) 全体を (ストリームで並行して) 読み取ります。

ノート

列の値はグラニュールに物理的に保存されるのではなく、グラニュールはクエリ処理のために列の値を論理的に編成したものにすぎません。

以下の図は、テーブル内の 887 万行 (列値) が 1083 のグラニュルにどのように編成されるかを示しています。これは、index_granularity の設定 (デフォルト値 8192 に設定) を含むテーブルの DDL ステートメントの結果です。

最初の (ディスク上の物理的な順序に従って) 8192 行 (その列値) は論理的にグラニュール 0 に属し、次の 8192 行 (その列値) はグラニュール 1 に属し、以下同様になります。

ノート
  • 最後の顆粒 (1082 顆粒) は 8192 行未満です。

  • このガイドの冒頭の「DDL ステートメントの詳細」で、(このガイドでの説明を簡略化し、図と結果を再現可能にするために) 適応インデックスの粒度を無効にしたことを説明しました。

    したがって、例のテーブル内のすべてのグレイン (最後のグレインを除く) は同じサイズになります。

  • 適応型インデックス粒度 (インデックス粒度はデフォルトで適応型) のテーブルの場合、行データ サイズに応じて、一部の粒度のサイズが 8192 行より小さくなることがあります。

  • 主キー列の一部の列値 (UserID、URL) をオレンジ色でマークしました。

    これらのオレンジ色でマークされた列の値は、各グラニュールの各主キー列の最小値です。ここでの例外は最後のパーティクル (上の画像の粒子 1082) であり、最大の値をマークしました。

    以下で説明するように、これらのオレンジ色のマークが付いている列の値は、テーブルのプライマリ インデックスのエントリになります。

  • メッセージのログ記録にも使用される ClickHouse の内部行番号付けスキームに合わせて、0 から始まる行番号を付けます。

各グラニュールはメインインデックスのエントリ

メインインデックスは上記画像の木目を元に作成しております。このインデックスは、0 から始まるいわゆる数値インデックス マーカーを含む非圧縮フラット配列ファイル (primary.idx) です。

次の図は、インデックスが各グラニュールの主キー列の最小値 (上の図でオレンジ色でマークされた値) を格納していることを示しています。例えば:

  • 最初のインデックス エントリ (下図の「マーク 0」) には、上図のパーティクル 0 の主キー列の最小値が格納されます。
  • 2 番目のインデックス エントリ (下図の「マーク 1」) には、上図のパーティクル 1 の主キー列の最小値が格納されます。

このテーブルでは、インデックスには合計 1,083 のエントリ、887 万行、1,083 グラニュルがあります。

ノート
  • 最後のインデックス エントリ (上図の「マーク 1082」) には、上図のパーティクル 1082 の主キー列の最大値が格納されます。

  • インデックス エントリ (インデックス タグ) は、テーブル内の特定の行ではなく、顆粒に基づいています。たとえば、上の画像のインデックス エントリ「マーク 0」の場合、テーブルには UserID 240.923 および URL「goal://metry=10000467796a411...」の行はありません。代わりに、このテーブルには粒度 0 があります。 , この顆粒では、UserID の最小値は 240.923、URL の最小値は「goal://metry=10000467796a411...」であり、これら 2 つの値は異なる行からのものです。

  • メイン インデックス ファイルはメイン メモリに完全にロードされます。ファイルが利用可能な空きメモリ容量より大きい場合、ClickHouse はエラーになります。

各インデックス エントリがデータの特定範囲の始まりをマークするため、主キー エントリはインデックス マーカーと呼ばれます。たとえば表:

  • UserID インデックス マーク:
    メイン インデックスに格納されている UserID 値は昇順に並べ替えられます。
    上の図の「マーク 1」は、グラニュール 1 および後続のすべてのグラニュール内のすべてのテーブル行の UserID 値が 4.073.710 以上であることが保証されていることを示します。

    後で説明するように、クエリが主キーの最初の列でフィルタリングされる場合、このグローバル順序付けにより、ClickHouse は最初のキー列のインデックス トークンで二分検索アルゴリズムを使用できるようになります。

  • URL インデックス マーク:
    主キー列の UserID と URL は同じカーディナリティを持ちます。つまり、最初の列以降のすべての主キー列のインデックス マークは、通常、各グラニュールのデータ範囲のみを表します。
    たとえば、「マーク 0」の URL 列のすべての値は、goal://metry=10000467796a411... 以上ですが、顆粒 1 の URL は当てはまりません。「マーク 1」と「マーク 0」には異なる UserID 列の値があります。

    これがクエリ実行パフォーマンスに与える影響については、後ほど詳しく説明します。

メインインデックスはパーティクルの

これで、プライマリ インデックスに基づいたクエリを実行できるようになりました。

次の例では、ユーザー ID 749927693 ごとにクリック数が最も多かった 10 件の URL を計算します。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">SELECT</span> URL<span style="color:#d4d4d4">,</span> <span style="color:#dcdcaa">count</span><span style="color:#d4d4d4">(</span>URL<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> Count
</span><span style="color:#9cdcfe"><span style="color:#569cd6">FROM</span> hits_UserID_URL
</span><span style="color:#9cdcfe"><span style="color:#569cd6">WHERE</span> UserID <span style="color:#d4d4d4">=</span> <span style="color:#b5cea8">749927693</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">GROUP</span> <span style="color:#569cd6">BY</span> URL
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> Count <span style="color:#569cd6">DESC</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">LIMIT</span> <span style="color:#b5cea8">10</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">┌─URL────────────────────────────┬─Count─┐
</span><span style="color:#9cdcfe">│ http://auto.ru/chatay-barana.. │   170 │
</span><span style="color:#9cdcfe">│ http://auto.ru/chatay-id=371...│    52 │
</span><span style="color:#9cdcfe">│ http://public_search           │    45 │
</span><span style="color:#9cdcfe">│ http://kovrik-medvedevushku-...│    36 │
</span><span style="color:#9cdcfe">│ http://forumal                 │    33 │
</span><span style="color:#9cdcfe">│ http://korablitz.ru/L_1OFFER...│    14 │
</span><span style="color:#9cdcfe">│ http://auto.ru/chatay-id=371...│    14 │
</span><span style="color:#9cdcfe">│ http://auto.ru/chatay-john-D...│    13 │
</span><span style="color:#9cdcfe">│ http://auto.ru/chatay-john-D...│    10 │
</span><span style="color:#9cdcfe">│ http://wot/html?page/23600_m...│     9 │
</span><span style="color:#9cdcfe">└────────────────────────────────┴───────┘
</span>
<span style="color:#9cdcfe">10 rows in set. Elapsed: 0.005 sec.
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">Processed 8.19 thousand rows,
</span></span><span style="color:#9cdcfe">740.18 KB (1.53 million rows/s., 138.59 MB/s.)
</span></code></span></span></span>

ClickHouse クライアントからの出力は、完全なテーブル スキャンが実行されず、819,000 行のみが ClickHouse に流れたことを示しています。

トレース ログがオンになっている場合、ClickHouse サーバー ログには、ClickHouse が1083 UserID インデックス トークンに対してバイナリ検索を実行して、UserID 列値が 749927693 の行を含む可能性のあるグラニュールを特定していることが示されます。これには 19 のステップが必要で、平均時間計算量は O(log2 n) です。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">...Executor): Key condition: (column 0 in [749927693, 749927693])
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">...Executor): Running binary search on index range for part all_1_9_2 (1083 marks)
</span></span><span style="color:#9cdcfe">...Executor): Found (LEFT) boundary mark: 176
</span><span style="color:#9cdcfe">...Executor): Found (RIGHT) boundary mark: 177
</span><span style="color:#9cdcfe">...Executor): Found continuous range in 19 steps
</span><span style="color:#9cdcfe">...Executor): Selected 1/1 parts by partition key, 1 parts by primary key,
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">              1/1083 marks by primary key, 1 marks to read from 1 ranges
</span></span><span style="color:#9cdcfe">...Reading ...approx. 8192 rows starting from 1441792
</span></code></span></span></span>

上記のトレース ログでは、1083 個の既存のトークンのうち 1 個がクエリを満たしていることがわかります。

トレースログの詳細

EXPLAINを使用してこの結果を再現することもできます。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">EXPLAIN</span> indexes <span style="color:#d4d4d4">=</span> <span style="color:#b5cea8">1</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">SELECT</span> URL<span style="color:#d4d4d4">,</span> <span style="color:#dcdcaa">count</span><span style="color:#d4d4d4">(</span>URL<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> Count
</span><span style="color:#9cdcfe"><span style="color:#569cd6">FROM</span> hits_UserID_URL
</span><span style="color:#9cdcfe"><span style="color:#569cd6">WHERE</span> UserID <span style="color:#d4d4d4">=</span> <span style="color:#b5cea8">749927693</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">GROUP</span> <span style="color:#569cd6">BY</span> URL
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> Count <span style="color:#569cd6">DESC</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">LIMIT</span> <span style="color:#b5cea8">10</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果は次のとおりです。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">┌─explain───────────────────────────────────────────────────────────────────────────────┐
</span><span style="color:#9cdcfe">│ Expression (Projection)                                                               │
</span><span style="color:#9cdcfe">│   Limit (preliminary LIMIT (without OFFSET))                                          │
</span><span style="color:#9cdcfe">│     Sorting (Sorting for ORDER BY)                                                    │
</span><span style="color:#9cdcfe">│       Expression (Before ORDER BY)                                                    │
</span><span style="color:#9cdcfe">│         Aggregating                                                                   │
</span><span style="color:#9cdcfe">│           Expression (Before GROUP BY)                                                │
</span><span style="color:#9cdcfe">│             Filter (WHERE)                                                            │
</span><span style="color:#9cdcfe">│               SettingQuotaAndLimits (Set limits and quota after reading from storage) │
</span><span style="color:#9cdcfe">│                 ReadFromMergeTree                                                     │
</span><span style="color:#9cdcfe">│                 Indexes:                                                              │
</span><span style="color:#9cdcfe">│                   PrimaryKey                                                          │
</span><span style="color:#9cdcfe">│                     Keys:                                                             │
</span><span style="color:#9cdcfe">│                       UserID                                                          │
</span><span style="color:#9cdcfe">│                     Condition: (UserID in [749927693, 749927693])                     │
</span><span style="color:#9cdcfe">│                     Parts: 1/1                                                        │
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">│                     Granules: 1/1083                                                  │
</span></span><span style="color:#9cdcfe">└───────────────────────────────────────────────────────────────────────────────────────┘
</span>
<span style="color:#9cdcfe">16 rows in set. Elapsed: 0.003 sec.
</span></code></span></span></span>

クライアント出力は、UserID 列値が 749927693 の行を含む可能性がある 1083 グラニュルの 1 つが選択されたことを示しています。

結論

クエリがユニオン主キーの一部である最初の主キーでフィルタリングすると、ClickHouse は主キー インデックス トークンに対してバイナリ検索アルゴリズムを実行します。

上で説明したように、ClickHouse はスパース プライマリ インデックスを使用して、クエリに一致する行を含む可能性が高いグラニュールを (バイナリ検索アルゴリズムを介して) 迅速に選択します。

これは、 ClickHouse クエリ実行の最初の段階 (詳細な選択)です。

第 2 フェーズ (データ読み取り)では、ClickHouse は選択されたグラニュールを見つけて、そのすべての行を ClickHouse エンジンにストリーミングし、クエリに実際に一致する行を見つけます。

第 2 段階については、次のセクションで詳しく説明します。

マーカー ファイルはパーティクルの

以下の図は、上の表のメインのインデックス ファイルの一部を示しています。

176 番目のトークンは、上記のようにインデックス付けされた 1083 個の UserID トークンのバイナリ検索を実行することで特定されました。したがって、対応するグラニュール 176 には、UserID 列値が 749.927.693 である行が含まれる可能性があります。

粒子選択の具体的なプロセス

グラニュール 176 の一部の行に UserID 列値 749.927.693 が含まれていることを確認 (または除外) するには、このグラニュールに属する 8192 行すべてを ClickHouse に読み取る必要があります。

データのこの部分を読み取るために、ClickHouse は粒子 176 の物理アドレスを知る必要があります。

ClickHouse では、テーブルのすべてのグレインの物理的な位置がタグ ファイルに保存されます。データ ファイルと同様に、テーブル列ごとにタグ ファイルがあります。

以下の図は、UserID.mrk、URL.mrk、EventTime.mrk という 3 つのマーカー ファイルを示しています。これらのファイルには、テーブルの UserID、URL、および EventTime 列のグラニュルの物理的な場所が保存されています。

プライマリ インデックスは、0 から始まる番号が付けられたインデックス トークンを含むフラットな非圧縮配列ファイル (primary.idx) であることはすでに説明しました。

同様に、マーカー ファイルも、0 から始まる番号が付けられたマーカーを含むフラットな非圧縮配列ファイル (*.mrk) です。

ClickHouse が、クエリで必要な一致する行を含む可能性が高いグラニュールのインデックス マーカーを特定して選択すると、マーカー ファイル配列内でインデックス マーカーを検索して、グラニュールの物理的な場所を取得できます。

特定の列の各タグ ファイル エントリには、次の 2 つの場所がオフセットとして保存されます。

  • 最初のオフセット (上の画像の「block_offset」) は、選択したグラニュールの圧縮バージョンを含む圧縮列形式データ ファイル内のブロックを見つけます。この圧縮ブロックには、複数の圧縮顆粒が含まれる場合があります。見つかった圧縮ファイル ブロックは、読み取り時にメモリに解凍されます。

  • マーカー ファイルの 2 番目のオフセット (上の画像の「granule_offset」) は、解凍されたデータ ブロック内のグラニュールの位置を提供します。

特定されたパーティクル内の 8192 行のデータはすべて、さらなる処理のために ClickHouse によってロードされます。

MARK ファイルが必要な理由

メイン インデックスには、インデックス マークに対応するパーティクルの物理的位置が直接含まれていないのはなぜですか?

ClickHouse によって設計されたシナリオは非常に大規模なデータであるため、ディスクとメモリを効率的に使用することが非常に重要です。

メインのインデックス ファイルはメモリに配置する必要があります。

このクエリの例では、ClickHouse はプライマリ インデックスを使用し、クエリに一致する行を含む可能性が高い個々のグラニュルを選択しました。この 1 つのパーティクルについてのみ、対応する行グループを読み取ってさらに処理できるように、ClickHouse は物理的な位置を特定する必要があります。

また、このオフセット情報が必要なのは、UserID 列と URL 列のみです。

EventTime など、クエリで使用されない列の場合、オフセット情報は必要ありません。

このクエリ例の場合、Clickhouse は、UserID データ ファイル (UserID.bin) 内の 176 個のパーティクルに対して 2 つの物理位置オフセットと、URL データ ファイル (URL.data) 内の 176 個のパーティクルに対して 2 つの物理位置オフセットのみを必要とします。

マーク ファイルによって提供される間接化により、3 つの列すべての 1083 顆粒すべての物理的な位置についてメイン インデックスにエントリが直接格納されることが回避されるため、メイン メモリ内の不要な (未使用の可能性がある) データが回避されます。

以下の図とテキストは、クエリの例を示しています。ClickHouse が UserID.bin データ ファイル内で 176 個のパーティクルを見つける方法を示しています。

この記事の前半で説明したように、ClickHouse はプライマリ インデックス マーカー 176 を選択したため、グラニュール 176 にはクエリで必要な一致する行が含まれる可能性があります。

ClickHouse は、インデックスから選択されたマーカー番号 (176) を使用して UserID.mark 内の位置配列ルックアップを実行し、粒子 176 の位置を特定するための 2 つのオフセットを取得します。

図示されるように、第1のオフセットは、パーティクル176の圧縮データを含むUserID.binデータファイル内の圧縮ファイルブロックの位置を特定する。

特定されたファイルブロックがメインメモリに解凍されると、マーカーファイルの第2のオフセットを使用して、非圧縮データ内のグレイン176を特定することができる。

ClickHouse は、クエリ例 (UserID 749.927.693 のインターネット ユーザーが最もクリックした URL の上位 10 位) を実行するために、UserID.bin データ ファイルと URL.bin データ ファイルからグラニュール 176 を見つける (読み取る) 必要があります。

上の画像は、ClickHouse が UserID.bin データ ファイルの顆粒を見つける方法を示しています。

同時に、ClickHouse は、URL.bin データ ファイルのパーティクル 176 に対して同じ操作を実行します。これら 2 つの異なる粒度は調整され、さらなる処理のために ClickHouse エンジンにロードされます。つまり、UserID 749.927.693 を持つすべての行の URL 値の各グループを集計して計算し、最後に最大 10 個の URL グループをカウントの降順で出力します。

2 番目の主キーを使用してパフォーマンスの問題を

複合キーの一部であり、最初の主キー列である列でクエリがフィルター処理されると、ClickHouse は主キー列のインデックス トークンに対して二分検索を実行します。

しかし、クエリが最初のキー列ではなく、ユニオン主キーの一部でフィルタリングするとどうなるでしょうか?

ノート

最初の主キー列で明示的にフィルタリングする代わりに、最初の主キー列以降の任意のキー列でクエリをフィルタリングするシナリオについて説明しました。

クエリが最初の主キー列と最初の主キー列の後のキー列の両方でフィルタリングする場合、ClickHouse は最初の主キー列のインデックス トークンに対してバイナリ検索を実行します。



クエリを使用して、「http://public_search」に最も多くヒットした上位 10 人のユーザーを計算します。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">SELECT</span> UserID<span style="color:#d4d4d4">,</span> <span style="color:#dcdcaa">count</span><span style="color:#d4d4d4">(</span>UserID<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> Count
</span><span style="color:#9cdcfe"><span style="color:#569cd6">FROM</span> hits_UserID_URL
</span><span style="color:#9cdcfe"><span style="color:#569cd6">WHERE</span> URL <span style="color:#d4d4d4">=</span> <span style="color:#ce9178">'http://public_search'</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">GROUP</span> <span style="color:#569cd6">BY</span> UserID
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> Count <span style="color:#569cd6">DESC</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">LIMIT</span> <span style="color:#b5cea8">10</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

消す:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">┌─────UserID─┬─Count─┐
</span><span style="color:#9cdcfe">│ 2459550954 │  3741 │
</span><span style="color:#9cdcfe">│ 1084649151 │  2484 │
</span><span style="color:#9cdcfe">│  723361875 │   729 │
</span><span style="color:#9cdcfe">│ 3087145896 │   695 │
</span><span style="color:#9cdcfe">│ 2754931092 │   672 │
</span><span style="color:#9cdcfe">│ 1509037307 │   582 │
</span><span style="color:#9cdcfe">│ 3085460200 │   573 │
</span><span style="color:#9cdcfe">│ 2454360090 │   556 │
</span><span style="color:#9cdcfe">│ 3884990840 │   539 │
</span><span style="color:#9cdcfe">│  765730816 │   536 │
</span><span style="color:#9cdcfe">└────────────┴───────┘
</span>
<span style="color:#9cdcfe">10 rows in set. Elapsed: 0.086 sec.
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">Processed 8.81 million rows,
</span></span><span style="color:#9cdcfe">799.69 MB (102.11 million rows/s., 9.27 GB/s.)
</span></code></span></span></span>

クライアント出力は、URL 列が複合主キーの一部であるにもかかわらず、ClickHouse がほぼ完全なテーブル スキャンを実行したことを示しています。ClickHouse は、テーブルの 887 万行から 881 万行を読み取りました。

トレース ログが有効になっている場合、ClickHouse サービス ログ ファイルには、URL 列値「http://public_search」を含む可能性のある行を識別するために、ClickHouse が 1083 URL インデックス トークンに対する汎用除外検索を使用したことが示されます。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">...Executor): Key condition: (column 1 in ['http://public_search',
</span><span style="color:#9cdcfe">                                           'http://public_search'])
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">...Executor): Used generic exclusion search over index for part all_1_9_2
</span></span><span style="color:#9cdcfe">              with 1537 steps
</span><span style="color:#9cdcfe">...Executor): Selected 1/1 parts by partition key, 1 parts by primary key,
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">              1076/1083 marks by primary key, 1076 marks to read from 5 ranges
</span></span><span style="color:#9cdcfe">...Executor): Reading approx. 8814592 rows with 10 streams
</span></code></span></span></span>

上記のトレース ログの例では、一致する URL 値を持つ行が含まれる可能性が高いとして、1083 グラニュールのうち 1076 が (フラグ設定によって) 選択されたことがわかります。

これにより、実際に URL 値「http://public_search」を含む行を識別するために、881 万行が ClickHouse エンジンに読み取られます (10 ストリームを使用して並行して)。

ただし、その後、一致する行を含む粒子は 39 個だけでした。

結合主キー (UserID、URL) に基づく主インデックスは、特定の UserID 値で行をフィルタリングするクエリの高速化には役立ちますが、このインデックスは、特定の URL 値で行をフィルタリングするクエリにはあまり役に立ちません。

その理由は、URL 列が最初の主キー列ではないため、ClickHouse は (バイナリ検索ではなく) 一般的な除外検索アルゴリズムを使用して URL 列のインデックス マークを見つけます。UserID の主キー列とは異なり、その有効性はアルゴリズムは URL 列ベースに依存します。

説明のために、一般的な除外検索アルゴリズムがどのように機能するかを示します。

汎用除外検索アルゴリズム

以下は、最初の列の後の任意の列でグラニュールを選択するときに、前のキー列のカーディナリティが高いか低い場合に、 ClickHouse の一般除外検索アルゴリズムがどのように 動作するかを示しています。

これら 2 つのケースの例として、次のことを想定します。

  • URL 値「W3」を持つ行を検索します。
  • クリック テーブルの抽象化は、UserID と UserID の単純な値に単純化されます。
  • 同じ結合主キー (UserID、URL)。これは、最初に行が UserID 値によって並べ替えられ、同じ UserID 値を持つ行が次に URL によって並べ替えられることを意味します。
  • グレイン サイズは 2 です。つまり、各グレインには 2 つの列が含まれます。

以下のグラフでは、各グラニュールのキー列の最小値をオレンジ色でマークしています。

プレフィックス主キーのカーディナリティが低い

UserID のカーディナリティが低いと仮定します。この場合、同じ UserID 値が複数のテーブル行とテーブル、つまりインデックス マーク全体に分散している可能性があります。同じ UserID を持つインデックス タグの場合、インデックス タグの URL 値は昇順で並べ替えられます (テーブルの行は最初に UserID で並べ替えられ、次に URL で並べ替えられるため)。これにより、以下で説明するように効率的なフィルタリングが可能になります。

上の図には、抽象サンプル データの粒子選択プロセスの 3 つの異なるシナリオがあります。

  1. インデックス トークン 0 の (最小) URL 値が W3 未満で、直後のインデックス トークンの URL 値も W3 未満である場合、トークン 0、トークン 1、およびトークン 2 の値が同じであるため、インデックス トークン 0 は除外できます。ユーザーIDの値。この除外の前提条件により、グラニュール 0 と次のグラニュール 1 が完全に U1 UserID 値で構成されることが保証されるため、ClickHouse はグラニュール 0 の最大 URL 値も W3 未満であると想定して、そのグラニュールを除外できることに注意してください。

  2. インデックス タグ 1 の URL 値が W3 より小さく (または等しく)、後続のインデックス タグの URL 値が W3 より大きい (または等しい) 場合、粒度 1 に行が含まれる可能性があることを意味するため、インデックス タグ 1 が選択されます。 URL W3)。

  3. W3 より大きい URL 値を持つインデックス トークン 2 と 3 は除外できます。これは、メイン インデックスのインデックス トークンには各グラニュールの最小キー列値が格納されるため、グラニュール 2 と 3 に URL 値 W3 が含まれることは不可能であるためです。

プレフィックス主キーの高カーディナリティ

UserID のカーディナリティが高い場合、同じ UserID 値が複数のテーブルの行や顆粒に分散される可能性は低くなります。これは、インデックス タグの URL 値が単調増加ではないことを意味します。

上のグラフでわかるように、W3 より小さい URL 値を持つすべてのタグが、関連付けられたグレインの行を ClickHouse エンジンにロードするために選択されます。

これは、グラフ内のすべてのインデックス トークンが上記のシナリオ 1 に属しているにもかかわらず、前述の除外前提条件を満たしていないためです。つまり、直後のインデックス トークンが両方とも現在のトークンと同じ UserID 値を持つため、除外できません。

たとえば、インデックス トークン 0 の URL 値が W3 より小さく、その直後の後続インデックス トークンの URL 値も W3 より小さいとします。すぐに続く 2 つのインデックス タグ 1 と 2 が現在のタグ 0 と同じ UserID 値を持たないため、これを除外することはできません。

次の 2 つのインデックス タグは同じ UserID 値を持つ必要があることに注意してください。これにより、現在および次にマークされたグラニュールが完全に U1 UserID 値で構成されることが保証されます。次のタグが同じ UserID を持つというだけの場合、次のタグの URL 値は、異なる UserID を持つテーブル行から取得される可能性があります。上の図を見ると実際にこれが当てはまります。ここで、W2 は U2 からのものであり、W2 は U2 からのものであり、 U1 からのものではありません。OK。

これにより、最終的に ClickHouse が粒度 0 の最大 URL 値についての仮定を行うことができなくなります。代わりに、グラニュール 0 に URL 値 W3 の行が含まれる可能性があると想定する必要があり、トークン 0 の選択が強制されます。


マーカー 1、2、3 にも同じことが当てはまります。

結論は

クエリがユニオン主キーの列のサブセット (最初のキー列ではない) でフィルター処理する場合、前のキー列のカーディナリティが低い場合、ClickHouse によって (バイナリ検索ではなく) 使用される一般的な除外検索アルゴリズムが最も効果的に機能します。

この例のデータセットでは、両方のキー列 (UserID、URL) のカーディナリティが同様に高く、前述したように、URL 列の前のキー列のカーディナリティが高い場合、一般的な除外検索アルゴリズムはあまり効果的ではありません。

ジャンプ指数を見てみよう

UserID と URL はカーディナリティが高いため、URL に基づいてデータをフィルタリングすることはあまり効果的ではなく、 URL 列にセカンダリ ホップ カウント インデックスを作成してもあまり改善されません。

たとえば、次の 2 つのステートメントは、テーブルの URL 列にminmaxホップ カウント インデックスを作成して設定します。

<span style="color:#161517"><span style="background-color:var(--ifm-alert-background-color)"><span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">ALTER</span> <span style="color:#569cd6">TABLE</span> hits_UserID_URL <span style="color:#569cd6">ADD</span> <span style="color:#569cd6">INDEX</span> url_skipping_index URL <span style="color:#569cd6">TYPE</span> minmax GRANULARITY <span style="color:#b5cea8">4</span><span style="color:#d4d4d4">;</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ALTER</span> <span style="color:#569cd6">TABLE</span> hits_UserID_URL MATERIALIZE <span style="color:#569cd6">INDEX</span> url_skipping_index<span style="color:#d4d4d4">;</span>
</span></code></span></span></span></span></span>

ClickHouse は、URL の最小値と最大値を格納する追加のインデックスを作成します (4 つの連続するグラニュルの各グループ (上記の ALTER TABLE ステートメントの GRANULARITY 4 句に注意してください))。

最初のインデックス エントリ (上図のマーク 0) には、テーブルの最初の 4 つの顆粒に属する行の最小 URL 値と最大 URL 値が格納されます。

2 番目のインデックス エントリ (マーク 1) には、テーブル内の次の 4 つのグラニュールに属する行の最小 URL 値と最大 URL 値が格納されます。

(ClickHouse は、ホップ カウント インデックス用の特別なマーカー ファイルも作成します。これは、インデックス マーカーに関連付けられたグラニュールのグループを見つけるために使用されます。)

UserID と URL の基数が類似しているため、このセカンダリ ホップ カウント インデックスは、URL に対してクエリ フィルタリングを実行するときに選択されたパーティクルを除外するのに役立ちません。

検索されている特定の URL 値 (「http://public_search」) は、インデックスがグラニュールのセットごとに保存する最小値と最大値の間の値である可能性が高く、ClickHouse はこのセットを選択することを強制されます。グラニュール (一致するクエリ行が含まれる可能性があるため)。

したがって、特定の URL で行をフィルターするサンプル クエリを大幅に高速化したい場合は、そのクエリ用に最適化されたプライマリ インデックスを使用する必要があります。

また、特定の UserID で行をフィルタリングするサンプル クエリの良好なパフォーマンスを維持したい場合は、複数のプライマリ インデックスを使用する必要があります。

これを達成する方法は次のとおりです。

複数の主キーインデックスを使用したチューニング

2 つのサンプル クエリ (1 つは特定の UserID で行をフィルタリングするクエリ、もう 1 つは特定の URL で行をフィルタリングするクエリ) を大幅に高速化したい場合は、次の 3 つの方法のいずれかを使用して、複数のプライマリ インデックスを使用する必要があります。

  • 別の主キーを持つ新しいテーブルを作成します。
  • マテリアライズドビューを作成します。
  • 投影量を増やします。

3 つの方法はすべて、サンプル データを別のテーブルに効果的にコピーして、テーブルのプライマリ インデックスと行の並べ替え順序を再編成します。

ただし、3 つのオプションは、クエリおよび挿入ステートメントのために追加のテーブルがユーザーに対して透過的にルーティングされる程度が異なります。

異なる主キーを持つ 2 番目のテーブルを作成する場合は、クエリに最適なバージョンのテーブルにクエリを明示的に送信する必要があります。また、テーブルの同期を保つために新しいデータを両方のテーブルに明示的に挿入する必要があります。

マテリアライズド ビューでは、追加のテーブルが非表示になり、データは 2 つのテーブル間で自動的に同期されます。

この投影方法は、非表示の追加テーブルをデータ変更と自動的に同期させることに加えて、ClickHouse がクエリに対して最も効率的なテーブル バージョンを自動的に選択するため、最も透過的なオプションです。

以下では、実際の例を使用して、これら 3 つの方法について詳しく説明します。

セカンダリテーブルでのフェデレーテッド主キーインデックス

新しい追加テーブルを作成し、主キーのキー列の順序を (元のテーブルと比較して) 入れ替えます。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">CREATE</span> <span style="color:#569cd6">TABLE</span> hits_URL_UserID
</span><span style="color:#9cdcfe"><span style="color:#d4d4d4">(</span>
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">`</span>UserID<span style="color:#d4d4d4">`</span> UInt32<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">`</span>URL<span style="color:#d4d4d4">`</span> String<span style="color:#d4d4d4">,</span>
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">`</span>EventTime<span style="color:#d4d4d4">`</span> <span style="color:#569cd6">DateTime</span>
</span><span style="color:#9cdcfe"><span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ENGINE</span> <span style="color:#d4d4d4">=</span> MergeTree
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe"><span style="color:#569cd6">PRIMARY</span> <span style="color:#569cd6">KEY</span> <span style="color:#d4d4d4">(</span>URL<span style="color:#d4d4d4">,</span> UserID<span style="color:#d4d4d4">)</span>
</span></span><span style="color:#9cdcfe"><span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> <span style="color:#d4d4d4">(</span>URL<span style="color:#d4d4d4">,</span> UserID<span style="color:#d4d4d4">,</span> EventTime<span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe">SETTINGS index_granularity <span style="color:#d4d4d4">=</span> <span style="color:#b5cea8">8192</span><span style="color:#d4d4d4">,</span> index_granularity_bytes <span style="color:#d4d4d4">=</span> <span style="color:#b5cea8">0</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

887 万行のソーステーブルデータを書き込みます。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">INSERT</span> <span style="color:#569cd6">INTO</span> hits_URL_UserID
</span><span style="color:#9cdcfe"><span style="color:#569cd6">SELECT</span> <span style="color:#d4d4d4">*</span> <span style="color:#569cd6">from</span> hits_UserID_URL<span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">Ok.
</span>
<span style="color:#9cdcfe">0 rows in set. Elapsed: 2.898 sec. Processed 8.87 million rows, 838.84 MB (3.06 million rows/s., 289.46 MB/s.)
</span></code></span></span></span>

最後に最適化します。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">OPTIMIZE</span> <span style="color:#569cd6">TABLE</span> hits_URL_UserID FINAL<span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

主キーの列の順序を切り替えたため、挿入された行は (元のテーブルと比較して) 異なる辞書編集順でディスクに保存されるため、テーブルの 1083 グラニュールにも以前とは異なる値が含まれます。

主キーのインデックスは次のとおりです。

次に、URL「http://public_search」を最も頻繁にクリックする上位 10 人のユーザーを計算します。このときのクエリ速度は大幅に高速化されます。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">SELECT</span> UserID<span style="color:#d4d4d4">,</span> <span style="color:#dcdcaa">count</span><span style="color:#d4d4d4">(</span>UserID<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> Count
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe"><span style="color:#569cd6">FROM</span> hits_URL_UserID
</span></span><span style="color:#9cdcfe"><span style="color:#569cd6">WHERE</span> URL <span style="color:#d4d4d4">=</span> <span style="color:#ce9178">'http://public_search'</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">GROUP</span> <span style="color:#569cd6">BY</span> UserID
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> Count <span style="color:#569cd6">DESC</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">LIMIT</span> <span style="color:#b5cea8">10</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">┌─────UserID─┬─Count─┐
</span><span style="color:#9cdcfe">│ 2459550954 │  3741 │
</span><span style="color:#9cdcfe">│ 1084649151 │  2484 │
</span><span style="color:#9cdcfe">│  723361875 │   729 │
</span><span style="color:#9cdcfe">│ 3087145896 │   695 │
</span><span style="color:#9cdcfe">│ 2754931092 │   672 │
</span><span style="color:#9cdcfe">│ 1509037307 │   582 │
</span><span style="color:#9cdcfe">│ 3085460200 │   573 │
</span><span style="color:#9cdcfe">│ 2454360090 │   556 │
</span><span style="color:#9cdcfe">│ 3884990840 │   539 │
</span><span style="color:#9cdcfe">│  765730816 │   536 │
</span><span style="color:#9cdcfe">└────────────┴───────┘
</span>
<span style="color:#9cdcfe">10 rows in set. Elapsed: 0.017 sec.
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">Processed 319.49 thousand rows,
</span></span><span style="color:#9cdcfe">11.38 MB (18.41 million rows/s., 655.75 MB/s.)
</span></code></span></span></span>

現在はテーブル全体のスキャンがなくなり、ClickHouse はより効率的に実行されます。

元のテーブルのプライマリ インデックス (UserID が最初のキー列、URL が 2 番目のキー列) の場合、ClickHouse はインデックス トークンの汎用除外検索を使用してこのクエリを実行しますが、これはあまり効率的ではありません。 URL の割合も同様に高いです。

URL をメイン インデックスの最初の列として、ClickHouse はインデックス付きトークンに対して二分検索を実行するようになりました。ClickHouse サーバー ログ ファイル内の対応するトレース ログ:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">...Executor): Key condition: (column 0 in ['http://public_search',
</span><span style="color:#9cdcfe">                                           'http://public_search'])
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">...Executor): Running binary search on index range for part all_1_9_2 (1083 marks)
</span></span><span style="color:#9cdcfe">...Executor): Found (LEFT) boundary mark: 644
</span><span style="color:#9cdcfe">...Executor): Found (RIGHT) boundary mark: 683
</span><span style="color:#9cdcfe">...Executor): Found continuous range in 19 steps
</span><span style="color:#9cdcfe">...Executor): Selected 1/1 parts by partition key, 1 parts by primary key,
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">              39/1083 marks by primary key, 39 marks to read from 1 ranges
</span></span><span style="color:#9cdcfe">...Executor): Reading approx. 319488 rows with 2 streams
</span></code></span></span></span>

ClickHouse は、一般的な除外検索を使用するときに、1076 個ではなく 39 個のインデックス付きトークンのみを選択しました。

補助テーブルは、URL に対するサンプル クエリ フィルタリングの実行を高速化するために最適化されていることに注意してください。

URL をクエリしてフィルタリングする前と同様に、ここで補助テーブルの UserID をクエリしてフィルタリングすると、パフォーマンスも低下します。これは、UserID が 2 番目のプライマリ インデックス キー列であるため、ClickHouse は一般的な除外検索アルゴリズムを使用してパーティクルを検索するためです。 、これは同様に高い Cardinal UserID と URL の場合、あまり効率的ではありません。

詳細については、以下をクリックしてください。

UserID に対するクエリ フィルタリングのパフォーマンスが低い

これでテーブルが 2 つになりました。UserID と URL それぞれに対して最適化されたクエリ フィルタリング:

マテリアライズド・ビューでフェデレーテッド主キー

元のテーブルにマテリアライズド ビューを作成します。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">CREATE</span> MATERIALIZED <span style="color:#569cd6">VIEW</span> mv_hits_URL_UserID
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ENGINE</span> <span style="color:#d4d4d4">=</span> MergeTree<span style="color:#d4d4d4">(</span><span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">PRIMARY</span> <span style="color:#569cd6">KEY</span> <span style="color:#d4d4d4">(</span>URL<span style="color:#d4d4d4">,</span> UserID<span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> <span style="color:#d4d4d4">(</span>URL<span style="color:#d4d4d4">,</span> UserID<span style="color:#d4d4d4">,</span> EventTime<span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe">POPULATE
</span><span style="color:#9cdcfe"><span style="color:#569cd6">AS</span> <span style="color:#569cd6">SELECT</span> <span style="color:#d4d4d4">*</span> <span style="color:#569cd6">FROM</span> hits_UserID_URL<span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">Ok.
</span>
<span style="color:#9cdcfe">0 rows in set. Elapsed: 2.935 sec. Processed 8.87 million rows, 838.84 MB (3.02 million rows/s., 285.84 MB/s.)
</span></code></span></span></span>
ノート
  • ビューの主キーのキー列の順序を(元のテーブルと比較して)切り替えます。
  • マテリアライズド ビューは、指定された主キーに基づいて行順序と主インデックスが定義される隠しテーブルによってサポートされます。
  • POPULATE キーワードを使用して、ソース テーブル Hist_UserID_URL から 887 万行すべてを含む新しいマテリアライズド ビューをすぐにインポートします。
  • 新しい行がソース テーブル Hist_UserID_URL に挿入されると、それらの行は非表示テーブルにも自動的に挿入されます。
  • 実際、暗黙的に作成された隠しテーブルには、上記で明示的に作成した補助テーブルと同じ行順序とプライマリ インデックスがあります。

ClickHouse は、隠しテーブルの列データ ファイル (.bin)、マーカー ファイル (.mrk2)、およびプライマリ インデックス (primary.idx) を、ClickHouse サーバーのデータ ディレクトリ内の特別なフォルダーに保存します。

マテリアライズド ビューの背後にある非表示テーブル (およびそのプライマリ インデックス) を使用して、URL 列に対するクエリ フィルタリングの実行を大幅に高速化できるようになりました。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">SELECT</span> UserID<span style="color:#d4d4d4">,</span> <span style="color:#dcdcaa">count</span><span style="color:#d4d4d4">(</span>UserID<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> Count
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe"><span style="color:#569cd6">FROM</span> mv_hits_URL_UserID
</span></span><span style="color:#9cdcfe"><span style="color:#569cd6">WHERE</span> URL <span style="color:#d4d4d4">=</span> <span style="color:#ce9178">'http://public_search'</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">GROUP</span> <span style="color:#569cd6">BY</span> UserID
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> Count <span style="color:#569cd6">DESC</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">LIMIT</span> <span style="color:#b5cea8">10</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">┌─────UserID─┬─Count─┐
</span><span style="color:#9cdcfe">│ 2459550954 │  3741 │
</span><span style="color:#9cdcfe">│ 1084649151 │  2484 │
</span><span style="color:#9cdcfe">│  723361875 │   729 │
</span><span style="color:#9cdcfe">│ 3087145896 │   695 │
</span><span style="color:#9cdcfe">│ 2754931092 │   672 │
</span><span style="color:#9cdcfe">│ 1509037307 │   582 │
</span><span style="color:#9cdcfe">│ 3085460200 │   573 │
</span><span style="color:#9cdcfe">│ 2454360090 │   556 │
</span><span style="color:#9cdcfe">│ 3884990840 │   539 │
</span><span style="color:#9cdcfe">│  765730816 │   536 │
</span><span style="color:#9cdcfe">└────────────┴───────┘
</span>
<span style="color:#9cdcfe">10 rows in set. Elapsed: 0.026 sec.
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">Processed 335.87 thousand rows,
</span></span><span style="color:#9cdcfe">13.54 MB (12.91 million rows/s., 520.38 MB/s.)
</span></code></span></span></span>

マテリアライズド ビューの背後にある隠しテーブル (およびそのプライマリ インデックス) は、実際には明示的に作成した補助テーブルと同じであるため、クエリは明示的に作成されたテーブルと同じ方法で実行されます。

ClickHouse サーバー ログ ファイル内の対応するトレース ログは、ClickHouse がインデックス トークンに対してバイナリ検索を実行していることを確認します。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">...Executor): Key condition: (column 0 in ['http://public_search',
</span><span style="color:#9cdcfe">                                           'http://public_search'])
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">...Executor): Running binary search on index range ...
</span></span><span style="color:#9cdcfe">...
</span><span style="color:#9cdcfe">...Executor): Selected 4/4 parts by partition key, 4 parts by primary key,
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">              41/1083 marks by primary key, 41 marks to read from 4 ranges
</span></span><span style="color:#9cdcfe">...Executor): Reading approx. 335872 rows with 4 streams
</span></code></span></span></span>

プロジェクションによる結合主キーインデックス

プロジェクションは現在実験的な機能であるため、ClickHouse に次のように指示する必要があります。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">SET</span> optimize_use_projections <span style="color:#d4d4d4">=</span> <span style="color:#b5cea8">1</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

元のテーブルにプロジェクションを作成します。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">ALTER</span> <span style="color:#569cd6">TABLE</span> hits_UserID_URL
</span><span style="color:#9cdcfe">    <span style="color:#569cd6">ADD</span> PROJECTION prj_url_userid
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">(</span>
</span><span style="color:#9cdcfe">        <span style="color:#569cd6">SELECT</span> <span style="color:#d4d4d4">*</span>
</span><span style="color:#9cdcfe">        <span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> <span style="color:#d4d4d4">(</span>URL<span style="color:#d4d4d4">,</span> UserID<span style="color:#d4d4d4">)</span>
</span><span style="color:#9cdcfe">    <span style="color:#d4d4d4">)</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

具体化された投影:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">ALTER</span> <span style="color:#569cd6">TABLE</span> hits_UserID_URL
</span><span style="color:#9cdcfe">    MATERIALIZE PROJECTION prj_url_userid<span style="color:#d4d4d4">;</span>
</span></code></span></span></span>
ノート
  • プロジェクションは、行順序とプライマリ インデックスがプロジェクションの指定された order BY 句に基づく非表示テーブルを作成しています。
  • MATERIALIZE キーワードを使用して、887 万行すべてを含む隠しテーブルをソース テーブル Hist_UserID_URL から即座にインポートします。
  • 新しい行がソース テーブル Hist_UserID_URL に挿入されると、それらの行は非表示テーブルにも自動的に挿入されます。
  • クエリは常に (構文的に) ソース テーブル Hist_UserID_URL に対して行われますが、行順序とプライマリ インデックスによりクエリをより効率的に実行できる場合は、非表示テーブルが使用されます。
  • 実際、暗黙的に作成された隠しテーブルには、明示的に作成された補助テーブルと同じ行順序とプライマリ インデックスがあります。

ClickHouse は、非表示のテーブルの列データ ファイル (.bin)、マーカー ファイル (.mrk2)、およびプライマリ インデックス (primary.idx) を、ソース テーブルのデータ ファイルの隣にある特別なフォルダー (下のスクリーンショットでオレンジ色でマークされている) に保存します。タグ ファイルとメイン インデックス ファイル:

プロジェクションによって作成された隠しテーブル (およびそのプライマリ インデックス) を (暗黙的に) 使用して、URL 列に対するクエリ フィルタリングの実行を大幅に高速化できるようになりました。クエリは構文的に、投影されたソース テーブルに対して指示されることに注意してください。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:var(--ifm-pre-color)"><code><span style="color:#9cdcfe"><span style="color:#569cd6">SELECT</span> UserID<span style="color:#d4d4d4">,</span> <span style="color:#dcdcaa">count</span><span style="color:#d4d4d4">(</span>UserID<span style="color:#d4d4d4">)</span> <span style="color:#569cd6">AS</span> Count
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe"><span style="color:#569cd6">FROM</span> hits_UserID_URL
</span></span><span style="color:#9cdcfe"><span style="color:#569cd6">WHERE</span> URL <span style="color:#d4d4d4">=</span> <span style="color:#ce9178">'http://public_search'</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">GROUP</span> <span style="color:#569cd6">BY</span> UserID
</span><span style="color:#9cdcfe"><span style="color:#569cd6">ORDER</span> <span style="color:#569cd6">BY</span> Count <span style="color:#569cd6">DESC</span>
</span><span style="color:#9cdcfe"><span style="color:#569cd6">LIMIT</span> <span style="color:#b5cea8">10</span><span style="color:#d4d4d4">;</span>
</span></code></span></span></span>

結果:

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">┌─────UserID─┬─Count─┐
</span><span style="color:#9cdcfe">│ 2459550954 │  3741 │
</span><span style="color:#9cdcfe">│ 1084649151 │  2484 │
</span><span style="color:#9cdcfe">│  723361875 │   729 │
</span><span style="color:#9cdcfe">│ 3087145896 │   695 │
</span><span style="color:#9cdcfe">│ 2754931092 │   672 │
</span><span style="color:#9cdcfe">│ 1509037307 │   582 │
</span><span style="color:#9cdcfe">│ 3085460200 │   573 │
</span><span style="color:#9cdcfe">│ 2454360090 │   556 │
</span><span style="color:#9cdcfe">│ 3884990840 │   539 │
</span><span style="color:#9cdcfe">│  765730816 │   536 │
</span><span style="color:#9cdcfe">└────────────┴───────┘
</span>
<span style="color:#9cdcfe">10 rows in set. Elapsed: 0.029 sec.
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">Processed 319.49 thousand rows, 1
</span></span><span style="color:#9cdcfe">1.38 MB (11.05 million rows/s., 393.58 MB/s.)
</span></code></span></span></span>

プロジェクションによって作成された隠しテーブル (およびそのプライマリ インデックス) は、明示的に作成したセカンダリ テーブルと事実上同じであるため、クエリは明示的に作成されたテーブルと同じ方法で実行されます。

ClickHouse サーバー ログ ファイルのトレース ログは、ClickHouse がインデックス付きトークンに対してバイナリ検索を実行していることを確認します。

<span style="color:var(--prism-color)"><span style="background-color:var(--ifm-pre-background)"><span style="color:#dc143c"><code><span style="color:#9cdcfe">...Executor): Key condition: (column 0 in ['http://public_search',
</span><span style="color:#9cdcfe">                                           'http://public_search'])
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">...Executor): Running binary search on index range for part prj_url_userid (1083 marks)
</span></span><span style="color:#9cdcfe">...Executor): ...
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">...Executor): Choose complete Normal projection prj_url_userid
</span></span><span style="color:#9cdcfe">...Executor): projection required columns: URL, UserID
</span><span style="color:#9cdcfe">...Executor): Selected 1/1 parts by partition key, 1 parts by primary key,
</span><span style="background-color:var(--docusaurus-highlighted-code-line-bg)"><span style="color:#9cdcfe">              39/1083 marks by primary key, 39 marks to read from 1 ranges
</span></span><span style="color:#9cdcfe">...Executor): Reading approx. 319488 rows with 2 streams
</span></code></span></span></span>

無効な主キー

結合主キー (UserID、URL) を持つテーブルの主インデックスは、UserID に対するクエリ フィルタリングを高速化するのに役立ちます。ただし、URL 列は複合主キーの一部ではありますが、このインデックスは URL クエリのフィルタリングを高速化する上で大きな助けにはなりません。

逆も同様です。複合主キー (URL、UserID) を持つテーブルの主インデックスは、URL に対するクエリ フィルタリングを高速化しますが、UserID に対するクエリ フィルタリングはあまりサポートしません。

主キー列の UserID と URL のカーディナリティも高いため、2 番目のキー列でフィルタリングするクエリには、インデックスに 2 番目のキー列があることによるメリットはあまりありません。

したがって、プライマリ インデックスから 2 番目のキー列を削除し (これにより、インデックスのメモリ消費量が削減され)、複数のプライマリ インデックスを使用することが合理的です。

ただし、複合主キー内のキー列のカーディナリティが大幅に異なる場合、クエリでは主キー列をカーディナリティの昇順で並べ替えることが有益です。

主キーのキー列間のカーディナリティの違いが大きいほど、主キーのキー列の順序が重要になります。

おすすめ

転載: blog.csdn.net/leesinbad/article/details/131604202