はじめに
- 完全な並べ替え
- 元のテーブルの一部を異なる順序で並べ替えたもの
- 事前計算された集約 (materialized view に似ています) で、集約に合わせた順序付けが されているもの
プロジェクションはどのように機能しますか?
- プライマリインデックスを適切に活用する
- 集計を事前計算する
_part_offset によるより効率的なストレージ
_part_offset をサポートしており、
これによりプロジェクションを定義する新しい方法が利用できるようになりました。
現在、プロジェクションを定義する方法は 2 つあります。
- 完全なカラムを保存する (従来の動作) : プロジェクションに完全な データを保持し、それを直接読み取れるため、フィルタが プロジェクションのソート順に一致する場合は、より高速に処理できます。
-
ソートキー +
_part_offsetのみを保存する: プロジェクションは索引のように機能します。 ClickHouse はプロジェクションのプライマリインデックスを使って一致する行を特定し、実際のデータは基となるテーブルから 読み取ります。これにより、クエリ時の I/O が わずかに増える代わりに、ストレージのオーバーヘッドを削減できます。
_part_offset を介して間接的に扱えます。
Projections を使用すべきケース
- Projections では、ソーステーブルと (隠し) ターゲットテーブルに異なる有効期限 (TTL) を設定できませんが、 materialized view では異なる有効期限 (TTL) を設定できます。
- Projections を持つテーブルでは、論理更新と削除はサポートされません。
- materialized view は連鎖できます。つまり、ある materialized view のターゲットテーブルを別の materialized view のソーステーブルにする、といった構成が可能です。これは Projections ではできません。
- Projection の定義では JOIN はサポートされませんが、materialized view ではサポートされます。ただし、Projections を持つテーブルに対するクエリでは JOIN を自由に使用できます。
- Projection の定義ではフィルター (
WHEREclause) はサポートされませんが、materialized view ではサポートされます。ただし、Projections を持つテーブルに対するクエリでは自由にフィルタリングできます。
- データを完全に再並べ替える必要がある場合です。理論上、projection 内の式で
GROUP BY,を使用することもできますが、集計の維持には materialized view のほうが 効果的です。また、クエリオプティマイザーも、単純な再並べ替えを行う projection、 すなわちSELECT * ORDER BY xを使用する projection を活用しやすい傾向があります。 この式では、ストレージ使用量を抑えるためにカラムの一部だけを選択できます。 - ストレージ使用量が増える可能性と、 データを二重に書き込むオーバーヘッドを許容できる場合です。挿入速度への影響を検証し、 ストレージのオーバーヘッドを評価してください。
例
主キーに含まれていないカラムでのフィルタリング
pickup_datetime で並べ替えられています。
まず、乗客がドライバーに $200 を超えるチップを支払ったすべての trip ID を見つける
シンプルなクエリを書いてみましょう。
ORDER BY に含まれていない tip_amount でフィルタしているため、ClickHouse は
テーブル全体をスキャンする必要があります。このクエリを高速化してみましょう。
元のテーブルと結果を保持するため、新しいテーブルを作成し、INSERT INTO SELECT を使ってデータをコピーします。
ALTER TABLEステートメントとADD PROJECTION
ステートメントを使用します。
MATERIALIZE PROJECTION
ステートメントを使用する必要があります。
system.query_log テーブルにクエリを実行することで確認できます。
PROJECTIONを使用して UK price paid クエリを高速化する
ORDER BY ステートメントに town も price も含まれていなかったため、いずれのクエリでも 3,003 万行すべてに対するフルテーブルスキャンが発生している点に注目してください。
INSERT INTO SELECT を使ってデータをコピーします:
prj_oby_town_price というPROJECTIONを作成してデータを投入します。これにより、
町名と価格で並べ替えたプライマリインデックスを持つ追加の (非表示の) テーブルが生成され、
特定の町における最高価格の取引について郡を一覧表示するクエリを最適化できます。
mutations_sync 設定は、
同期実行を強制するために使用されます。
PROJECTION prj_gby_county を作成してデータを投入します。これは追加の (非表示の) テーブルであり、
英国にある既存の130のすべてのカウンティについて、avg(price) の集計値を段階的に事前計算します。
上記の
prj_gby_county PROJECTIONのように、PROJECTIONで GROUP BY 句が使われている場合、
(隠し) テーブルの基盤となるストレージエンジンは AggregatingMergeTree
となり、すべての集約関数は
AggregateFunction に変換されます。これにより、データの増分集約が適切に行われます。uk_price_paid_with_projections
と、その 2 つのPROJECTIONを可視化したものです。
ここで、ロンドンの county のうち価格が最も高い 3 件を
一覧表示するクエリを再度実行すると、クエリパフォーマンスが改善していることがわかります。
同様に、平均支払価格が最も高いイギリスの county を 3 件
一覧表示するクエリでも改善が見られます。
両方のクエリはいずれも元のテーブルを対象としており、2 つのプロジェクションを作成する前は、どちらのクエリでもフルテーブルスキャンが発生していたことに注意してください (3,003 万行すべてがディスクからストリーミングされました) 。
また、支払価格が最も高い 3 件について London の counties を一覧表示するクエリでは、217 万行がストリーミングされている点にも注意してください。このクエリ用に最適化した 2 つ目のテーブルを直接使用した場合は、ディスクからストリーミングされたのは 8.192 万行だけでした。
この違いが生じる理由は、前述の optimize_read_in_order 最適化が、現時点ではプロジェクションではサポートされていないためです。
system.query_log テーブルを調べると、ClickHouse が上記 2 つのクエリに対して 2 つのプロジェクションを自動的に使用していたことがわかります (下の projections カラムを参照) :
さらに例を見ていきます
CREATE AS と INSERT INTO SELECT を使用してテーブルのコピーを作成します。
プロジェクションを作成する
toYear(date)、district、town の各次元で集計プロジェクションを作成しましょう。
optimize_use_projections を使用します。
クエリ 1. 年ごとの平均価格
クエリ 2. ロンドンの年別平均価格
クエリ 3. 最も高額な地域
(date >= '2020-01-01') は、projection の dimension (toYear(date) >= 2020) に合うように変更する必要があります。
今回も結果は同じですが、2 番目のクエリではクエリのパフォーマンスが向上していることがわかります。
1つのクエリで複数のプロジェクションを組み合わせる
_part_offset サポートを基盤として、ClickHouse では複数のフィルターを含む1つのクエリの高速化に複数のプロジェクションを利用できるようになりました。
重要なのは、ClickHouse は引き続き 1 つのプロジェクション (または基となるテーブル) からしかデータを読み取りませんが、読み取り前に不要なパーツを絞り込むために、ほかのプロジェクションのプライマリインデックスを利用できるという点です。
これは特に、複数のカラムでフィルタリングするクエリで有効です。各カラムがそれぞれ異なるプロジェクションに対応している可能性があるためです。
現在、この仕組みで絞り込めるのはパーツ全体のみです。グラニュール単位の絞り込みは、まだサポートされていません。これを示すため、 (
_part_offset カラムを使うプロジェクションを含む) テーブルを定義し、上の図に対応する 5 つの行の例を挿入します。
注: このテーブルでは説明のため、1 行ごとの granule や無効化した part merge などのカスタム設定を使用していますが、これらは本番環境では推奨されません。
- 5 つの個別のパーツ (挿入した各行につき 1 つ)
- 各行に対して 1 つのプライマリインデックスのエントリ (基となるテーブルと各プロジェクション)
- 各パーツにはちょうど 1 行だけが含まれる
region と user_id の両方で絞り込むクエリを実行します。
基となるテーブルのプライマリインデックスは event_date と id から構築されているため、
この場合は役に立ちません。そのため ClickHouse は次を使用します。
region_projを使って region でパーツを絞り込むuser_id_projを使ってuser_idでさらに絞り込む
EXPLAIN projections = 1 を使うと確認でき、
ClickHouse がどのようにプロジェクションを選択して適用するかが示されます。
EXPLAIN の出力 (上記参照) には、論理クエリプランが上から下の順に示されています。
最終的に、基となるテーブルから読み取られるのは 5 つのパーツのうち 1 つだけです。
複数のプロジェクションの索引解析を組み合わせることで、ClickHouse はスキャン対象のデータ量を大幅に削減し、
ストレージのオーバーヘッドを低く抑えながらパフォーマンスを向上させます。