プロジェクション
はじめに
ClickHouseは、大量のデータに対する分析クエリをリアルタイムで高速化するさまざまなメカニズムを提供しています。そのようなメカニズムの1つが、_プロジェクション_を使用することです。プロジェクションは、関心のある属性によってデータの並べ替えを行うことでクエリを最適化します。これには次のようなものが含まれます:
- 完全な並べ替え
- 元のテーブルのサブセットで別の順序
- 事前に計算された集約(Materialized Viewに似ています)が、集約に沿った順序を持ちます。
プロジェクションはどのように機能しますか?
実際には、プロジェクションは元のテーブルに対する追加の隠れたテーブルと考えることができます。プロジェクションは異なる行の順序を持つことができるため、元のテーブルとは異なる主キーを持ち、自動的に増分的に集約値を事前計算することができます。その結果、プロジェクションを利用することでクエリの実行を高速化するための2つの「調整ノブ」が提供されます:
- 主インデックスの適切な使用
- 集約の事前計算
プロジェクションは、複数の行の順序を持ち、挿入時に集約を事前計算できるMaterialized Viewsと、ある意味似ています。プロジェクションは自動的に更新され、元のテーブルと同期されます。一方、Materialized Viewsは明示的に更新されます。クエリが元のテーブルをターゲットにすると、ClickHouseは自動的に主キーをサンプリングし、同じ正しい結果を生成できるテーブルを選択しますが、読み取るデータの量が最も少ないものを選びます。以下の図に示すように:

プロジェクションを使用するタイミングは?
プロジェクションは、自動的にデータが挿入されるため、新しいユーザーにとって魅力的な機能です。さらに、クエリは単一のテーブルに送信され、可能な限りプロジェクションを利用して応答時間を短縮できます。
これは、ユーザーが適切な最適化されたターゲットテーブルを選択する必要があるMaterialized Viewsとは対照的です。この場合、フィルターに応じてクエリを再構築する必要があります。これにより、ユーザーアプリケーションへの重要性が増し、クライアントサイドの複雑性が増加します。
これらの利点にもかかわらず、プロジェクションにはいくつかの固有の制限があり、ユーザーはこれを認識し、したがって慎重に展開すべきです。
- プロジェクションは、ソーステーブルと(隠れた)ターゲットテーブルに対して異なるTTLを使用することを許可しませんが、Materialized Viewsは異なるTTLを許可します。
- プロジェクションは現在、(隠れた)ターゲットテーブルに対して
optimize_read_in_order
をサポートしていません。 - プロジェクションを持つテーブルに対しては、軽量更新と削除がサポートされていません。
- Materialized Viewsはチェーン化できます:1つのMaterialized Viewのターゲットテーブルは、別のMaterialized Viewのソーステーブルになり得ますが、これはプロジェクションでは不可能です。
- プロジェクションは結合をサポートしていませんが、Materialized Viewsはサポートしています。
- プロジェクションはフィルター(
WHERE
句)をサポートしていませんが、Materialized Viewsはサポートしています。
プロジェクションを使用することをお勧めするのは次のような場合です:
- データの完全な再構成が必要な場合。プロジェクションの式は理論上
GROUP BY
を使用できますが、集約を維持するにはMaterialized Viewsがより効果的です。クエリオプティマイザーは、単純な並べ替えを使用するプロジェクションを利用する可能性が高いです。つまり、SELECT * ORDER BY x
のようになります。この式で、ストレージフットプリントを減らすために列のサブセットを選択できます。 - ユーザーがストレージフットプリントの増加とデータの二重書き込みのオーバーヘッドに対して快適である場合。挿入速度に対する影響をテストし、ストレージオーバーヘッドを評価する。
例
主キーに含まれていないカラムでのフィルタリング
この例では、テーブルにプロジェクションを追加する方法を示します。また、主キーに含まれていないカラムでフィルターを行うクエリを高速化するためにプロジェクションを使用できる方法も見ていきます。
この例では、pickup_datetime
の順序で整理された New York Taxi Data データセットを使用します。このデータセットは sql.clickhouse.com で利用可能です。
では、$200以上チップを渡した乗客の全旅行IDを見つける簡単なクエリを書いてみましょう:
ORDER BY
に含まれていない tip_amount
でフィルタリングしているため、ClickHouseは全行スキャンを行う必要があったことに注意してください。このクエリを高速化しましょう。
元のテーブルと結果を保持するために、新しいテーブルを作成し、INSERT INTO SELECT
を使用してデータをコピーします:
プロジェクションを追加するには、ALTER TABLE
ステートメントと ADD PROJECTION
ステートメントを使用します:
プロジェクションを追加した後、MATERIALIZE PROJECTION
ステートメントを使用して、指定されたクエリに従って物理的にデータが順序づけられて書き直される必要があります:
プロジェクションを追加したので、クエリを再度実行しましょう:
クエリ時間を大幅に短縮でき、スキャンする行数が少なくて済んだことに気づくでしょう。
上記のクエリが実際に作成したプロジェクションを使用したことを確認するために、system.query_log
テーブルをクエリします:
UKの支払額クエリを高速化するためのプロジェクションの使用
プロジェクションがクエリパフォーマンスを高速化するためにどのように使用できるかを示すために、実際のデータセットを使用した例を見てみましょう。この例では、私たちのUK Property Price Paid チュートリアルのテーブルを使用します。これは3003万行のデータセットです。このデータセットは、私たちのsql.clickhouse.com環境内でも利用できます。
テーブルが作成され、データが挿入される方法を確認したい場合は、"UK不動産価格データセット" ページを参照してください。
このデータセットに対して2つの簡単なクエリを実行できます。最初のクエリはロンドン内の支払いが最も高い郡をリストし、2番目は郡の平均価格を計算します:
注意してください。両方のクエリの結果、30.03百万行全体のフルテーブルスキャンが発生しました。これは、テーブルを作成したときに town
および price
が ORDER BY
ステートメントに含まれていなかったためです:
プロジェクションを使用してこのクエリを高速化できるか見てみましょう。
元のテーブルと結果を保持するために、新しいテーブルを作成し、INSERT INTO SELECT
を使用してデータをコピーします:
prj_obj_town_price
というプロジェクションを作成し、町と価格で並べ替えた主キーを持つ追加の(隠れた)テーブルを生成します。これにより、特定の町での支払額が最も高い郡をリスト化するクエリを最適化します:
mutations_sync
設定は、同期実行を強制するために使用されます。
prj_gby_county
という別のプロジェクションを作成し、既存の130のイギリスの郡のaverage(price)集約値を段階的に事前計算する追加の(隠れた)テーブルを構築します:
プロジェクションに GROUP BY
句が使用されている場合(上記の prj_gby_county
プロジェクションのように)、その隠れたテーブルの基になるストレージエンジンは AggregatingMergeTree
となり、すべての集約関数が AggregateFunction
に変換されます。これは、適切な増分データ集約を保証します。
下の図は、主テーブル uk_price_paid_with_projections
とその2つのプロジェクションの可視化です:

ロンドンの支払いが最も高い価格の郡をリスト化するクエリを再度実行すると、クエリパフォーマンスに改善が見られます:
同様に、イギリスの郡での平均支払額が最も高い3つをリスト화するクエリについても:
両方のクエリは元のテーブルをターゲットにし、また両方のクエリはフルテーブルスキャンを行ったことに注意してください(30.03百万行すべてがディスクからストリーミングされました)。プロジェクションを2つ作成する前に。
また、ロンドンの支払いが最も高い価格の郡をリスト化するクエリは2.17百万行をストリーミングしています。直接、最適化された2つ目のテーブルを使用した場合、ディスクからストリーミングされたのはわずか81.92千行でした。
この差の理由は、現在、上記の optimize_read_in_order
最適化がプロジェクションにはサポートされていないためです。
system.query_log
テーブルを調べると、ClickHouseが上記の2つのクエリに対して自動的に2つのプロジェクションを使用したことがわかります(下のプロジェクション列を参照):
さらなる例
以下の例では、同じUK価格データセットを使用して、プロジェクションありとなしのクエリを対比させます。
オリジナルテーブル(およびパフォーマンス)を保存するために、再度 CREATE AS
と INSERT INTO SELECT
を使用してテーブルのコピーを作成します。
プロジェクションを構築
toYear(date)
、district
、town
の次元ごとに集約プロジェクションを作成します:
既存のデータに対してプロジェクションをポピュレートします。(物理的に指定された順序でデータは書き直されません。これにより、新たに挿入されたデータのみに対してプロジェクションが作成されます):
以下のクエリは、プロジェクションの有無によるパフォーマンスの対比です。プロジェクションを強制的に無効にするには、設定 optimize_use_projections
を使用します。これはデフォルトで有効になっています。
クエリ1. 年ごとの平均価格
結果は同様であるべきですが、後者の例の方がパフォーマンスが向上します!
クエリ2. ロンドン年ごとの平均価格
クエリ3. 最も高価な地区
条件 (date >= '2020-01-01') は、プロジェクションの次元 (toYear(date) >= 2020) に一致するように変更する必要があります:
再び、結果は同じですが、2番目のクエリのクエリパフォーマンスの改善に気づいてください。